///|
/// 设计标注系统:自动从原型生成开发者交接规范(Spec)。
///
/// 这是 Figma Dev Mode / Zeplin 的等价物:
/// - 间距标注:每个元素到父容器/兄弟的精确距离
/// - 颜色标注:所有用到的颜色及其语义令牌名
/// - 字体标注:字号/颜色/令牌映射
/// - 组件清单:用了哪些组件、每个的变体和属性
/// - 交互标注:点击/滑动触发什么
///
/// 输出为结构化 Markdown,工程师直接复制使用。
// ---------------------------------------------------------------------------
// 标注数据
// ---------------------------------------------------------------------------
pub(all) struct SpacingNote {
node_id : String
label : String // "margin-left" / "gap" / "padding"
value : Double
unit : String // "px"
}
///|
pub(all) struct ColorNote {
node_id : String
field : String // "fill" / "text_color" / "stroke"
hex : String
token_name : String // 匹配到的令牌名(或 "custom")
}
///|
pub(all) struct FontNote {
node_id : String
size : Double
color : String
token_hint : String // "h2" / "body" / "caption" / "custom"
}
///|
pub(all) struct ComponentNote {
node_id : String
component : String
variant : String
props : String // 序列化属性
}
// ---------------------------------------------------------------------------
// 标注生成
// ---------------------------------------------------------------------------
///|
/// 生成画板的完整设计规范(Markdown 格式)
pub fn Project::generate_spec(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) => {
let layout = solve(ab.doc)
let tokens = self.tokens
let sb = StringBuilder::new()
// 头部
sb.write_string("# Design Spec: \{ab.name}\n\n")
sb.write_string("> Auto-generated by MoonViz Agent Engine\n\n")
sb.write_string(
"**Artboard**: \{ab.width.to_int()} × \{ab.height.to_int()}px\n\n",
)
sb.write_string("---\n\n")
// ── 组件清单 ──
sb.write_string("## Components\n\n")
sb.write_string("| Component | Instance | Variant | Dimensions |\n")
sb.write_string("|:---|:---|:---|:---|\n")
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
if id != "root" && n.component != "" {
match layout.rect_of(id) {
Some(r) => {
let w = r.width.to_int().to_string()
let h = r.height.to_int().to_string()
let x = r.x.to_int().to_string()
let y = r.y.to_int().to_string()
sb.write_string(
"| `\{n.component}` | `\{id}` | default | \{w}×\{h} @ (\{x},\{y}) |\n",
)
}
None => ()
}
}
None => ()
}
}
sb.write_string("\n")
// ── 颜色标注 ──
sb.write_string("## Colors\n\n")
sb.write_string("| Node | Field | Value | Token |\n")
sb.write_string("|:---|:---|:---|:---|\n")
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
if id != "root" {
// fill
if n.style.fill != "none" && n.style.fill != "" {
let tn = match_token_color(tokens, n.style.fill)
sb.write_string(
"| \{id} | fill | `\{n.style.fill}` | \{tn} |\n",
)
}
// text_color
if n.style.text_color != "#111111" && n.style.text_color != "" {
let tn2 = match_token_color(tokens, n.style.text_color)
sb.write_string(
"| \{id} | text | `\{n.style.text_color}` | \{tn2} |\n",
)
}
}
None => ()
}
}
sb.write_string("\n")
// ── 字体标注 ──
sb.write_string("## Typography\n\n")
sb.write_string("| Node | Size | Token |\n")
sb.write_string("|:---|:---|:---|\n")
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) => {
let is_txt = is_text_node(n)
if is_txt || n.text != "" {
let size_str = n.style.font_size.to_int().to_string()
let token = match_font_token(tokens, n.style.font_size)
sb.write_string("| \{id} | \{size_str}px | \{token} |\n")
}
}
None => ()
}
}
sb.write_string("\n")
// ── 间距标注 ──
sb.write_string("## Spacing\n\n")
sb.write_string("```text\n")
for id in ab.doc.nodes.keys() {
match layout.rect_of(id) {
Some(r) =>
if id != "root" {
let x_str = r.x.to_int().to_string()
let y_str = r.y.to_int().to_string()
let w_str = r.width.to_int().to_string()
let h_str = r.height.to_int().to_string()
sb.write_string(
"\{id}: x=\{x_str} y=\{y_str} w=\{w_str} h=\{h_str}\n",
)
}
None => ()
}
}
sb.write_string("```\n\n")
// ── 布局信息 ──
sb.write_string("## Layout\n\n")
for id in ab.doc.nodes.keys() {
match ab.doc.nodes.get(id) {
Some(n) =>
if n.layout is Some(_) && id != "root" {
match n.layout {
Some(l) => {
let dir = match l.dir {
Vertical => "column"
Horizontal => "row"
}
let gap = l.gap.to_int().to_string()
let pt = l.padding.top.to_int().to_string()
let pr = l.padding.right.to_int().to_string()
let pb = l.padding.bottom.to_int().to_string()
let pl = l.padding.left.to_int().to_string()
sb.write_string(
"- **\{id}**: flex-direction: \{dir}; gap: \{gap}px; padding: \{pt}px \{pr}px \{pb}px \{pl}px\n",
)
}
None => ()
}
}
None => ()
}
}
sb.write_string("\n")
// ── 交互标注 ──
sb.write_string("## Interactions\n\n")
let mut has_interactions = false
for e in self.flows.iter() {
if e.from == artboard {
sb.write_string("- **\{e.trigger}** → navigate to `\{e.to}`\n")
has_interactions = true
}
}
// 空段判断只看本画板是否列出过条目——其他画板的 flow 不代表本画板有交互
if !has_interactions {
sb.write_string("_No interactions defined_\n")
}
sb.write_string("\n")
// ── CSS 变量(令牌映射)──
sb.write_string("## CSS Custom Properties\n\n")
sb.write_string("```css\n")
for k in tokens.colors.keys() {
match tokens.colors.get(k) {
Some(v) => sb.write_string(" --color-\{k}: \{v};\n")
None => ()
}
}
for k in tokens.spacing.keys() {
match tokens.spacing.get(k) {
Some(v) => sb.write_string(" --spacing-\{k}: \{v.to_int()}px;\n")
None => ()
}
}
sb.write_string("```\n")
Ok(sb.to_string())
}
}
}
///|
/// 标注摘要(JSON,机器可读)
pub fn Project::spec_json(
self : Project,
artboard~ : String,
) -> Result[String, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard")
Some(ab) => {
let layout = solve(ab.doc)
let items : Array[String] = []
for id in ab.doc.nodes.keys() {
match (ab.doc.nodes.get(id), layout.rect_of(id)) {
(Some(n), Some(r)) =>
if id != "root" {
// rect 用合法 JSON 数组值 [x,y,w,h];id/component/fill/text
// 过 json_escape——消费端 JSON.parse 不再被裸数字花括号/
// 未转义引号打崩
let w = r.width.to_int().to_string()
let h = r.height.to_int().to_string()
let x = r.x.to_int().to_string()
let y = r.y.to_int().to_string()
let fill = json_escape(n.style.fill)
let comp = json_escape(n.component)
let txt = json_escape(n.text)
items.push(
"{\"id\":\"\{json_escape(id)}\",\"component\":\"\{comp}\",\"rect\":[\{x},\{y},\{w},\{h}],\"fill\":\"\{fill}\",\"text\":\"\{txt}\"}",
)
}
_ => ()
}
}
let j = jstr20(items)
Ok("[\{j}]")
}
}
}
// ---------------------------------------------------------------------------
// 辅助
// ---------------------------------------------------------------------------
///|
fn match_token_color(tokens : DesignTokens, hex : String) -> String {
for k in tokens.colors.keys() {
match tokens.colors.get(k) {
Some(v) => if v == hex { return "`" + k + "`" }
None => ()
}
}
"custom"
}
///|
fn match_font_token(tokens : DesignTokens, size : Double) -> String {
for k in tokens.typography.keys() {
match tokens.typography.get(k) {
Some(v) => if v == size { return "`" + k + "`" }
None => ()
}
}
"custom"
}
///|
fn jstr20(parts : Array[String]) -> String {
let sb = StringBuilder::new()
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string(parts[i])
}
sb.to_string()
}
///|
fn is_text_node(n : Node) -> Bool {
match n.kind {
Text => true
_ => false
}
}