@helping-ai-workflow/md2doc 3.5.0 → 3.6.0
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.
- package/lib/editor/client.js +44 -5
- package/lib/editor/server.js +21 -2
- package/lib/editor/wave-codec.js +160 -3
- package/lib/editor/wave-draw.js +1470 -0
- package/lib/editor/wave-geometry.js +469 -19
- package/lib/editor/wave-panels.js +1102 -0
- package/lib/editor/wave-store.js +36 -1
- package/lib/editor/wave-ui.js +1807 -1802
- package/lib/md2doc.js +520 -50
- package/package.json +2 -2
|
@@ -44,6 +44,64 @@
|
|
|
44
44
|
|
|
45
45
|
const codec = require('./wave-codec.js');
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* MEASURED against `node_modules/wavedrom` 3.5.0 (`node_modules/wavedrom/package.json`
|
|
49
|
+
* `"version"`), by reading its `waveSkin` object directly in Node:
|
|
50
|
+
*
|
|
51
|
+
* - socket: `["g",{"id":"socket"},["rect",{"y":"15","x":"6","height":"20","width":"20"}]]`.
|
|
52
|
+
* A brick is `width` wide (20), a cycle is two bricks (40), and the engine
|
|
53
|
+
* anchors a `node` marker at the socket rect's `x` — so `anchorRatio` is
|
|
54
|
+
* `6 / 40 = 0.15`.
|
|
55
|
+
* - transition brick `0m1` — a REAL 0->1 change. Measure the ramp from
|
|
56
|
+
* THIS brick, not from `0m0`: the id looks similar, but `0m0` is a
|
|
57
|
+
* same-LEVEL blip (a down-and-back notch that never changes level) and
|
|
58
|
+
* is not a transition at all.
|
|
59
|
+
* `["g",{"id":"0m1"},["path",{"d":"M0,20 3,20 9,0 20,0", …}]]`, i.e.
|
|
60
|
+
* absolute points `(0,20)->(3,20)->(9,0)->(20,0)`. The flat lead-in at
|
|
61
|
+
* the old level ends at x=3, so `slewStartRatio` is `3 / 40 = 0.075`;
|
|
62
|
+
* the diagonal reaches the new level at x=9, so `slewEndRatio` is
|
|
63
|
+
* `9 / 40 = 0.225`. Cross-checked against `1m0` (1->0):
|
|
64
|
+
* `["g",{"id":"1m0"},["path",{"d":"m0,0 3,0 6,20 11,0", …}]]` (relative
|
|
65
|
+
* coordinates, resolving to `(0,0)->(3,0)->(9,20)->(20,20)`) — the same
|
|
66
|
+
* 3-to-9 span, mirrored.
|
|
67
|
+
* - `anchorRatio` (0.15, x=6) is neither ramp endpoint — it is the ramp's
|
|
68
|
+
* MIDPOINT: `(slewStartRatio + slewEndRatio) / 2 === (0.075+0.225)/2
|
|
69
|
+
* === 0.15`. That is the transition's 50% crossing, which is why the
|
|
70
|
+
* engine anchors an annotation there rather than at either edge of the
|
|
71
|
+
* ramp.
|
|
72
|
+
* - Cross-checked end-to-end: rendering `{signal:[{wave:'0.1.0...',node:'a.b.c...'}],
|
|
73
|
+
* edge:['a-b']}` through `wavedrom.renderAny` puts the `gmark_a_b` path's
|
|
74
|
+
* endpoints at `cycle * 40 + 0.15 * 40` for both cycle 0 (`a`) and cycle 2
|
|
75
|
+
* (`b`) — see `test/wave-geometry.test.js`'s SKIN_METRICS block, which
|
|
76
|
+
* re-derives these same three ratios from `waveSkin` (from `0m1`,
|
|
77
|
+
* cross-checked against `1m0` — not from `0m0`) and re-runs this exact
|
|
78
|
+
* render, so a wavedrom bump that moves the skin turns that test red and
|
|
79
|
+
* names the number to re-pin.
|
|
80
|
+
*
|
|
81
|
+
* v3.6.0 Task 7 fix round 1 (ruling R23): `slewEndRatio` was originally
|
|
82
|
+
* pinned at 0.15 (`=== anchorRatio`) on the belief that the ramp finishes
|
|
83
|
+
* exactly where the anchor sits. It does not: that belief was measured
|
|
84
|
+
* against `0m0`, whose down-and-back peak coincidentally lands on the same
|
|
85
|
+
* x as the anchor while telling you nothing about where a REAL transition's
|
|
86
|
+
* ramp ends. Repinned to the measured 0.225 above once `0m1`/`1m0` (the
|
|
87
|
+
* actual transition bricks) were read instead.
|
|
88
|
+
*
|
|
89
|
+
* Pinned rather than derived at module load: this file cannot pull in the
|
|
90
|
+
* `wavedrom` package itself. In the browser, `lib/editor/server.js`'s CommonJS
|
|
91
|
+
* shim resolves that same import against a page-global registry keyed by module
|
|
92
|
+
* basename, and the shipped `wavedrom.min.js` bundle only attaches `ProcessAll` /
|
|
93
|
+
* `RenderWaveForm` / `EditorRefresh` / `eva` / `version` to that page global — no
|
|
94
|
+
* `waveSkin` — so a page-time derivation would read `undefined` on every real
|
|
95
|
+
* page, not just as a rare fallback. The tests, which run in Node against the
|
|
96
|
+
* real npm package, are where the derivation actually happens and where a
|
|
97
|
+
* version bump would be caught.
|
|
98
|
+
*/
|
|
99
|
+
const SKIN_METRICS = Object.freeze({
|
|
100
|
+
anchorRatio: 0.15,
|
|
101
|
+
slewStartRatio: 0.075,
|
|
102
|
+
slewEndRatio: 0.225,
|
|
103
|
+
});
|
|
104
|
+
|
|
47
105
|
/**
|
|
48
106
|
* Sizes to use when the caller has nothing better, read out of the engine's own
|
|
49
107
|
* `lane` module after a render rather than inferred: `lane.yo` (row pitch) is
|
|
@@ -59,6 +117,47 @@ const codec = require('./wave-codec.js');
|
|
|
59
117
|
*/
|
|
60
118
|
const DEFAULTS = { laneHeight: 30, cycleWidth: 40, nameColWidth: 40 };
|
|
61
119
|
|
|
120
|
+
/**
|
|
121
|
+
* v3.6.0 Task 6: the decorated bands above lane 0 — a ruler (always), an
|
|
122
|
+
* optional head-title band, and an optional foot-tick band below the last
|
|
123
|
+
* lane.
|
|
124
|
+
*
|
|
125
|
+
* v3.6.0 Task 9 fix round 1 (ruling R29): foot now gets a SECOND stacked
|
|
126
|
+
* band too, mirroring head's own two — `parse-config.js` sets `yf0 = 20`
|
|
127
|
+
* for a foot `tick`/`tock` and `yf1 = 46` for a foot `text`, the exact
|
|
128
|
+
* mirror of `yh0`/`yh1` for head, and both are legal at once in real
|
|
129
|
+
* WaveJSON. Before this fix `footHeight` was one band shared by text AND
|
|
130
|
+
* ticks, so a document setting both drew them on top of each other.
|
|
131
|
+
*
|
|
132
|
+
* 22 / 20 are layout constants, not derived from the engine: the engine's own
|
|
133
|
+
* canvas carries its own margin for this, and this editor's canvas only needs
|
|
134
|
+
* to reserve enough room to paint a row of 11px numerals (ruler) or a title
|
|
135
|
+
* (head text) without clipping it. `RULER_HEIGHT` and `FOOT_HEIGHT` are the
|
|
136
|
+
* same 22 because a foot band draws the same kind of tick row the ruler does,
|
|
137
|
+
* just below the last lane instead of above the first, and `FOOT_TEXT_HEIGHT`
|
|
138
|
+
* matches `HEAD_TEXT_HEIGHT` (20) for the same reason. It is the STRUCTURE —
|
|
139
|
+
* two stacked bands per side, not one shared band — that has to mirror the
|
|
140
|
+
* engine, never the pixel counts: the engine's own 46 for a text band is not
|
|
141
|
+
* a target to hit here, do not "correct" 20 to it.
|
|
142
|
+
*/
|
|
143
|
+
const RULER_HEIGHT = 22;
|
|
144
|
+
const HEAD_TEXT_HEIGHT = 20;
|
|
145
|
+
const FOOT_HEIGHT = 22;
|
|
146
|
+
const FOOT_TEXT_HEIGHT = 20;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether `doc[which]` (`'head'` or `'foot'`) is a WaveJSON banner object —
|
|
150
|
+
* any of the four keys the engine draws something for. A banner with none of
|
|
151
|
+
* these keys draws nothing, so it does not reserve a band either.
|
|
152
|
+
*/
|
|
153
|
+
function hasBanner(doc, which) {
|
|
154
|
+
if (doc === null || typeof doc !== 'object') return false;
|
|
155
|
+
const b = doc[which];
|
|
156
|
+
if (b === null || typeof b !== 'object') return false;
|
|
157
|
+
return b.text !== undefined || b.tick !== undefined ||
|
|
158
|
+
b.tock !== undefined || b.every !== undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
62
161
|
/**
|
|
63
162
|
* How many lanes are laid out before the rest are dropped.
|
|
64
163
|
*
|
|
@@ -185,42 +284,190 @@ function cyclesOf(lane) {
|
|
|
185
284
|
* overflow `width` to Infinity. Both are outside anything `getBoundingClientRect`
|
|
186
285
|
* can report, and neither is defended against here.
|
|
187
286
|
*/
|
|
287
|
+
/**
|
|
288
|
+
* `config.hscale`, normalized exactly the way the pinned engine's own
|
|
289
|
+
* `parseConfig` does it (`node_modules/wavedrom/lib/parse-config.js:4-21`,
|
|
290
|
+
* measured this session): `tonumber(x) = x > 0 ? Math.round(x) : 1`, rounded
|
|
291
|
+
* a second time, then clamped to 100. So `hscale: 2.5` renders as **3** in
|
|
292
|
+
* the engine, not 2.5, and `hscale: 250` clamps to **100** — a caller that
|
|
293
|
+
* skipped this and used the raw value would draw a canvas that disagrees
|
|
294
|
+
* with the engine's own preview, which is the exact defect class v3.6.0
|
|
295
|
+
* exists to remove (v3.6.0 Task 4 fix round 1, R15).
|
|
296
|
+
*
|
|
297
|
+
* A value that rounds to 0 or below (the engine's own `tonumber` maps every
|
|
298
|
+
* non-positive input to 1, and `Math.round(0.4)` is 0) is the engine's
|
|
299
|
+
* default of 1, same as a missing or non-numeric `hscale` — this is not an
|
|
300
|
+
* error, `config` is user data and a hand-edited stray value must still lay
|
|
301
|
+
* out rather than take the editor down.
|
|
302
|
+
*/
|
|
303
|
+
function hscaleOf(cfg) {
|
|
304
|
+
if (cfg === null || typeof cfg.hscale !== 'number' || !isFinite(cfg.hscale) ||
|
|
305
|
+
cfg.hscale <= 0) {
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const rounded = Math.round(cfg.hscale);
|
|
309
|
+
if (rounded <= 0) return 1;
|
|
310
|
+
return rounded > 100 ? 100 : rounded;
|
|
311
|
+
}
|
|
312
|
+
|
|
188
313
|
function layoutOf(doc, opts) {
|
|
189
314
|
const o = options(opts);
|
|
190
315
|
const laneHeight = size(o.laneHeight, DEFAULTS.laneHeight, 'laneHeight', false);
|
|
191
316
|
const cycleWidth = size(o.cycleWidth, DEFAULTS.cycleWidth, 'cycleWidth', false);
|
|
192
317
|
const nameColWidth = size(o.nameColWidth, DEFAULTS.nameColWidth, 'nameColWidth', true);
|
|
193
318
|
|
|
319
|
+
// `config.hscale` scales every lane's cycle width the same way the engine's
|
|
320
|
+
// own `lane.hscale` does — see `hscaleOf` for the exact rounding/clamping
|
|
321
|
+
// this mirrors.
|
|
322
|
+
const cfg = (doc !== null && typeof doc === 'object' &&
|
|
323
|
+
doc.config !== null && typeof doc.config === 'object') ? doc.config : null;
|
|
324
|
+
const hscale = hscaleOf(cfg);
|
|
325
|
+
|
|
326
|
+
// v3.6.0 Task 6: the ruler band always reserves room; a head.text banner
|
|
327
|
+
// adds a title band above it; lane 0 starts below both.
|
|
328
|
+
const headTextHeight = (doc !== null && typeof doc === 'object' &&
|
|
329
|
+
doc.head !== null && typeof doc.head === 'object' &&
|
|
330
|
+
doc.head.text !== undefined) ? HEAD_TEXT_HEIGHT : 0;
|
|
331
|
+
const originY = headTextHeight + RULER_HEIGHT;
|
|
332
|
+
const footHeight = hasBanner(doc, 'foot') ? FOOT_HEIGHT : 0;
|
|
333
|
+
// v3.6.0 Task 9 fix round 1 (R29): stacked BELOW `footHeight`, mirroring
|
|
334
|
+
// `headTextHeight` stacked ABOVE `RULER_HEIGHT` — `footHeight` alone still
|
|
335
|
+
// covers a foot with only `tick`/`tock`/`every` (untouched: a document
|
|
336
|
+
// that sets none of those keeps `footTextHeight` at 0 and its `height`
|
|
337
|
+
// pin does not move), and this adds the second band only when `foot.text`
|
|
338
|
+
// is actually present, exactly the condition `headTextHeight` already
|
|
339
|
+
// uses for `head.text`.
|
|
340
|
+
const footTextHeight = (doc !== null && typeof doc === 'object' &&
|
|
341
|
+
doc.foot !== null && typeof doc.foot === 'object' &&
|
|
342
|
+
doc.foot.text !== undefined) ? FOOT_TEXT_HEIGHT : 0;
|
|
343
|
+
|
|
194
344
|
const found = enumerateLanes(doc);
|
|
195
345
|
const lanes = [];
|
|
196
346
|
let cycles = 0;
|
|
197
347
|
for (let i = 0; i < found.lanes.length; i++) {
|
|
198
348
|
const laneCycles = cyclesOf(found.lanes[i].lane);
|
|
199
349
|
if (laneCycles > cycles) cycles = laneCycles;
|
|
350
|
+
const lane = found.lanes[i].lane;
|
|
351
|
+
// A wave character spans `period` diagram cycles, and the whole lane is
|
|
352
|
+
// then scaled by `hscale`. `phase` only shifts the origin, and by the
|
|
353
|
+
// BASE cycle width, not the effective one — the engine's own expression
|
|
354
|
+
// is `xs * (2*i*period*hscale - phase)`, and that `phase` term is never
|
|
355
|
+
// multiplied by hscale (measured in node_modules/wavedrom/lib/render-arcs.js).
|
|
356
|
+
const laneObj = (lane !== null && typeof lane === 'object') ? lane : {};
|
|
357
|
+
// Deliberate divergence from the engine (R15): the engine's own
|
|
358
|
+
// `element.period ? element.period : 1` passes a negative value straight
|
|
359
|
+
// through, which the renderer then turns into a negative pixel width.
|
|
360
|
+
// Here a non-positive `period` normalizes to 1 instead — a negative
|
|
361
|
+
// `cycleWidth` would make `reachOf`'s width bound run backwards and
|
|
362
|
+
// break R14(b) (`cellRect`/`cellAt` inverting each other), not just
|
|
363
|
+
// look wrong. Do not "fix" this back to match the engine bit-for-bit;
|
|
364
|
+
// the engine's own behaviour here is the bug, not this file's.
|
|
365
|
+
const period = (typeof laneObj.period === 'number' && isFinite(laneObj.period) &&
|
|
366
|
+
laneObj.period > 0) ? laneObj.period : 1;
|
|
367
|
+
const phase = (typeof laneObj.phase === 'number' && isFinite(laneObj.phase)) ?
|
|
368
|
+
laneObj.phase : 0;
|
|
200
369
|
lanes.push({
|
|
201
370
|
index: i,
|
|
202
|
-
y: i * laneHeight,
|
|
371
|
+
y: originY + i * laneHeight,
|
|
203
372
|
height: laneHeight,
|
|
204
373
|
cycles: laneCycles,
|
|
205
374
|
depth: found.lanes[i].path.length - 2,
|
|
206
375
|
path: found.lanes[i].path,
|
|
207
|
-
lane:
|
|
376
|
+
lane: lane,
|
|
377
|
+
cycleWidth: cycleWidth * period * hscale,
|
|
378
|
+
originX: nameColWidth - cycleWidth * phase,
|
|
208
379
|
});
|
|
209
380
|
}
|
|
210
381
|
if (lanes.length > 0 && cycles === 0) cycles = 1;
|
|
211
382
|
|
|
383
|
+
// Each lane can now have its own cycleWidth/originX, so the diagram's width
|
|
384
|
+
// is the widest lane's own right edge, not a single global expression. A
|
|
385
|
+
// lane with zero cycles of its own (an empty or wave-less lane) still
|
|
386
|
+
// reserves the same one-cycle floor documented above `layoutOf` — the
|
|
387
|
+
// engine renders a placeholder cycle for it too, so width must not collapse
|
|
388
|
+
// to the name column just because THAT lane happens to be the widest one.
|
|
389
|
+
let width = nameColWidth;
|
|
390
|
+
for (const row of lanes) {
|
|
391
|
+
const rowCycles = row.cycles > 0 ? row.cycles : 1;
|
|
392
|
+
const right = row.originX + rowCycles * row.cycleWidth;
|
|
393
|
+
if (right > width) width = right;
|
|
394
|
+
}
|
|
395
|
+
if (lanes.length === 0) width = nameColWidth;
|
|
396
|
+
|
|
212
397
|
return {
|
|
213
398
|
laneHeight: laneHeight,
|
|
214
399
|
cycleWidth: cycleWidth,
|
|
215
400
|
nameColWidth: nameColWidth,
|
|
401
|
+
hscale: hscale,
|
|
216
402
|
lanes: lanes,
|
|
217
403
|
cycles: cycles,
|
|
218
404
|
truncated: found.truncated,
|
|
219
|
-
width:
|
|
220
|
-
|
|
405
|
+
width: width,
|
|
406
|
+
// v3.6.0 Task 6: originY / rulerHeight / headTextHeight / footHeight —
|
|
407
|
+
// the vertical bands lane 0 and the last lane now sit between.
|
|
408
|
+
// v3.6.0 Task 9 fix round 1 (R29): footTextHeight — the second, outer
|
|
409
|
+
// foot band, present only when `foot.text` is set. `footHeight` stays
|
|
410
|
+
// the inner band (adjacent to the last lane), same as before this fix.
|
|
411
|
+
originY: originY,
|
|
412
|
+
rulerHeight: RULER_HEIGHT,
|
|
413
|
+
headTextHeight: headTextHeight,
|
|
414
|
+
footHeight: footHeight,
|
|
415
|
+
footTextHeight: footTextHeight,
|
|
416
|
+
height: originY + lanes.length * laneHeight + footHeight + footTextHeight,
|
|
221
417
|
};
|
|
222
418
|
}
|
|
223
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The group titles in the lane tree, each with the span of display rows it
|
|
422
|
+
* covers.
|
|
423
|
+
*
|
|
424
|
+
* A group title occupies NO ROW — measured in Task 2 against the pinned
|
|
425
|
+
* engine, and `wave-geometry` is built on it. What it occupies is horizontal
|
|
426
|
+
* 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
|
|
428
|
+
* would put the lane list one row out of step with the waveform beside it for
|
|
429
|
+
* every lane below the group, which is exactly the class of silent
|
|
430
|
+
* disagreement this batch keeps producing.
|
|
431
|
+
*
|
|
432
|
+
* The span is derived from the lanes' own paths — `layout.lanes[i].path` is
|
|
433
|
+
* the codec's — so a group with no lanes under it has no span and is not
|
|
434
|
+
* shown here; it is not addressable through the bridge either.
|
|
435
|
+
*
|
|
436
|
+
* 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
|
|
439
|
+
* unchanged; it now reads this file's own `valueAt` instead of a copy that
|
|
440
|
+
* used to live in `wave-ui.js`.
|
|
441
|
+
*/
|
|
442
|
+
function groupSpansOf(doc, layout) {
|
|
443
|
+
const byKey = new Map();
|
|
444
|
+
for (let i = 0; i < layout.lanes.length; i++) {
|
|
445
|
+
const path = layout.lanes[i].path;
|
|
446
|
+
// Every proper ancestor between `signal` and the lane itself is a group.
|
|
447
|
+
for (let cut = 2; cut < path.length; cut++) {
|
|
448
|
+
const prefix = path.slice(0, cut);
|
|
449
|
+
const key = JSON.stringify(prefix);
|
|
450
|
+
const seen = byKey.get(key);
|
|
451
|
+
if (seen === undefined) {
|
|
452
|
+
byKey.set(key, { path: prefix, depth: cut - 1, from: i, to: i });
|
|
453
|
+
} else {
|
|
454
|
+
seen.to = i;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const out = [];
|
|
459
|
+
for (const span of byKey.values()) {
|
|
460
|
+
const container = valueAt(doc, span.path);
|
|
461
|
+
const title = Array.isArray(container) && typeof container[0] === 'string'
|
|
462
|
+
? container[0] : null;
|
|
463
|
+
if (title === null) continue; // an untitled group has nothing to show
|
|
464
|
+
out.push({ path: span.path, title: title, depth: span.depth,
|
|
465
|
+
from: span.from, to: span.to });
|
|
466
|
+
}
|
|
467
|
+
out.sort(function (a, b) { return a.from - b.from || a.depth - b.depth; });
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
224
471
|
/** A real, finite coordinate — `'70'` and `NaN` are not one. */
|
|
225
472
|
function isCoord(n) {
|
|
226
473
|
return typeof n === 'number' && isFinite(n);
|
|
@@ -258,6 +505,35 @@ function indexAt(pos, origin, step, count) {
|
|
|
258
505
|
return i + 0; // `Math.floor(-0.2)` is -0, and -0 fails deepStrictEqual against 0
|
|
259
506
|
}
|
|
260
507
|
|
|
508
|
+
/**
|
|
509
|
+
* How many cells of `step` width, starting at `origin`, fit before `limit` —
|
|
510
|
+
* the largest `n >= 0` with `origin + n * step <= limit`.
|
|
511
|
+
*
|
|
512
|
+
* v3.6.0 Task 4 fix round 1 (Critical, ruling R14): once every lane can carry
|
|
513
|
+
* its own `cycleWidth` (via `period`/`hscale`), the "paint one past a short
|
|
514
|
+
* lane" affordance can no longer borrow the DIAGRAM's overall cycle count
|
|
515
|
+
* (`layout.cycles`) and multiply it by THIS lane's pixel width — that was
|
|
516
|
+
* exactly the bug: the two only ever agreed while every lane shared one
|
|
517
|
+
* `cycleWidth`, and diverged the moment `period` did not. `layout.width` is
|
|
518
|
+
* itself built from this same per-lane `originX + n * cycleWidth` expression
|
|
519
|
+
* (see `layoutOf`), so asking "how many of THIS lane's own cells fit inside
|
|
520
|
+
* that width" is the one bound guaranteed never to exceed it — R14(a).
|
|
521
|
+
*
|
|
522
|
+
* A floor-division estimate is only a proposal, for the identical reason
|
|
523
|
+
* `indexAt` above does not trust its own division: subtraction and division
|
|
524
|
+
* round differently, so the answer is snapped against the exact expression
|
|
525
|
+
* `cellRect` builds a cell with (`origin + n * step`), not derived from it
|
|
526
|
+
* algebraically. This is the same technique, run at the boundary between
|
|
527
|
+
* "reachable" and "not", instead of between one cell and the next.
|
|
528
|
+
*/
|
|
529
|
+
function reachOf(origin, step, limit) {
|
|
530
|
+
let n = Math.floor((limit - origin) / step);
|
|
531
|
+
if (n < 0) n = 0;
|
|
532
|
+
while (n > 0 && origin + n * step > limit) n--;
|
|
533
|
+
while (origin + (n + 1) * step <= limit) n++;
|
|
534
|
+
return n;
|
|
535
|
+
}
|
|
536
|
+
|
|
261
537
|
/**
|
|
262
538
|
* Which cell a point is in, or `null` for the name column, outside the diagram,
|
|
263
539
|
* or a coordinate that is not a number.
|
|
@@ -269,21 +545,40 @@ function indexAt(pos, origin, step, count) {
|
|
|
269
545
|
*
|
|
270
546
|
* A cycle past the end of that particular lane still hits: the lane is shorter
|
|
271
547
|
* than the diagram, painting there is how a lane gets extended, and the caller
|
|
272
|
-
* can see it went past from `layout.lanes[laneIndex].cycles`.
|
|
548
|
+
* can see it went past from `layout.lanes[laneIndex].cycles`. How far past is
|
|
549
|
+
* bounded by `reachOf` against THIS lane's own `originX`/`cycleWidth` and
|
|
550
|
+
* `layout.width` (v3.6.0 Task 4 fix round 1, R14) — never by reusing another
|
|
551
|
+
* lane's cycle count against this lane's pixel width, which is what let a
|
|
552
|
+
* short, wide-`period` lane hand out cells entirely outside the canvas.
|
|
553
|
+
*
|
|
554
|
+
* A lane whose `phase` shifts it left of the diagram's own x=0 has that many
|
|
555
|
+
* of its leading cycles entirely off-canvas; per R14(a) a cell only counts as
|
|
556
|
+
* hit if its WHOLE rect is inside `[0, layout.width]`, so those leading
|
|
557
|
+
* cycles are excluded the same way cycles past `layout.width` are.
|
|
273
558
|
*/
|
|
274
559
|
function cellAt(layout, x, y) {
|
|
275
560
|
if (layout === null || typeof layout !== 'object' || !Array.isArray(layout.lanes)) {
|
|
276
561
|
return null;
|
|
277
562
|
}
|
|
278
563
|
if (!isCoord(x) || !isCoord(y)) return null;
|
|
279
|
-
if (x <
|
|
280
|
-
|
|
564
|
+
if (x < 0) return null;
|
|
565
|
+
// v3.6.0 Task 6: lane 0 no longer starts at y=0 — the ruler band (and an
|
|
566
|
+
// optional head-title band) sits above it, and a press there must land on
|
|
567
|
+
// NOTHING, not fall through to lane 0 the way a naive `y < 0` guard would.
|
|
568
|
+
if (y < layout.originY || y >= layout.originY + layout.lanes.length * layout.laneHeight) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
281
571
|
if (layout.lanes.length === 0 || layout.cycles === 0) return null;
|
|
282
572
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
573
|
+
const laneIndex = indexAt(y, layout.originY, layout.laneHeight, layout.lanes.length);
|
|
574
|
+
const row = layout.lanes[laneIndex];
|
|
575
|
+
const reach = reachOf(row.originX, row.cycleWidth, layout.width);
|
|
576
|
+
if (reach === 0) return null;
|
|
577
|
+
if (x < row.originX || x >= row.originX + reach * row.cycleWidth) return null;
|
|
578
|
+
|
|
579
|
+
const cycle = indexAt(x, row.originX, row.cycleWidth, reach);
|
|
580
|
+
if (row.originX + cycle * row.cycleWidth < 0) return null; // R14(a): left edge off-canvas
|
|
581
|
+
return { laneIndex: laneIndex, cycle: cycle };
|
|
287
582
|
}
|
|
288
583
|
|
|
289
584
|
/**
|
|
@@ -294,19 +589,32 @@ function cellAt(layout, x, y) {
|
|
|
294
589
|
* top-left corner. A cycle past the diagram's width has no box, so extending a
|
|
295
590
|
* lane is an edit the caller makes and then lays out again, not a rectangle it
|
|
296
591
|
* can ask for in advance.
|
|
592
|
+
*
|
|
593
|
+
* v3.6.0 Task 4 fix round 1 (Critical, R14): "past the diagram's width" is now
|
|
594
|
+
* judged against THIS lane's own `originX`/`cycleWidth` via `reachOf`, not
|
|
595
|
+
* against `layout.cycles` (the diagram-wide cycle COUNT) — see `reachOf`'s own
|
|
596
|
+
* comment for why those two stopped being interchangeable the moment `period`
|
|
597
|
+
* could differ per lane. R14(a): every non-null rect returned here lies
|
|
598
|
+
* entirely within `[0, layout.width]`, both edges — a lane shifted left of
|
|
599
|
+
* x=0 by `phase` has that many of its own leading cycles return `null` too.
|
|
297
600
|
*/
|
|
298
601
|
function cellRect(layout, laneIndex, cycle) {
|
|
299
602
|
if (layout === null || typeof layout !== 'object' || !Array.isArray(layout.lanes)) {
|
|
300
603
|
return null;
|
|
301
604
|
}
|
|
302
605
|
if (!isIndex(laneIndex) || laneIndex < 0 || laneIndex >= layout.lanes.length) return null;
|
|
303
|
-
if (!isIndex(cycle) || cycle < 0
|
|
606
|
+
if (!isIndex(cycle) || cycle < 0) return null;
|
|
304
607
|
|
|
305
608
|
const row = layout.lanes[laneIndex];
|
|
609
|
+
const reach = reachOf(row.originX, row.cycleWidth, layout.width);
|
|
610
|
+
if (cycle >= reach) return null;
|
|
611
|
+
const x = row.originX + cycle * row.cycleWidth;
|
|
612
|
+
if (x < 0) return null; // R14(a): left edge off-canvas (a large positive `phase`)
|
|
613
|
+
|
|
306
614
|
return {
|
|
307
|
-
x:
|
|
615
|
+
x: x,
|
|
308
616
|
y: row.y,
|
|
309
|
-
width:
|
|
617
|
+
width: row.cycleWidth,
|
|
310
618
|
height: row.height,
|
|
311
619
|
};
|
|
312
620
|
}
|
|
@@ -314,10 +622,146 @@ function cellRect(layout, laneIndex, cycle) {
|
|
|
314
622
|
/** 端點把手的邊長(px)。夠大到手指按得到,小到不會蓋住相鄰格。 */
|
|
315
623
|
const EDGE_HANDLE = 12;
|
|
316
624
|
|
|
317
|
-
|
|
625
|
+
/**
|
|
626
|
+
* 一格的錨點——引擎畫 `node` 字母的那一點。
|
|
627
|
+
*
|
|
628
|
+
* 不是格子中心。使用者回報「連線應該要頭尾相接」,量到的病因是:v3.5.0 的畫布
|
|
629
|
+
* 把 edge 端點放在格子正中央,引擎的 `render-arcs.js` 卻把錨點放在
|
|
630
|
+
* `lane.xlabel`,也就是 skin 的 `socket.x`——`SKIN_METRICS.anchorRatio` 那個
|
|
631
|
+
* 比例——而轉態磚的斜坡正好在那一點跨過一半(`anchorRatio === (slewStartRatio +
|
|
632
|
+
* slewEndRatio) / 2`,是斜坡的中點、50% 跨越點,不是斜坡的終點——
|
|
633
|
+
* `SKIN_METRICS` 上方註解已經逐值對過引擎,R23 修正過這裡的舊說法)。MEASURED(pinned wavedrom
|
|
634
|
+
* 3.5.0):cycle 寬 40 時 a(cycle 0)的錨點在 x=6、b(cycle 2)在 x=86,
|
|
635
|
+
* 格線在 0/40/80。v3.5.0 用格子中心,於是畫布的線比引擎的右移了
|
|
636
|
+
* `(0.5 - anchorRatio)` 個 cycle。v3.6.0 Task 18 重算了這個偏移:引擎自己的
|
|
637
|
+
* 40px cycle 上是 **14px**(20 − 6),而使用者看到的是編輯器畫布的 48px cycle
|
|
638
|
+
* (`wave-ui.js` 的 `SIZES`),也就是 **16.8px**——先前這裡寫的「17px@40px
|
|
639
|
+
* cycle」把量到的距離(48px 尺度)配了錯的尺度名。一條標注上升緣的線因此
|
|
640
|
+
* 肉眼可見地沒有接上那個緣。
|
|
641
|
+
*
|
|
642
|
+
* 沿用 `cellRect` 而不是自己重算一次格子的位置——那一格「在不在圖裡」只有
|
|
643
|
+
* 一個答案,`cellRect`/`reachOf` 已經是它,錨點只是在那個答案上再加一個
|
|
644
|
+
* 固定比例的偏移。
|
|
645
|
+
*/
|
|
646
|
+
function anchorOfCell(layout, laneIndex, cell) {
|
|
318
647
|
const r = cellRect(layout, laneIndex, cell);
|
|
319
648
|
if (r === null) return null;
|
|
320
|
-
return {
|
|
649
|
+
return {
|
|
650
|
+
x: r.x + SKIN_METRICS.anchorRatio * r.width,
|
|
651
|
+
y: r.y + r.height / 2,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* `(x, y)` 最接近哪一個 cell 的錨點。
|
|
657
|
+
*
|
|
658
|
+
* 用 round 而不是 `cellAt` 的 floor:`cellAt` 回答「滑鼠現在壓在哪一格上」,
|
|
659
|
+
* 目標是游標底下那一格,floor 是對的——格子的左緣屬於自己是 `cellAt` 存在的
|
|
660
|
+
* 理由。`boundaryAt` 回答的是另一個問題:使用者想把 edge 端點釘在哪一個
|
|
661
|
+
* 「線」上,那條線畫在錨點,不是格子邊界,使用者的眼睛瞄準的是那個點,往左
|
|
662
|
+
* 偏一點點不該跳到前一格的錨點去。兩個函式故意不同,不是選了兩種寫法裡的
|
|
663
|
+
* 一種將就,統一成同一個以後有人「簡化」掉這個差異,行為就會跟著壞掉。
|
|
664
|
+
*
|
|
665
|
+
* 上界跟 `cellRect` 用的是同一顆 `reachOf`,不是 `layout.cycles`(整張圖的
|
|
666
|
+
* cycle 數)——v3.6.0 Task 4 fix round 1(R14)已經證明過後者在每條 lane
|
|
667
|
+
* 自己的 `period`/`hscale` 不同時會跟這條 lane 自己的像素寬度對不上,
|
|
668
|
+
* `boundaryAt` 若另外相信 `layout.cycles`,就是同一個「兩處定義,中間那層
|
|
669
|
+
* 假設它們一致」的錯誤又長出一次:這裡要保證的不變量是「`boundaryAt` 回傳
|
|
670
|
+
* 的 cell,`cellRect` 對它永遠不是 null」,兩個函式各自的上界只能有一份。
|
|
671
|
+
*
|
|
672
|
+
* v3.6.0 Task 5 fix round 1(Critical):`reachOf` 只是 `cellRect` 兩道關卡的
|
|
673
|
+
* 右邊那一道,左邊那一道(`phase` 把 `originX` 推成負值時,這一格的左緣整個
|
|
674
|
+
* 在畫面外——R14(a))先前這裡漏掉了,於是 `boundaryAt` 對著一條大 `phase` 的
|
|
675
|
+
* lane 會把左邊那幾格也算進可觸及範圍,回傳一個 `cellRect` 判成 null 的
|
|
676
|
+
* cell,UI 拿著它去編輯會寫到不存在的地方。修法不是另外發明一個左界算式,
|
|
677
|
+
* 是照抄 `cellRect` 自己判左緣的那一行(`row.originX + cell * row.cycleWidth
|
|
678
|
+
* < 0`)——兩個函式判斷「這一格在不在畫面裡」只能有一份答案,`reachOf` 判
|
|
679
|
+
* 右邊、這一行判左邊,加起來才是 `cellRect` 的完整兩道關卡。
|
|
680
|
+
*/
|
|
681
|
+
function boundaryAt(layout, x, y) {
|
|
682
|
+
if (layout === null || typeof layout !== 'object' || !Array.isArray(layout.lanes)) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
if (!isCoord(x) || !isCoord(y)) return null;
|
|
686
|
+
// v3.6.0 Task 6: same vertical origin shift as `cellAt` — a press in the
|
|
687
|
+
// ruler/head band must not resolve to lane 0. `boundaryAt` picks the
|
|
688
|
+
// NEAREST boundary rather than floor-dividing, but it still has to agree
|
|
689
|
+
// with `cellAt` on where lane 0's own row starts; two different vertical
|
|
690
|
+
// origins in the same file is exactly the "two definitions, one layer
|
|
691
|
+
// assumes they agree" defect class this codebase has already paid for.
|
|
692
|
+
if (y < layout.originY || y >= layout.originY + layout.lanes.length * layout.laneHeight) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
if (layout.lanes.length === 0 || layout.cycles === 0) return null;
|
|
696
|
+
const laneIndex = indexAt(y, layout.originY, layout.laneHeight, layout.lanes.length);
|
|
697
|
+
const row = layout.lanes[laneIndex];
|
|
698
|
+
const reach = reachOf(row.originX, row.cycleWidth, layout.width);
|
|
699
|
+
if (reach === 0) return null;
|
|
700
|
+
const raw = (x - row.originX - SKIN_METRICS.anchorRatio * row.cycleWidth) / row.cycleWidth;
|
|
701
|
+
let cell = Math.round(raw);
|
|
702
|
+
if (cell < 0) cell = 0;
|
|
703
|
+
if (cell > reach - 1) cell = reach - 1;
|
|
704
|
+
if (row.originX + cell * row.cycleWidth < 0) return null; // R14(a): same left-edge exclusion as cellRect
|
|
705
|
+
return { laneIndex: laneIndex, cell: cell + 0 }; // `+0` folds a possible -0, same reason as `indexAt`
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* 一條 lane 的轉態 cell。
|
|
710
|
+
*
|
|
711
|
+
* v3.6.0 Task 11 fix round 3:這裡原本寫「一個 cycle 若電位跟前一個 cycle
|
|
712
|
+
* 不同(或是文件第一拍本身),使用者才會想把 edge 端點釘在那裡」——那句話
|
|
713
|
+
* 描述的是另一支函式,不是下面這支。真正的規則是「引擎會在那一格畫出東西
|
|
714
|
+
* 的那些格」:wave 字串裡每一個顯式宣告電位的字元(不是 `.`、不是 `|`),
|
|
715
|
+
* 包含跟前一格同值的顯式重複,因為引擎在那裡照樣畫出東西——一般電位在
|
|
716
|
+
* 顯式重複處是 `0m0`/`1m1` 的凹痕,bus 是 `vmv-*` 的接縫,`.`(延續上一格)
|
|
717
|
+
* 才是真正「什麼都不畫」的那一種。MEASURED(pinned wavedrom 3.5.0,逐
|
|
718
|
+
* half-brick 讀出來的):
|
|
719
|
+
*
|
|
720
|
+
* '0011' -> 000, 000, 0m0, 000, 0m1, 111, 1m1, 111 顯式重複處也有凹痕
|
|
721
|
+
* '0.1.' -> 000, 000, 000, 000, 0m1, 111, 111, 111 用 `.` 的地方什麼都沒有
|
|
722
|
+
* '22' -> vvv-2, vvv-2, vmv-2-2, vvv-2 bus 顯式重複也有接縫
|
|
723
|
+
*
|
|
724
|
+
* 「跟前一個 cycle 電位不同」不是這條規則、也不能拿來當判斷式:`0011` 的
|
|
725
|
+
* 第二格是顯式 `0`、跟前一格同值,量到的 `0m0` 證明它依然是引擎會畫出東西
|
|
726
|
+
* 的一格,是真的轉態。若照著那句錯的舊註解把這支函式「修」成比對前後電位
|
|
727
|
+
* 是否相同,會把這一格漏掉——這正是這次要防的那個方向的錯,不是要修的
|
|
728
|
+
* 方向。
|
|
729
|
+
*
|
|
730
|
+
* (我們自己的畫布不畫這些凹痕——`brickPath` 在 `prevLevel === level`
|
|
731
|
+
* 時回傳一條平線,見 `wave-draw.js`——所以一顆轉態標記有可能落在畫布上什麼
|
|
732
|
+
* 都沒畫的地方。這是畫磚層本身的真實落差,v3.5.0 就已經如此,不在這個
|
|
733
|
+
* function 的職責範圍內,這裡只負責如實描述 `transitionsOf` 自己做的事。)
|
|
734
|
+
*
|
|
735
|
+
* 資料問的是 `codec.expandWave`,跟畫布展開波形用的是同一份:另寫一份展開
|
|
736
|
+
* 邏輯會跟它漂移,那正是這個版本要清掉的病。`held`(來源寫的是 `.`)不算
|
|
737
|
+
* 轉態;`|`(斷點)本身也不算,但它後面那格算——斷點之後電位重新宣告,即使
|
|
738
|
+
* 值沒變,那正是「不算 `.`」的同一條規則自然帶出來的結果。
|
|
739
|
+
*
|
|
740
|
+
* 走訪順序是 codec 攤平後的顯示序,跟 `layoutOf`/`enumerateLanes` 問
|
|
741
|
+
* `codec.lanePaths` 是同一份列舉:直接問它,而不是先 `layoutOf` 整份文件只
|
|
742
|
+
* 為了讀一個欄位——`layoutOf` 的列舉是 O(N)、量到的成本見上方 `MAX_LANES`
|
|
743
|
+
* 的註解,但它同時算版面的每一格座標、每條 lane 的 cycles,這裡只需要一條
|
|
744
|
+
* lane 的 `wave` 字串,兩者要做的工不同,沒有理由把後者的工都做一遍。
|
|
745
|
+
* `MAX_LANES` 這條線在這裡也照樣守:超過上限的 lane 對 `layoutOf` 是截斷、
|
|
746
|
+
* 不可觸及,`transitionsOf` 問同一個 lane 也該是同一個「不可觸及」的答案,
|
|
747
|
+
* 不能因為抄了近路就多看見一條 `layoutOf` 看不到的 lane。
|
|
748
|
+
*/
|
|
749
|
+
function transitionsOf(doc, laneIndex) {
|
|
750
|
+
if (doc === null || typeof doc !== 'object') return [];
|
|
751
|
+
const paths = codec.lanePaths(doc);
|
|
752
|
+
const n = paths.length < MAX_LANES ? paths.length : MAX_LANES;
|
|
753
|
+
if (!isIndex(laneIndex) || laneIndex < 0 || laneIndex >= n) return [];
|
|
754
|
+
const lane = valueAt(doc, paths[laneIndex]);
|
|
755
|
+
const wave = (lane !== null && typeof lane === 'object' && typeof lane.wave === 'string') ?
|
|
756
|
+
lane.wave : '';
|
|
757
|
+
const cells = codec.expandWave(wave);
|
|
758
|
+
const out = [];
|
|
759
|
+
for (let i = 0; i < cells.length; i += 1) {
|
|
760
|
+
if (cells[i].held === true) continue;
|
|
761
|
+
if (cells[i].ch === '|') continue;
|
|
762
|
+
out.push(i);
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
321
765
|
}
|
|
322
766
|
|
|
323
767
|
/**
|
|
@@ -352,7 +796,7 @@ function centerOfCell(layout, laneIndex, cell) {
|
|
|
352
796
|
* ALSO refuses to draw — measured against `node_modules/wavedrom`, an
|
|
353
797
|
* edge naming an absent letter renders nothing, same as here.
|
|
354
798
|
* - `'out-of-range'` — the shape parsed and both letters resolve to a real
|
|
355
|
-
* `{lane, cell}`, but that cell has no box in this layout: `
|
|
799
|
+
* `{lane, cell}`, but that cell has no box in this layout: `anchorOfCell`
|
|
356
800
|
* returned `null`, which happens when a lane's `node` string outlives the
|
|
357
801
|
* lane itself (`insertCycles`/`deleteCycles` narrow `wave` but do not
|
|
358
802
|
* reindex `node`, so a deleted cycle can leave an anchor past the lane's
|
|
@@ -388,8 +832,8 @@ function edgeLayoutDetail(doc, layout) {
|
|
|
388
832
|
skipped.push({ index: i, edge: edge, reason: 'dangling' });
|
|
389
833
|
continue;
|
|
390
834
|
}
|
|
391
|
-
const from =
|
|
392
|
-
const to =
|
|
835
|
+
const from = anchorOfCell(layout, a.at, a.cell);
|
|
836
|
+
const to = anchorOfCell(layout, b.at, b.cell);
|
|
393
837
|
if (from === null || to === null) {
|
|
394
838
|
skipped.push({ index: i, edge: edge, reason: 'out-of-range' });
|
|
395
839
|
continue;
|
|
@@ -491,8 +935,13 @@ function edgeHandleAt(edges, x, y) {
|
|
|
491
935
|
|
|
492
936
|
module.exports = {
|
|
493
937
|
layoutOf: layoutOf,
|
|
938
|
+
valueAt: valueAt,
|
|
939
|
+
groupSpansOf: groupSpansOf,
|
|
494
940
|
cellAt: cellAt,
|
|
495
941
|
cellRect: cellRect,
|
|
942
|
+
anchorOfCell: anchorOfCell,
|
|
943
|
+
boundaryAt: boundaryAt,
|
|
944
|
+
transitionsOf: transitionsOf,
|
|
496
945
|
edgeLayout: edgeLayout,
|
|
497
946
|
edgeLayoutDetail: edgeLayoutDetail,
|
|
498
947
|
edgeHandleRect: edgeHandleRect,
|
|
@@ -500,4 +949,5 @@ module.exports = {
|
|
|
500
949
|
EDGE_HANDLE: EDGE_HANDLE,
|
|
501
950
|
DEFAULTS: DEFAULTS,
|
|
502
951
|
MAX_LANES: MAX_LANES,
|
|
952
|
+
SKIN_METRICS: SKIN_METRICS,
|
|
503
953
|
};
|