/// 约束系统(Figma Constraints)+ Z 序(Reorder)。
///
/// constraint 语义(四字符/词法:水平 _ 垂直,简写如 "lt" "rt" "ct" "cb" "stretch-c" "hscale-m"):
/// 水平:l=左锚定 c=水平居中 r=右锚定 stretch=拉伸 hscale=等比
/// 垂直:t=上锚定 m=垂直居中 b=下锚定(stretch/hscale 亦适用)
/// 画板 resize 时按约束重排全部节点——这是 Figma 的灵魂行为:
/// 同一设计稿出一排尺寸,不再需要逐个手调。
///|
/// 解析约束:返回 (水平, 垂直)
/// "lt" → (l,t);"stretch-c" → (stretch,c);"hscale_m" → (hscale,m)
fn parse_constraint(c : String) -> (String, String) {
// 分隔符兼容 '_' 与 '-'
let parts = split_two_axes(c)
let h_part = first_char(parts[0])
let v_part = if parts.length() > 1 {
first_char(parts[1])
} else {
second_char(parts[0], "t")
}
let hnorm = normalize_axis(h_part, "l")
let vnorm = normalize_axis(v_part, "t")
(hnorm, vnorm)
}
///|
/// 两字符简写的第二个字符("lt" → "t")
fn second_char(s : String, fallback : String) -> String {
if s.length() >= 2 {
s.substring(start=1, end=2)
} else {
fallback
}
}
///|
/// 按 '_' 或 '-' 拆成两段(无分隔:两字符简写拆开)
fn split_two_axes(c : String) -> Array[String] {
let out : Array[String] = []
let cur = StringBuilder::new()
for ch in c.iter() {
if ch == '_' || ch == '-' {
out.push(cur.to_string())
cur.reset()
} else {
cur.write_char(ch)
}
}
out.push(cur.to_string())
out
}
///|
fn first_char(s : String) -> String {
if s.length() > 0 {
s.substring(start=0, end=1)
} else {
""
}
}
///|
fn normalize_axis(ch : String, fallback : String) -> String {
match ch {
"l" | "t" => "l"
"c" | "m" => "c"
"r" | "b" => "r"
"s" => "stretch"
"h" => "hscale"
_ => fallback
}
}
///|
/// 画板尺寸变更:按约束重排所有节点的 PosSpec / SizeSpec。
/// 逐节点处理(父先于子不敏感——PosSpec 均相对父内容盒,dW/dH 逐层传递比例)。
pub fn Project::resize_canvas(
self : Project,
artboard~ : String,
width~ : Double,
height~ : Double,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard: \{artboard}")
Some(ab) => {
let old_w = ab.width
let old_h = ab.height
if old_w <= 0.0 || old_h <= 0.0 {
return Err("invalid_old_size")
}
let d_w = width - old_w
let d_h = height - old_h
// 根节点尺寸
let _ = ab.doc.update_node(ab.doc.root_id, fn(n) {
{ ..n, width: Fixed(width), height: Fixed(height), }
})
// 非根节点按 constraint 调整(相对其父内容盒的尺寸变化近似 = 画板变化,
// 对嵌套栈布局,栈主轴由布局决定不受影响;cross 轴与绝对定位节点按约束适配)
for id in ab.doc.nodes.keys() {
if id == ab.doc.root_id {
continue
}
let _ = ab.doc.update_node(id, fn(n) {
let (h_c, v_c) = parse_constraint(n.constraint)
let mut out = n
// 水平
match h_c {
"r" => {
let off = pos_offset(out.x)
out = { ..out, x: Fixed(off + d_w), }
}
"c" => {
let off = pos_offset(out.x)
out = { ..out, x: Fixed(off + d_w / 2.0), }
}
"stretch" => {
let off = pos_offset(out.x)
let w = size_val(out.width)
if w > 0.0 {
out = { ..out, x: Fixed(off), width: Fixed(w + d_w), }
}
}
"hscale" => {
let off = pos_offset(out.x)
let w = size_val(out.width)
if w > 0.0 && old_w > 0.0 {
let k = width / old_w
out = { ..out, x: Fixed(off * k), width: Fixed(w * k), }
}
}
_ => ()
}
// 垂直
match v_c {
"r" => {
let off = pos_offset(out.y)
out = { ..out, y: Fixed(off + d_h), }
}
"c" => {
let off = pos_offset(out.y)
out = { ..out, y: Fixed(off + d_h / 2.0), }
}
"stretch" => {
let off = pos_offset(out.y)
let h = size_val(out.height)
if h > 0.0 {
out = { ..out, y: Fixed(off), height: Fixed(h + d_h), }
}
}
"hscale" => {
let off = pos_offset(out.y)
let h = size_val(out.height)
if h > 0.0 && old_h > 0.0 {
let k = height / old_h
out = { ..out, y: Fixed(off * k), height: Fixed(h * k), }
}
}
_ => ()
}
out
})
}
// 更新画板元数据
let meta = self.artboards.get(artboard)
match meta {
Some(m) => self.artboards.set(artboard, { ..m, width, height, })
None => ()
}
// 审查修复:板尺寸变化必须推进 content_seq——按板失效的渲染缓存
// (svg 含画板宽高)依赖它感知 resize
match self.artboards.get(artboard) {
Some(m) => m.doc.content_seq = m.doc.content_seq + 1
None => ()
}
self.revision = self.revision + 1
Ok(
"{\"ok\":true,\"op\":\"resize_canvas\",\"artboard\":\"\{artboard}\",\"from_w\":\{old_w},\"from_h\":\{old_h},\"to_w\":\{width},\"to_h\":\{height}}",
)
}
}
}
///|
fn pos_offset(p : PosSpec) -> Double {
match p {
Fixed(v) => v
AlignStart(v) => v
AlignEnd(v) => v
CenterInParent => 0.0
}
}
///|
fn size_val(s : SizeSpec) -> Double {
match s {
Fixed(v) => v
_ => 0.0
}
}
///|
/// Z 序:reorder front|back|up|down
pub fn Project::reorder_node(
self : Project,
artboard~ : String,
node_id~ : String,
order~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard: \{artboard}")
Some(ab) => {
let parent = match ab.doc.find_parent(node_id) {
Some(p) => p
None => return Err("orphan_node: \{node_id}")
}
let mut moved = false
let _ = ab.doc.update_node(parent, fn(pn) {
let kids = pn.children.copy()
let mut idx = -1
for i in 0.. {
for c in rest.iter() {
out.push(c)
}
out.push(item)
moved = true
{ ..pn, children: out, }
}
"back" => {
out.push(item)
for c in rest.iter() {
out.push(c)
}
moved = true
{ ..pn, children: out, }
}
"up" =>
if idx < kids.length() - 1 {
// 与后一个交换
for i in 0..
if idx > 0 {
for i in 0.. pn
}
})
if !moved {
return Ok("{\"ok\":false,\"reason\":\"no_change\"}")
}
self.revision = self.revision + 1
Ok(
"{\"ok\":true,\"op\":\"reorder\",\"node\":\"\{node_id}\",\"order\":\"\{order}\"}",
)
}
}
}
///|
/// 主组件样式传播(Figma Component Override 的批量简化语义):
/// restyle k=v ... → 该画板所有 component 匹配的节点统一重样式。
/// 引擎职责仍是确定性批量命令,不含任何 NL。
pub fn Project::restyle_component(
self : Project,
artboard~ : String,
component_id~ : String,
kvs~ : Array[(String, String)],
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard: \{artboard}")
Some(ab) => {
let targets : Array[String] = []
for id in ab.doc.nodes.keys() {
match ab.doc.get(id) {
Some(n) => if n.component == component_id { targets.push(id) }
None => ()
}
}
let mut applied = 0
for tid in targets.iter() {
match self.edit_node(artboard~, node_id=tid, kvs~) {
Ok(_) => applied = applied + 1
Err(_) => ()
}
}
self.revision = self.revision + 1
Ok(
"{\"ok\":true,\"op\":\"restyle\",\"component\":\"\{component_id}\",\"instances\":\{applied}}",
)
}
}
}