Skip to content

Conversation

@zicjin
Copy link
Collaborator

@zicjin zicjin commented Nov 9, 2025

新增一个确定性的布局引擎(src/positioning/VerticalCoordinates.ts 以及 src/positioning/vertical/* 中的辅助模块),它遍历解析树,测量 Markdown 注释、片段、async/sync/creation/return 语句,并记录绝对的 top/height 与锚点(message、occurrence、return)。具备主题感知的间距常量覆盖我们当前使用的 Tailwind 类,且按主题的覆盖项可扩展 THEME_OVERRIDES。按参与者为 creation 锚点建立索引,以便生命线能够找到各自的 «create» 偏移量。

通过 verticalCoordinatesAtom 将新引擎接入 state(src/store/Store.ts:37-57),共享现有的 width provider、来自 Coordinates 的参与者排序,以及与 MessageLayer 相同的起点检测逻辑。现在任何使用方都可以从该 atom 提取完整的语句坐标或特定锚点。

生命线现在使用这些预计算值,而不是查询 DOM(src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLine.tsx:1-69)。每条生命线都会监听 verticalCoordinatesAtom 的变化,应用已解析的 creation 偏移量(或沿用旧的 20 px 回退),并在无需等待布局抖动或 participant_set_top 事件的情况下将自身标记为就绪。

新增 src/positioning/VerticalCoordinates.spec.ts,用带有 stub width provider 的方式覆盖 creation 锚点的顺利路径(happy path),以便在浏览器构建之外暴露回归问题。

新增 StatementCoordinateDebug overlay,当 localStorage.zenumlDebug 为真值时,会显示完整的语句 Y-map,支持过滤、复制到剪贴板以及 console-table 日志输出,以便快速发现不匹配问题(src/components/DiagramFrame/Debug/StatementCoordinateDebug.tsx:1-121)。

  • 将该调试 overlay 渲染在图表容器内部,使其能跟随渲染的图表变化而不影响导出结果(src/components/DiagramFrame/SeqDiagram/SeqDiagram.tsx:9,74-75)。
  • 在开发工具中运行 localStorage.zenumlDebug = "1" 即可启用,然后使用面板中的按钮来检查条目或将其输出到控制台。

已运行 bun test src/positioning/VerticalCoordinates.spec.ts。

@MrCoder
Copy link
Collaborator

MrCoder commented Nov 10, 2025

Async Self Message后面的Creation好像还是有问题。我创建了一个最小复现DSL,供参考。
image

@MrCoder
Copy link
Collaborator

MrCoder commented Nov 10, 2025

另外,你确信它不是在下一个tick里面设置的吗?我在Performance profiling的时候能看到下面的跳跃情况。
image
对应的DSLru如下:


// comments at the beginning should be ignored
title This is a title
@Lambda <<stereotype>> ParticipantName
group "B C" {@EC2 B @ECS C}
"bg color" #FF0000
@Starter("OptionalStarter")
new B
ReturnType ret = ParticipantName.methodA(a, b) {
  critical("This is a critical message") {
    // Customised style for RESTFul API - `POST /order` <br>
    ReturnType ret2 = selfCall() {
      B.syncCallWithinSelfCall() {
        ParticipantName.rightToLeftCall()
        return B
      }
      "space in name"->"bg color".syncMethod(from, to)
    }
  }
  // A comment for alt
  if (condition) {
    // A comment for creation
    ret = new CreatAndAssign()
    "ret:CreatAndAssign".method(create, and, assign)
    // A comment for async self
    B->B: Self Async
    // A comment for async message
    B->C: Async Message within fragment
    new Creation() {
      return from_creation
    }
    return "from if to original source"
    try {
      new AHasAVeryLongNameLongNameLongNameLongName() {
        new CreatWithinCreat()
        C.rightToLeftFromCreation() {
          B.FurtherRightToLeftFromCreation()
        }
      }
    } catch (Exception) {
      self {
        return C
      }
    } finally {
      C: async call from implied source  
    }
    =====divider can be anywhere=====
  } else if ("another condition") {
    par {
      B.method
      C.method
    }
  } else {
    // A comment for loop
    forEach(Z) {
      Z.method() {
        return Z
      }
    }
  }
}

@MrCoder
Copy link
Collaborator

MrCoder commented Nov 11, 2025

好像是没有考虑在Creation中嵌套Creation的情况。

new A() {
    new B
}
image

@zicjin
Copy link
Collaborator Author

zicjin commented Nov 11, 2025

Async Self Message后面的Creation好像还是有问题。我创建了一个最小复现DSL,供参考。 image

这个误差没了。

另外,你确信它不是在下一个tick里面设置的吗?我在Performance profiling的时候能看到下面的跳跃情况。 image 对应的DSLru如下:


// comments at the beginning should be ignored
title This is a title
@Lambda <<stereotype>> ParticipantName
group "B C" {@EC2 B @ECS C}
"bg color" #FF0000
@Starter("OptionalStarter")
new B
ReturnType ret = ParticipantName.methodA(a, b) {
  critical("This is a critical message") {
    // Customised style for RESTFul API - `POST /order` <br>
    ReturnType ret2 = selfCall() {
      B.syncCallWithinSelfCall() {
        ParticipantName.rightToLeftCall()
        return B
      }
      "space in name"->"bg color".syncMethod(from, to)
    }
  }
  // A comment for alt
  if (condition) {
    // A comment for creation
    ret = new CreatAndAssign()
    "ret:CreatAndAssign".method(create, and, assign)
    // A comment for async self
    B->B: Self Async
    // A comment for async message
    B->C: Async Message within fragment
    new Creation() {
      return from_creation
    }
    return "from if to original source"
    try {
      new AHasAVeryLongNameLongNameLongNameLongName() {
        new CreatWithinCreat()
        C.rightToLeftFromCreation() {
          B.FurtherRightToLeftFromCreation()
        }
      }
    } catch (Exception) {
      self {
        return C
      }
    } finally {
      C: async call from implied source  
    }
    =====divider can be anywhere=====
  } else if ("another condition") {
    par {
      B.method
      C.method
    }
  } else {
    // A comment for loop
    forEach(Z) {
      Z.method() {
        return Z
      }
    }
  }
}

这话我就看不懂,其实不熟悉zenuml,我只是在不断跟 codex 掰扯,让其通过计算方式能够像素级匹配基准 snapshots。避免它用作弊方式通过 e2e。

好像是没有考虑在Creation中嵌套Creation的情况。

new A() {
    new B
}
image

这我看了一下确实不对。新增了一个 async-message-mini-2.html,我有空再继续。我的 codex 居然干没了!第一次遇到
image
得换 claude 继续干了。

按照我的理解,这个事情是很难做到真正 100% 还原。这是在分解浏览器的 reflow/repaint 了,只是因为 zenuml 的对象类型极其有限所以目前才看起来可行。但是未来增加任何单元类型/功能都要去重头微调这些计算函数。

codex 写的这个 tests/layout-metrics.spec.ts 确实提供了一个好思路,得到的 tmp/layout-metrics.json 可以帮我们快速微调数值。

@MrCoder
Copy link
Collaborator

MrCoder commented Nov 11, 2025

DSL还是比较稳定的。过去七八年基本没有大的影响布局的变化。UML本身也没有什么改进(虽然不是啥好事😄)。

- model separate top/bottom occurrence insets to match CSS padding/borders
- rework server-side occurrence measurement to preserve collapsed margins
- refresh async-message-mini Playwright snapshot to capture corrected lifeline position
@zicjin
Copy link
Collaborator Author

zicjin commented Nov 12, 2025

我本地测试是可以了,如果要保留新的 async-message-mini.spec.ts 需要更新 linux snapshots

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants