@helping-ai-workflow/md2doc 3.12.1 → 3.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,13 @@
2
2
 
3
3
  /**
4
4
  * wave-geometry — where every cycle of a WaveJSON document sits, and which
5
- * cycle a point lands in. Pure arithmetic: no DOM, no measuring, no mutation.
6
- * Every size arrives as a parameter, so the layer above can hand in whatever
7
- * the rendered SVG actually measures and this file stays testable in
8
- * milliseconds.
5
+ * cycle a point lands in. Arithmetic, no mutation, and no globals: every size
6
+ * arrives as a parameter, so the layer above can hand in whatever the
7
+ * rendered SVG actually measures and this file stays testable in
8
+ * milliseconds. The one function that reads a DOM is `measureEngineSvg`, and
9
+ * it reads only the engine SVG it is handed (getScreenCTM, text lengths) —
10
+ * never a page global, which the purity test in wave-geometry.test.js keeps
11
+ * true.
9
12
  *
10
13
  * It lives on its own for one reason: the hit test's boundary rule — a cell's
11
14
  * LEFT edge belongs to it, its RIGHT edge belongs to the next cell — is the
@@ -315,6 +318,10 @@ function layoutOf(doc, opts) {
315
318
  const laneHeight = size(o.laneHeight, DEFAULTS.laneHeight, 'laneHeight', false);
316
319
  const cycleWidth = size(o.cycleWidth, DEFAULTS.cycleWidth, 'cycleWidth', false);
317
320
  const nameColWidth = size(o.nameColWidth, DEFAULTS.nameColWidth, 'nameColWidth', true);
321
+ // wave-redesign P1 Task 4: the engine canvas measures where lane 0's row
322
+ // starts (`measureEngineSvg(...).laneTops[0]`) and passes it here, which
323
+ // replaces the editor-only band constants below. Absent, nothing changes.
324
+ const originYOverride = size(o.originY, undefined, 'originY', true);
318
325
 
319
326
  // `config.hscale` scales every lane's cycle width the same way the engine's
320
327
  // own `lane.hscale` does — see `hscaleOf` for the exact rounding/clamping
@@ -328,7 +335,7 @@ function layoutOf(doc, opts) {
328
335
  const headTextHeight = (doc !== null && typeof doc === 'object' &&
329
336
  doc.head !== null && typeof doc.head === 'object' &&
330
337
  doc.head.text !== undefined) ? HEAD_TEXT_HEIGHT : 0;
331
- const originY = headTextHeight + RULER_HEIGHT;
338
+ const originY = originYOverride !== undefined ? originYOverride : headTextHeight + RULER_HEIGHT;
332
339
  const footHeight = hasBanner(doc, 'foot') ? FOOT_HEIGHT : 0;
333
340
  // v3.6.0 Task 9 fix round 1 (R29): stacked BELOW `footHeight`, mirroring
334
341
  // `headTextHeight` stacked ABOVE `RULER_HEIGHT` — `footHeight` alone still
@@ -424,7 +431,8 @@ function layoutOf(doc, opts) {
424
431
  * A group title occupies NO ROW — measured in Task 2 against the pinned
425
432
  * engine, and `wave-geometry` is built on it. What it occupies is horizontal
426
433
  * space in the name column, so that is where this puts it: a label in the
427
- * lane list's rail, spanning its lanes' rows. Drawing it as a row of its own
434
+ * name column (the lane list's rail, until the redesign removed it),
435
+ * spanning its lanes' rows. Drawing it as a row of its own
428
436
  * would put the lane list one row out of step with the waveform beside it for
429
437
  * every lane below the group, which is exactly the class of silent
430
438
  * disagreement this batch keeps producing.
@@ -434,8 +442,10 @@ function layoutOf(doc, opts) {
434
442
  * shown here; it is not addressable through the bridge either.
435
443
  *
436
444
  * v3.6.0 Task 2: moved here from `lib/editor/wave-ui.js` per ruling R9 — both
437
- * `wave-panels.js` (the lane rail) and `wave-draw.js` (the canvas) need it,
438
- * and this file is the one place both already depend on. Behaviour
445
+ * `wave-panels.js` (the lane rail) and `wave-draw.js` (the canvas) needed it,
446
+ * and this file is the one place both already depended on. Since the wave
447
+ * redesign its caller is `wave-ui.js` (the name column's group ⠿ and the
448
+ * group-row hover). Behaviour
439
449
  * unchanged; it now reads this file's own `valueAt` instead of a copy that
440
450
  * used to live in `wave-ui.js`.
441
451
  */
@@ -643,11 +653,21 @@ const EDGE_HANDLE = 12;
643
653
  * 一個答案,`cellRect`/`reachOf` 已經是它,錨點只是在那個答案上再加一個
644
654
  * 固定比例的偏移。
645
655
  */
656
+ /*
657
+ * wave-redesign P1 Task 4: the offset is `anchorRatio` of the BASE cycle
658
+ * (`layout.cycleWidth`), not of this lane's own cell width. The engine's
659
+ * `render-arcs.js` places a letter at `xs * (2*pos*period*hscale - phase) +
660
+ * xlabel`, and `xlabel` (the skin's `socket.x`, 6 of a 40-unit cycle) is not
661
+ * multiplied by `period` or `hscale`. With the cell width a `period: 2` lane
662
+ * put every anchor 6 engine units right of the engine's, which the engine
663
+ * canvas test (test/wave-canvas.test.js) measures against the drawn
664
+ * `gmark_` paths. For `period` 1 and `hscale` 1 the two are the same number.
665
+ */
646
666
  function anchorOfCell(layout, laneIndex, cell) {
647
667
  const r = cellRect(layout, laneIndex, cell);
648
668
  if (r === null) return null;
649
669
  return {
650
- x: r.x + SKIN_METRICS.anchorRatio * r.width,
670
+ x: r.x + SKIN_METRICS.anchorRatio * layout.cycleWidth,
651
671
  y: r.y + r.height / 2,
652
672
  };
653
673
  }
@@ -697,7 +717,8 @@ function boundaryAt(layout, x, y) {
697
717
  const row = layout.lanes[laneIndex];
698
718
  const reach = reachOf(row.originX, row.cycleWidth, layout.width);
699
719
  if (reach === 0) return null;
700
- const raw = (x - row.originX - SKIN_METRICS.anchorRatio * row.cycleWidth) / row.cycleWidth;
720
+ // Same anchor offset as `anchorOfCell`: a fraction of the BASE cycle.
721
+ const raw = (x - row.originX - SKIN_METRICS.anchorRatio * layout.cycleWidth) / row.cycleWidth;
701
722
  let cell = Math.round(raw);
702
723
  if (cell < 0) cell = 0;
703
724
  if (cell > reach - 1) cell = reach - 1;
@@ -823,6 +844,8 @@ function edgeLayoutDetail(doc, layout) {
823
844
  for (let i = 0; i < doc.edge.length; i += 1) {
824
845
  const edge = codec.parseEdge(doc.edge[i]);
825
846
  if (edge === null) {
847
+ // 單點標註(`<字母> <文字>`)由標註層畫,不算畫不出來的 edge。
848
+ if (codec.parseNote(doc.edge[i]) !== null) continue;
826
849
  skipped.push({ index: i, edge: null, reason: 'unparseable' });
827
850
  continue;
828
851
  }
@@ -933,6 +956,117 @@ function edgeHandleAt(edges, x, y) {
933
956
  return null;
934
957
  }
935
958
 
959
+ /**
960
+ * What the WaveDrom engine actually drew, read off its own SVG — the half of
961
+ * the editor canvas's geometry that `layoutOf` cannot know (wave-redesign
962
+ * spec §3.1). `layoutOf` stays the source of every lane's horizontal cell
963
+ * arithmetic (period, phase, hscale); this supplies the three things only the
964
+ * rendered picture has:
965
+ *
966
+ * - `xg`: the name column, from the `lanes_<i>` group's translate, which the
967
+ * engine writes as `lane.xg + 0.5` (insert-svg-template.js). Callers pass
968
+ * `xg + 0.5` to `layoutOf` as `nameColWidth`, because that half unit is
969
+ * part of where every cell starts.
970
+ * - `laneTops[j]`: the top of lane j's 30-unit row band. Each lane is a
971
+ * `wavelane_<j>_<i>` group translated by `lane.y0 + j * lane.yo` inside
972
+ * `lanes_<i>` (5 + 30j); the 20-unit brick is drawn from that y down, so
973
+ * the band that has the brick in its middle starts y0 above the group.
974
+ * y0 is measured as wavelane_0's offset from `lanes_<i>`, not pinned.
975
+ * With the band so placed, a row's vertical centre is exactly the y the
976
+ * engine anchors a `node` letter at (`i * yo + y0 + ys / 2`, render-arcs.js).
977
+ * - `labels`: every data label `text` in each lane's `wavelane_draw_` group,
978
+ * with its centre `x`, its advance `width` (`getComputedTextLength`, the
979
+ * same quantity lib/wave-overflow.js estimates), its box `y` / `height`,
980
+ * and — when the document is passed — which `data` slot it belongs to
981
+ * and how many cycles that segment spans (`cells`). The pairing is by
982
+ * position, never by text (two segments may carry the same label): the
983
+ * label belongs to the segment of the same lane whose x range contains
984
+ * its centre.
985
+ *
986
+ * Every number is in ROOT engine units (the svg's viewBox), converted through
987
+ * `getScreenCTM`, so the canvas can scale the svg without this changing.
988
+ * Grid lines and lane contents carry coordinates LOCAL to translated groups;
989
+ * reading their raw attributes is how a prototype ended up half a cycle off.
990
+ * `originY` is the `lanes_` group's own y, the fallback when there is no lane.
991
+ */
992
+ function measureEngineSvg(svg, doc) {
993
+ const out = { xg: 0, originY: 0, laneTops: [], laneHeight: DEFAULTS.laneHeight, labels: [] };
994
+ const rootCtm = svg && typeof svg.getScreenCTM === 'function' ? svg.getScreenCTM() : null;
995
+ if (rootCtm === null) return out;
996
+ const rootInv = rootCtm.inverse();
997
+ function toRoot(el) { return rootInv.multiply(el.getScreenCTM()); }
998
+ const lanesG = svg.querySelector('g[id^="lanes_"]');
999
+ if (lanesG === null) return out;
1000
+ const lm = toRoot(lanesG);
1001
+ out.xg = lm.e - 0.5;
1002
+ out.originY = lm.f;
1003
+
1004
+ const groups = [];
1005
+ for (let k = 0; k < lanesG.children.length; k++) {
1006
+ const g = lanesG.children[k];
1007
+ const m = /^wavelane_(\d+)_\d+$/.exec(g.id || '');
1008
+ if (m !== null) groups[Number(m[1])] = g;
1009
+ }
1010
+ const tops = [];
1011
+ let y0 = null;
1012
+ for (let j = 0; j < groups.length; j++) {
1013
+ if (groups[j] === undefined) break;
1014
+ const gy = toRoot(groups[j]).f;
1015
+ if (y0 === null) y0 = gy - lm.f;
1016
+ tops.push(gy - y0);
1017
+ }
1018
+ out.laneTops = tops;
1019
+ if (tops.length > 1) out.laneHeight = tops[1] - tops[0];
1020
+
1021
+ const layout = (doc !== null && typeof doc === 'object' && tops.length > 0)
1022
+ ? layoutOf(doc, { laneHeight: out.laneHeight, nameColWidth: out.xg + 0.5, originY: tops[0] })
1023
+ : null;
1024
+
1025
+ for (let j = 0; j < tops.length; j++) {
1026
+ let draw = null;
1027
+ for (let k = 0; k < groups[j].children.length; k++) {
1028
+ if (/^wavelane_draw_/.test(groups[j].children[k].id || '')) { draw = groups[j].children[k]; break; }
1029
+ }
1030
+ if (draw === null) continue;
1031
+ const segs = layout !== null && j < layout.lanes.length ? segmentsOf(layout.lanes[j]) : [];
1032
+ for (let k = 0; k < draw.children.length; k++) {
1033
+ const t = draw.children[k];
1034
+ if (String(t.tagName).toLowerCase() !== 'text') continue;
1035
+ const m = toRoot(t);
1036
+ const box = t.getBBox();
1037
+ const x = m.a * (box.x + box.width / 2) + m.c * (box.y + box.height / 2) + m.e;
1038
+ const top = m.b * box.x + m.d * box.y + m.f;
1039
+ let slot = null;
1040
+ let cells = null;
1041
+ for (const s of segs) {
1042
+ if (x >= s.from && x < s.to) { slot = s.slot; cells = s.cells; break; }
1043
+ }
1044
+ out.labels.push({
1045
+ laneIndex: j, slot: slot, x: x, width: t.getComputedTextLength() * m.a,
1046
+ text: t.textContent, y: top, height: box.height * m.d, cells: cells,
1047
+ });
1048
+ }
1049
+ }
1050
+ return out;
1051
+ }
1052
+
1053
+ /** Each data segment of one laid-out lane: its slot, cycle count and x range. */
1054
+ function segmentsOf(row) {
1055
+ const lane = row.lane;
1056
+ if (lane === null || typeof lane !== 'object' || typeof lane.wave !== 'string') return [];
1057
+ const cells = codec.expandWave(lane.wave);
1058
+ const out = [];
1059
+ for (let i = 0; i < cells.length; i++) {
1060
+ const slot = codec.dataSlotOf(lane, i);
1061
+ if (slot === null) continue;
1062
+ let n = 1;
1063
+ while (i + n < cells.length && cells[i + n].held) n++;
1064
+ out.push({ slot: slot, cells: n, from: row.originX + i * row.cycleWidth,
1065
+ to: row.originX + (i + n) * row.cycleWidth });
1066
+ }
1067
+ return out;
1068
+ }
1069
+
936
1070
  module.exports = {
937
1071
  layoutOf: layoutOf,
938
1072
  valueAt: valueAt,
@@ -946,6 +1080,7 @@ module.exports = {
946
1080
  edgeLayoutDetail: edgeLayoutDetail,
947
1081
  edgeHandleRect: edgeHandleRect,
948
1082
  edgeHandleAt: edgeHandleAt,
1083
+ measureEngineSvg: measureEngineSvg,
949
1084
  EDGE_HANDLE: EDGE_HANDLE,
950
1085
  DEFAULTS: DEFAULTS,
951
1086
  MAX_LANES: MAX_LANES,