///|
/// 布局推理引擎:Agent 用自然语言说"居中对齐按钮",引擎推断参数并执行。
///
/// 这不是 NLP——是**结构化的约束语言解析**:
/// 引擎识别设计意图关键词(居中/排列/等宽/间距/缩放),
/// 匹配到对应的布局操作,返回可直接执行的动作。
///
/// 支持 12 种意图:
/// 居中 / 垂直排列 / 水平排列 / 等宽 / 等高 / 等间距 /
/// 网格对齐 / 移到顶部 / 移到底部 / 整体缩放 / 增加边距 / 自动布局
pub(all) enum LayoutIntent {
CenterNode(String) // "居中 btn" → CenterInParent
VerticalStack // "垂直排列" → StackLayout(Vertical)
HorizontalRow // "水平排列" → StackLayout(Horizontal)
EqualWidth // "等宽" → 所有 width = Fill
EqualHeight // "等高" → 所有 height = Fill
EqualSpacing // "等间距" → auto_arrange
GridAlign(Double) // "网格 8" → snap_to_grid(8)
MoveTop // "顶部" → y = 0 或容器顶部
MoveBottom // "底部" → y = 容器底部
MoveCenter // "垂直居中" → 整体居中
ScaleAll(Double) // "放大 1.5" → 全部尺寸 × 1.5
SetPadding(Double) // "边距 16" → 容器 padding = 16
SetGap(Double) // "间距 12" → StackLayout.gap = 12
Unknown // 无法识别
}
///|
pub fn LayoutIntent::describe(self : LayoutIntent) -> String {
match self {
CenterNode(id) => "center(\{id})"
VerticalStack => "vertical_stack"
HorizontalRow => "horizontal_row"
EqualWidth => "equal_width"
EqualHeight => "equal_height"
EqualSpacing => "equal_spacing"
GridAlign(n) => "grid(\{n})"
MoveTop => "move_top"
MoveBottom => "move_bottom"
MoveCenter => "move_center"
ScaleAll(f) => "scale(\{f})"
SetPadding(v) => "padding(\{v})"
SetGap(v) => "gap(\{v})"
Unknown => "unknown"
}
}
// ---------------------------------------------------------------------------
// 约束语言解析
// ---------------------------------------------------------------------------
///|
/// 解析自然语言约束 → LayoutIntent
/// Agent 传入 "居中对齐按钮" / "垂直排列" / "等宽" 等中文或英文
pub fn parse_layout_intent(text : String) -> LayoutIntent {
let lower = text.to_lower()
// 居中
if lower.contains("居中") || lower.contains("center") {
if lower.contains("垂直") || lower.contains("vertical") {
return MoveCenter
}
return CenterNode("")
}
// 排列
if lower.contains("垂直排列") ||
lower.contains("vertical") ||
lower.contains("纵向") {
return VerticalStack
}
if lower.contains("水平排列") ||
lower.contains("horizontal") ||
lower.contains("横向") {
return HorizontalRow
}
// 等宽 / 等高
if lower.contains("等宽") || lower.contains("equal width") {
return EqualWidth
}
if lower.contains("等高") || lower.contains("equal height") {
return EqualHeight
}
// 等间距
if lower.contains("等间距") ||
lower.contains("equal spacing") ||
lower.contains("均匀") {
return EqualSpacing
}
// 网格
if lower.contains("网格") ||
lower.contains("grid") ||
lower.contains("吸附") ||
lower.contains("snap") {
let grid = extract_number(lower)
return GridAlign(if grid > 0.0 { grid } else { 8.0 })
}
// 顶部 / 底部
if lower.contains("顶部") || lower.contains("top") {
return MoveTop
}
if lower.contains("底部") || lower.contains("bottom") {
return MoveBottom
}
// 缩放
if lower.contains("放大") ||
lower.contains("scale up") ||
lower.contains("增大") {
let f = extract_number(lower)
return ScaleAll(if f > 0.0 { f } else { 1.25 })
}
if lower.contains("缩小") || lower.contains("scale down") {
let f = extract_number(lower)
return ScaleAll(if f > 0.0 { 1.0 / f } else { 0.75 })
}
// 边距
if lower.contains("边距") ||
lower.contains("padding") ||
lower.contains("内边") {
let v = extract_number(lower)
return SetPadding(if v > 0.0 { v } else { 16.0 })
}
// 间距
if lower.contains("间距") ||
lower.contains("gap") ||
lower.contains("间隔") {
let v = extract_number(lower)
return SetGap(if v > 0.0 { v } else { 8.0 })
}
Unknown
}
///|
/// 从文本中提取数字(支持 "8px"、"1.5倍"、"间距 16")
fn extract_number(text : String) -> Double {
let mut result = 0.0
let mut has_dot = false
let mut frac = 0.1
let mut found = false
for c in text.iter() {
let code = c.to_int()
if code >= 48 && code <= 57 { // 数字
let d = (code - 48).to_double()
if has_dot {
result = result + d * frac
frac = frac * 0.1
} else {
result = result * 10.0 + d
}
found = true
} else if code == 46 && found { // 小数点(且前面已有数字)
has_dot = true
} else if found {
break // 遇到非数字且已有数字 → 停止
}
}
if found {
result
} else {
0.0
}
}
// ---------------------------------------------------------------------------
// 意图执行
// ---------------------------------------------------------------------------
///|
/// 执行解析后的布局意图
pub fn Project::apply_intent(
self : Project,
intent : LayoutIntent,
artboard~ : String,
parent_id? : String = "root",
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) =>
match intent {
CenterNode(node_id) => {
let target = if node_id == "" {
find_first_child(ab.doc, parent_id)
} else {
node_id
}
match target {
"" => Err("no_node_to_center")
id => self.center_in_parent(artboard~, node_id=id)
}
}
VerticalStack => {
// 设置父容器为垂直栈布局
let _ = ab.doc.update_node(parent_id, fn(n) {
{
..n,
layout: Some({
dir: Vertical,
gap: 8.0,
justify: Start,
padding: Padding::uniform(16.0),
}),
}
})
Ok(
"{\"intent\":\"vertical_stack\",\"applied\":true,\"parent\":\"\{parent_id}\"}",
)
}
HorizontalRow => {
let _ = ab.doc.update_node(parent_id, fn(n) {
{
..n,
layout: Some({
dir: Horizontal,
gap: 8.0,
justify: Start,
padding: Padding::uniform(16.0),
}),
}
})
Ok(
"{\"intent\":\"horizontal_row\",\"applied\":true,\"parent\":\"\{parent_id}\"}",
)
}
EqualWidth => {
// 所有子节点 width = Fill
let count = equalize_width(ab.doc, parent_id)
Ok("{\"intent\":\"equal_width\",\"applied\":\{count}}")
}
EqualHeight => {
let count = equalize_height(ab.doc, parent_id)
Ok("{\"intent\":\"equal_height\",\"applied\":\{count}}")
}
EqualSpacing => self.auto_arrange(artboard~, parent_id~)
GridAlign(grid) => self.snap_to_grid(artboard~, grid~)
MoveTop => {
let layout = solve(ab.doc)
let count = move_to_edge(ab.doc, parent_id, layout, true)
Ok("{\"intent\":\"move_top\",\"moved\":\{count}}")
}
MoveBottom => {
let layout = solve(ab.doc)
let count = move_to_edge(ab.doc, parent_id, layout, false)
Ok("{\"intent\":\"move_bottom\",\"moved\":\{count}}")
}
MoveCenter => {
// 垂直居中所有子节点
let layout = solve(ab.doc)
let count = center_all(ab.doc, parent_id, layout)
Ok("{\"intent\":\"move_center\",\"centered\":\{count}}")
}
ScaleAll(factor) => {
let count = scale_all(ab.doc, factor)
Ok("{\"intent\":\"scale\",\"factor\":\{factor},\"scaled\":\{count}}")
}
SetPadding(v) => {
let _ = ab.doc.update_node(parent_id, fn(n) {
let base = match n.layout {
Some(l) => l
None =>
{
dir: Vertical,
gap: 8.0,
justify: Start,
padding: Padding::uniform(0.0),
}
}
{ ..n, layout: Some({ ..base, padding: Padding::uniform(v), }), }
})
Ok("{\"intent\":\"set_padding\",\"value\":\{v}}")
}
SetGap(v) => {
let _ = ab.doc.update_node(parent_id, fn(n) {
let base = match n.layout {
Some(l) => l
None =>
{
dir: Vertical,
gap: 0.0,
justify: Start,
padding: Padding::uniform(0.0),
}
}
{ ..n, layout: Some({ ..base, gap: v, }), }
})
Ok("{\"intent\":\"set_gap\",\"value\":\{v}}")
}
Unknown =>
Err(
"cannot_parse_intent(支持:居中 | 垂直居中 | 垂直排列 | 水平排列 | 等宽 | 等高 | 等间距 | 网格 N | 顶部 | 底部 | 放大 N | 缩小 N | 边距 N | 间距 N)",
)
}
}
}
///|
/// 一步到位:解析文本 → 执行意图
pub fn Project::apply_constraint(
self : Project,
text : String,
artboard~ : String,
parent_id? : String = "root",
) -> Result[String, String] {
let intent = parse_layout_intent(text)
match intent {
// 错误即文档:宿主(和 LLM)看到 cannot_parse 时就地拿到可用的全部
// 意图词表——issue #17 的「语法无文档」由此闭环,无需翻 SKILL
Unknown =>
Err(
"cannot_parse: \"\{text}\"。constrain 是布局意图(非层级/z-order):居中 | 垂直居中 | 垂直排列 | 水平排列 | 等宽 | 等高 | 等间距 | 网格 N | 顶部 | 底部 | 放大 N | 缩小 N | 边距 N | 间距 N",
)
_ => self.apply_intent(intent, artboard~, parent_id~)
}
}
// ---------------------------------------------------------------------------
// 辅助操作
// ---------------------------------------------------------------------------
///|
fn find_first_child(doc : Document, parent_id : String) -> String {
match doc.nodes.get(parent_id) {
Some(p) => if p.children.length() > 0 { p.children[0] } else { "" }
None => ""
}
}
///|
fn equalize_width(doc : Document, parent_id : String) -> Int {
let mut count = 0
match doc.nodes.get(parent_id) {
Some(parent) =>
for cid in parent.children.iter() {
let _ = doc.update_node(cid, fn(n) { { ..n, width: Fill, } })
count = count + 1
}
None => ()
}
count
}
///|
fn equalize_height(doc : Document, parent_id : String) -> Int {
let mut count = 0
match doc.nodes.get(parent_id) {
Some(parent) =>
for cid in parent.children.iter() {
let _ = doc.update_node(cid, fn(n) { { ..n, height: Fill, } })
count = count + 1
}
None => ()
}
count
}
///|
/// 顶部/底部对齐(MoveTop/MoveBottom):按求解后的子节点高度级联错开
/// (保留相对次序,行距 8,与 center_all 同法)——曾把全部子节点压到同一
/// y(顶部全 0、底部写死 700),必然产生重叠违规且 700 与画板高度无关。
/// 底边锚定父容器 bottom(pr.height - 总高,不足则收在顶部)。
fn move_to_edge(
doc : Document,
parent_id : String,
layout : SolvedLayout,
top : Bool,
) -> Int {
let mut count = 0
match (doc.nodes.get(parent_id), layout.rect_of(parent_id)) {
(Some(parent), Some(pr)) => {
if parent.children.length() == 0 {
return 0
}
let gap = 8.0
let mut total_h = 0.0
for cid in parent.children.iter() {
match layout.rect_of(cid) {
Some(r) => total_h = total_h + r.height
None => ()
}
}
let n = parent.children.length()
let total_with_gap = total_h + gap * (n - 1).to_double()
let mut y = if top {
0.0
} else {
let start = pr.height - total_with_gap
if start > 0.0 { start } else { 0.0 }
}
for cid in parent.children.iter() {
let _ = doc.update_node(cid, fn(c) { { ..c, y: AlignStart(y), } })
match layout.rect_of(cid) {
Some(r) => y = y + r.height + gap
None => ()
}
count = count + 1
}
}
_ => ()
}
count
}
///|
fn center_all(doc : Document, parent_id : String, layout : SolvedLayout) -> Int {
let mut count = 0
match (doc.nodes.get(parent_id), layout.rect_of(parent_id)) {
(Some(parent), Some(pr)) => {
// 计算总高度
let mut total_h = 0.0
for cid in parent.children.iter() {
match layout.rect_of(cid) {
Some(r) => total_h = total_h + r.height
None => ()
}
}
let gap = 8.0
let n = parent.children.length()
let total_with_gap = total_h + gap * (n - 1).to_double()
let start_y = (pr.height - total_with_gap) / 2.0
let mut y = if start_y > 0.0 { start_y } else { 0.0 }
for cid in parent.children.iter() {
let _ = doc.update_node(cid, fn(c) { { ..c, y: AlignStart(y), } })
match layout.rect_of(cid) {
Some(r) => y = y + r.height + gap
None => ()
}
count = count + 1
}
}
_ => ()
}
count
}
///|
fn scale_all(doc : Document, factor : Double) -> Int {
let mut count = 0
for id in doc.nodes.keys() {
if id != "root" {
let _ = doc.update_node(id, fn(n) {
let w = scale_spec(n.width, factor)
let h = scale_spec(n.height, factor)
{ ..n, width: w, height: h, }
})
count = count + 1
}
}
count
}
///|
fn scale_spec(s : SizeSpec, factor : Double) -> SizeSpec {
match s {
Fixed(v) => Fixed(v * factor)
Fill => Fill
Hug => Hug
}
}