@helping-ai-workflow/md2doc 3.5.0 → 3.6.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.
- package/lib/editor/cli.js +2 -2
- package/lib/editor/client.js +82 -5
- package/lib/editor/server.js +225 -49
- 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
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canvas drawing layer.
|
|
5
|
+
*
|
|
6
|
+
* The nine functions moved out of `wave-ui.js`, behaviour unchanged line for
|
|
7
|
+
* line -- this module is a pure move, not a rewrite. The interface is a
|
|
8
|
+
* factory: `wave-ui.js` hands in its own `document`, `geometry`, `codec` and
|
|
9
|
+
* `SIZES` (plus a handful of small helpers/constants the moved functions
|
|
10
|
+
* turned out to depend on -- see task-1-report.md for the exact list), and
|
|
11
|
+
* gets back a set of drawing functions closed over those dependencies.
|
|
12
|
+
*
|
|
13
|
+
* This layer does not require `wave-panels.js` (the dependency direction is
|
|
14
|
+
* one-way, see the plan's File Structure). Any state the drawing functions
|
|
15
|
+
* need from the dialog is passed in per call, either via an explicit
|
|
16
|
+
* parameter (the SVG element being drawn into) or via the `state` parameter,
|
|
17
|
+
* a plain data object the caller builds fresh for each call.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function createDrawer(deps) {
|
|
21
|
+
const d = deps.d;
|
|
22
|
+
const geometry = deps.geometry;
|
|
23
|
+
const codec = deps.codec;
|
|
24
|
+
const SIZES = deps.SIZES;
|
|
25
|
+
const SVGNS = deps.SVGNS;
|
|
26
|
+
const BRUSHES = deps.BRUSHES;
|
|
27
|
+
const labelsOf = deps.labelsOf;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What wavedrom 3.5.0 actually draws for one level, by its own brick id.
|
|
31
|
+
*
|
|
32
|
+
* Every row was MEASURED in this session against the pinned engine, by
|
|
33
|
+
* rendering `wave: 'z' + c + c` (the `z` anchors the char so it is never at
|
|
34
|
+
* cycle 0) and reading the second half-brick of each cycle out of the
|
|
35
|
+
* rendered SVG's `<use xlink:href="#…">` list:
|
|
36
|
+
*
|
|
37
|
+
* 0 000 1 111 x xxx z zzz u uuu d ddd
|
|
38
|
+
* h 111 l 000 H 111 L 000
|
|
39
|
+
* p nclk P nclk n pclk N pclk
|
|
40
|
+
* = vvv-2 2 vvv-2 3 vvv-3 … 9 vvv-9
|
|
41
|
+
* anything else xxx (measured with 'Q' and 'ä')
|
|
42
|
+
*
|
|
43
|
+
* So this is not a re-implementation of the engine's level model — `levelsOf`
|
|
44
|
+
* is that, and it lives in the codec. This is the last step: which PICTURE
|
|
45
|
+
* the engine paints for a level it has already decided. Keeping it in
|
|
46
|
+
* production code rather than in the test is deliberate: the drawing has to
|
|
47
|
+
* make exactly this decision anyway (an `h` lane is drawn high, an `=` lane
|
|
48
|
+
* is drawn as a bus), and a copy of it living only in a test would be a
|
|
49
|
+
* second belief about the engine with nothing pinning it to the first.
|
|
50
|
+
*/
|
|
51
|
+
function brickOf(level) {
|
|
52
|
+
if (typeof level !== 'string' || level.length === 0) return 'xxx';
|
|
53
|
+
if (level === '0' || level === 'l' || level === 'L') return '000';
|
|
54
|
+
if (level === '1' || level === 'h' || level === 'H') return '111';
|
|
55
|
+
if (level === 'z') return 'zzz';
|
|
56
|
+
if (level === 'u') return 'uuu';
|
|
57
|
+
if (level === 'd') return 'ddd';
|
|
58
|
+
if (level === 'p' || level === 'P') return 'nclk';
|
|
59
|
+
if (level === 'n' || level === 'N') return 'pclk';
|
|
60
|
+
if (level === '=') return 'vvv-2';
|
|
61
|
+
if (level >= '2' && level <= '9') return 'vvv-' + level;
|
|
62
|
+
return 'xxx';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A brick that draws a whole clock period and therefore never merges with
|
|
66
|
+
* its neighbour: two `p` cycles are two clock periods, not one long one. */
|
|
67
|
+
function isClock(brick) {
|
|
68
|
+
return brick === 'nclk' || brick === 'pclk';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isBus(brick) {
|
|
72
|
+
return brick.slice(0, 4) === 'vvv-';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The SVG path for one cell's picture, in LOCAL coordinates (0,0 is the
|
|
77
|
+
* cell's own top-left; `d.createElementNS` callers translate it onto the
|
|
78
|
+
* real canvas — see `drawLane`'s `translatePath`).
|
|
79
|
+
*
|
|
80
|
+
* A real level change is not a vertical line: the engine holds the OLD
|
|
81
|
+
* level until `slewStartRatio` into the cell, ramps to the NEW level, and
|
|
82
|
+
* holds it for the rest of the cell — `node`/edge anchors (`anchorRatio`,
|
|
83
|
+
* `wave-geometry.js`) land partway up that ramp, not at a vertical jump
|
|
84
|
+
* that has nothing under it. `|` (gap) and a cell with no previous level
|
|
85
|
+
* (the first cell of a lane) are not transitions and get a flat line.
|
|
86
|
+
*
|
|
87
|
+
* MEASURED against `node_modules/wavedrom` 3.5.0's `waveSkin`, dumped
|
|
88
|
+
* directly (not recalled) — and specifically against the bricks that
|
|
89
|
+
* actually CHANGE level, `0m1`/`1m0`, not the same-level `0m0` blip
|
|
90
|
+
* `wave-geometry.js`'s own `SKIN_METRICS` comment originally (wrongly)
|
|
91
|
+
* measured `slewEndRatio` from:
|
|
92
|
+
*
|
|
93
|
+
* 0m1 "M0,20 3,20 9,0 20,0" -> (0,20)(3,20)(9,0)(20,0)
|
|
94
|
+
* 1m0 "m0,0 3,0 6,20 11,0" (relative) -> (0,0)(3,0)(9,20)(20,20)
|
|
95
|
+
*
|
|
96
|
+
* Both put the diagonal from local x=3 to local x=9 out of a 40-wide cycle
|
|
97
|
+
* — `slewStartRatio` (0.075) to `slewEndRatio` (0.225, repinned by R23;
|
|
98
|
+
* see `wave-geometry.js`'s `SKIN_METRICS` comment for the full
|
|
99
|
+
* derivation and why `anchorRatio` is the ramp's MIDPOINT rather than its
|
|
100
|
+
* end). `slewEndRatio` is used directly below rather than recomputed from
|
|
101
|
+
* `anchorRatio`/`slewStartRatio` — now that the constant itself is
|
|
102
|
+
* correct, a second formula for the same point is exactly the "one fact,
|
|
103
|
+
* two definitions" failure this repo keeps paying for, not a safeguard.
|
|
104
|
+
*/
|
|
105
|
+
function brickPath(level, prevLevel, width, height) {
|
|
106
|
+
const s = geometry.SKIN_METRICS;
|
|
107
|
+
const rampStart = s.slewStartRatio * width;
|
|
108
|
+
const rampEnd = s.slewEndRatio * width;
|
|
109
|
+
const yFor = function (ch) {
|
|
110
|
+
if (ch === '1' || ch === 'P' || ch === 'h') return 0;
|
|
111
|
+
if (ch === '0' || ch === 'n' || ch === 'l') return height;
|
|
112
|
+
return height / 2;
|
|
113
|
+
};
|
|
114
|
+
const yNow = yFor(level);
|
|
115
|
+
if (level === '|' || prevLevel === null || prevLevel === level) {
|
|
116
|
+
return 'M0,' + yNow + ' L' + width + ',' + yNow;
|
|
117
|
+
}
|
|
118
|
+
const yPrev = yFor(prevLevel);
|
|
119
|
+
return 'M0,' + yPrev +
|
|
120
|
+
' L' + rampStart + ',' + yPrev +
|
|
121
|
+
' L' + rampEnd + ',' + yNow +
|
|
122
|
+
' L' + width + ',' + yNow;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The set of brick-id PAIRS `brickPath`'s straight ramp applies to.
|
|
127
|
+
*
|
|
128
|
+
* v3.6.0 Task 7 fix round 2 (ruling R24): this used to be a DENY-list
|
|
129
|
+
* (exclude bus/xxx/clock, ramp everything else), which silently opted
|
|
130
|
+
* `zzz`/`uuu`/`ddd` into the same straight-line ramp shape as `000`/`111`
|
|
131
|
+
* even though nobody had measured whether the engine actually draws them
|
|
132
|
+
* that way. It does not. MEASURED against `node_modules/wavedrom` 3.5.0's
|
|
133
|
+
* `waveSkin` — every brick a transition touching `z`/`u`/`d` can produce:
|
|
134
|
+
*
|
|
135
|
+
* 0mz "m0,20 3,0 C 10,10 15,10 20,10" curve; reaches mid only at x=20 (ratio 0.5)
|
|
136
|
+
* 1mz "m0,0 3,0 c 7,10 12,10 17,10" curve
|
|
137
|
+
* zm1 "M0,10 6,10 9,0 20,0" ramp starts at x=6 (0.15), not x=3
|
|
138
|
+
* zm0 "m0,10 6,0 3,10 11,0" same: ramp 6->9, not 3->9
|
|
139
|
+
* 0mu "m0,20 3,0 C 7,10 10.107603,0 20,0" curve
|
|
140
|
+
* um0 "m0,0 3,0 6,20 11,0" straight 3->9 -- matches ours by coincidence
|
|
141
|
+
* 0md "m8,20 10,0" a stub, not a ramp at all
|
|
142
|
+
* 1mu "M0,0 5,0" a stub, not a ramp at all
|
|
143
|
+
*
|
|
144
|
+
* Only `0m1`/`1m0` (see `brickPath`'s own comment) are the straight
|
|
145
|
+
* `slewStartRatio`-to-`slewEndRatio` ramp this file draws. Everything
|
|
146
|
+
* else touching `z`/`u`/`d` either curves, starts its ramp at a different
|
|
147
|
+
* ratio, or isn't a ramp at all, so this is now an ALLOW-list of exactly
|
|
148
|
+
* the measured, straight case (`000`<->`111`) rather than a deny-list of
|
|
149
|
+
* the shapes known not to work — a deny-list opts every brick nobody has
|
|
150
|
+
* measured yet INTO unverified new behaviour by default; an allow-list
|
|
151
|
+
* makes the next brush someone adds default back to the pre-existing
|
|
152
|
+
* vertical `<line>` instead. Bus (hexagon) and `xxx` (box) already carry
|
|
153
|
+
* their own slanted/boxed edges and stay out of scope the same way; clock
|
|
154
|
+
* bricks draw a whole period with their own sharp vertical edges built in
|
|
155
|
+
* (MEASURED: `pclk`/`nclk` are plain verticals, `'M0,20 0,0 20,0'`/
|
|
156
|
+
* `'m0,0 0,20 20,0'`, no ramp at all) and never reach here (`drawLane`'s
|
|
157
|
+
* `!isClock` guard on both sides).
|
|
158
|
+
*
|
|
159
|
+
* Deliberately checked on the BRICK ids, not on the level chars
|
|
160
|
+
* `lineLevelOf` returns: `lineLevelOf('uuu') === '1'`, same as `'111'`,
|
|
161
|
+
* so a check written against the level chars would let `uuu` straight
|
|
162
|
+
* back through this allow-list.
|
|
163
|
+
*/
|
|
164
|
+
function isRampPair(a, b) {
|
|
165
|
+
return (a === '000' || a === '111') && (b === '000' || b === '111');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `brickPath`'s own `yFor` wants a level char, not a brick id. Now that
|
|
170
|
+
* `isRampPair` (R24) only ever admits `000`/`111`, this only ever sees
|
|
171
|
+
* those two — it does not need to (and must not silently claim to) cover
|
|
172
|
+
* `zzz`/`uuu`/`ddd` any more.
|
|
173
|
+
*/
|
|
174
|
+
function lineLevelOf(brick) {
|
|
175
|
+
return brick === '111' ? '1' : '0';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `brickPath` returns LOCAL coordinates (matching how
|
|
180
|
+
* `test/wave-draw.test.js` calls it directly) — every other shape in this
|
|
181
|
+
* file is drawn in absolute canvas coordinates with no `transform`
|
|
182
|
+
* attribute anywhere, so this shifts the returned path onto the cell it
|
|
183
|
+
* is actually drawn at rather than introducing the one `transform` this
|
|
184
|
+
* file would otherwise have.
|
|
185
|
+
*/
|
|
186
|
+
function translatePath(pathStr, dx, dy) {
|
|
187
|
+
return pathStr.replace(/(-?[0-9.]+),(-?[0-9.]+)/g, function (_, x, y) {
|
|
188
|
+
return (Number(x) + dx) + ',' + (Number(y) + dy);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which vertical band a brick's own flat run sits at, in PIXELS. Lifted
|
|
194
|
+
* out of `drawLane` (v3.6.0 Task 10) so it can also drive `levelIcon`'s
|
|
195
|
+
* height decision — `uuu`/`ddd` painting at the SAME height as `111`/`000`
|
|
196
|
+
* (not at mid, which is `brickPath`'s own `yFor` default for any char it
|
|
197
|
+
* does not explicitly recognise) is a fact this function already owns,
|
|
198
|
+
* and Task 10's own brief warns against a second belief about it.
|
|
199
|
+
*/
|
|
200
|
+
function bandOf(brick, hi, lo, mid) {
|
|
201
|
+
if (brick === '111' || brick === 'uuu') return [hi, hi];
|
|
202
|
+
if (brick === '000' || brick === 'ddd') return [lo, lo];
|
|
203
|
+
if (brick === 'zzz') return [mid, mid];
|
|
204
|
+
return [hi, lo];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The bus hexagon's point list — lifted out of `drawLane`'s bus branch
|
|
208
|
+
* (v3.6.0 Task 10) so `levelIcon`'s bus glyph is the exact same hexagon,
|
|
209
|
+
* not a second hand-drawn approximation of it. */
|
|
210
|
+
function busPolygonPoints(x0, x1, hi, lo, mid, s) {
|
|
211
|
+
return (x0 + s) + ',' + hi + ' ' + (x1 - s) + ',' + hi + ' ' +
|
|
212
|
+
x1 + ',' + mid + ' ' + (x1 - s) + ',' + lo + ' ' +
|
|
213
|
+
(x0 + s) + ',' + lo + ' ' + x0 + ',' + mid;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The clock brick's stepped edge — lifted out of `drawLane`'s `isClock`
|
|
217
|
+
* branch (v3.6.0 Task 10), same reasoning as `busPolygonPoints`. */
|
|
218
|
+
function clockStepPath(brick, x0, x1, hi, lo) {
|
|
219
|
+
const xm = (x0 + x1) / 2;
|
|
220
|
+
return brick === 'nclk'
|
|
221
|
+
? 'M' + x0 + ',' + lo + ' L' + x0 + ',' + hi + ' L' + xm + ',' + hi +
|
|
222
|
+
' L' + xm + ',' + lo + ' L' + x1 + ',' + lo
|
|
223
|
+
: 'M' + x0 + ',' + hi + ' L' + x0 + ',' + lo + ' L' + xm + ',' + lo +
|
|
224
|
+
' L' + xm + ',' + hi + ' L' + x1 + ',' + hi;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The `P`/`N` explicit-edge arrowhead — lifted out of `drawLane`'s
|
|
228
|
+
* `isClock` branch (v3.6.0 Task 10). `null` for any other level char,
|
|
229
|
+
* same as the inline `if` it replaces. */
|
|
230
|
+
function clockArrowPath(edgeLevel, x0, xm, hi, lo) {
|
|
231
|
+
if (edgeLevel !== 'P' && edgeLevel !== 'N') return null;
|
|
232
|
+
const sx = (xm - x0) / 20;
|
|
233
|
+
const sy = (lo - hi) / 20;
|
|
234
|
+
const apexY = edgeLevel === 'P' ? hi + 3 * sy : hi + 17 * sy;
|
|
235
|
+
const baseY = edgeLevel === 'P' ? hi + 12 * sy : hi + 8 * sy;
|
|
236
|
+
return 'M' + (x0 - 3 * sx) + ',' + baseY +
|
|
237
|
+
' L' + x0 + ',' + apexY +
|
|
238
|
+
' L' + (x0 + 3 * sx) + ',' + baseY + ' Z';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The gap `|` marker's double chevron — lifted out of `drawLane`'s gap
|
|
242
|
+
* loop (v3.6.0 Task 10), same reasoning as `busPolygonPoints`. */
|
|
243
|
+
function gapMarkPath(xm, hi, lo, mid) {
|
|
244
|
+
return 'M' + (xm - 5) + ',' + (lo + 3) + ' L' + (xm + 1) + ',' + mid +
|
|
245
|
+
' L' + (xm - 5) + ',' + (hi - 3) +
|
|
246
|
+
' M' + (xm - 1) + ',' + (lo + 3) + ' L' + (xm + 5) + ',' + mid +
|
|
247
|
+
' L' + (xm - 1) + ',' + (hi - 3);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The toolbar's per-brush glyph — v3.6.0 Task 10.
|
|
252
|
+
*
|
|
253
|
+
* Every shape below is built from the SAME functions `drawLane` calls to
|
|
254
|
+
* paint the real canvas (`brickOf`, `bandOf`, `brickPath`, `isBus`,
|
|
255
|
+
* `isClock`, `busPolygonPoints`, `clockStepPath`, `clockArrowPath`,
|
|
256
|
+
* `gapMarkPath`) — never a second, hand-drawn set. That is the one rule
|
|
257
|
+
* this task exists to enforce: change a brush's on-canvas appearance and
|
|
258
|
+
* every toolbar icon updates with it, because there is only one place
|
|
259
|
+
* that appearance is decided.
|
|
260
|
+
*
|
|
261
|
+
* A glyph is a single cell with no previous level, so `brickPath` is only
|
|
262
|
+
* called here with the SAME char as both `level` and `prevLevel` — its
|
|
263
|
+
* flat-line case, never the ramp. That flat line is the RIGHT picture for
|
|
264
|
+
* a plain level (`0`/`1`/`h`/`l`/`z`/`u`/`d`): that is genuinely what the
|
|
265
|
+
* engine paints for one of those. It is the WRONG picture for a bus, a
|
|
266
|
+
* clock, `xxx` or a gap — those are not flat lines on canvas, so drawing
|
|
267
|
+
* one for them would be exactly the "worse than the bare character"
|
|
268
|
+
* failure the brief warns about; those five get their own shape instead,
|
|
269
|
+
* built from the same lane-drawing helpers, with no flat line underneath.
|
|
270
|
+
*
|
|
271
|
+
* `uuu`/`ddd` are the landmine `brickPath` on its own does not resolve:
|
|
272
|
+
* its own `yFor` only recognises the 000/111 ramp endpoints (R24) and
|
|
273
|
+
* silently drops any other char — `u`/`d` included — at mid, which
|
|
274
|
+
* disagrees with `bandOf`'s `uuu -> hi`, `ddd -> lo` on the real canvas.
|
|
275
|
+
* So the level handed to `brickPath` here is redirected through `bandOf`
|
|
276
|
+
* first (via `brick`), not passed through raw. MEASURED (see
|
|
277
|
+
* task-10-report.md): calling `brickPath(ch, ch, 24, 16)` for the raw
|
|
278
|
+
* character of all 22 brushes — the brief's own sketch — puts 13 of them
|
|
279
|
+
* (`x`, `z`, `p`, `N`, `u`, `d`, and every bus digit) on the exact SAME
|
|
280
|
+
* `M0,8 L24,8` mid line, and gets `P`/`n` backwards relative to which
|
|
281
|
+
* half of their clock cycle actually starts low vs. high.
|
|
282
|
+
*/
|
|
283
|
+
function levelIcon(level) {
|
|
284
|
+
const W = 24;
|
|
285
|
+
const H = 16;
|
|
286
|
+
const hi = 0;
|
|
287
|
+
const lo = H;
|
|
288
|
+
const mid = H / 2;
|
|
289
|
+
const brick = brickOf(level);
|
|
290
|
+
|
|
291
|
+
const svg = d.createElementNS(SVGNS, 'svg');
|
|
292
|
+
svg.setAttribute('width', W);
|
|
293
|
+
svg.setAttribute('height', H);
|
|
294
|
+
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
295
|
+
svg.setAttribute('class', 'ed-wave-level-icon');
|
|
296
|
+
|
|
297
|
+
if (level === '|') {
|
|
298
|
+
// `brickOf('|')` itself falls through to its own `'xxx'` default (it
|
|
299
|
+
// has no case for `|` — production code never asks it: `levelsOf`
|
|
300
|
+
// erases `|` before `brickOf` ever sees it, per `drawLane`'s own
|
|
301
|
+
// `gaps` comment above). So this has to be checked ahead of the
|
|
302
|
+
// `brick === 'xxx'` branch below, or the gap brush would silently
|
|
303
|
+
// draw the `x` hatch instead of its own chevron marker.
|
|
304
|
+
const gap = d.createElementNS(SVGNS, 'path');
|
|
305
|
+
gap.setAttribute('class', 'ed-wave-gap');
|
|
306
|
+
gap.setAttribute('d', gapMarkPath(W / 2, hi, lo, mid));
|
|
307
|
+
svg.appendChild(gap);
|
|
308
|
+
} else if (isBus(brick)) {
|
|
309
|
+
const s = Math.min(6, W / 4);
|
|
310
|
+
const poly = d.createElementNS(SVGNS, 'path');
|
|
311
|
+
poly.setAttribute('class', 'ed-wave-bus ed-wave-bus-' + brick.slice(4));
|
|
312
|
+
poly.setAttribute('d', 'M' + busPolygonPoints(0, W, hi, lo, mid, s).split(' ').join(' L') + ' Z');
|
|
313
|
+
svg.appendChild(poly);
|
|
314
|
+
} else if (brick === 'xxx') {
|
|
315
|
+
const rect = d.createElementNS(SVGNS, 'path');
|
|
316
|
+
rect.setAttribute('class', 'ed-wave-x');
|
|
317
|
+
rect.setAttribute('d', 'M' + 0 + ',' + hi + ' L' + W + ',' + hi +
|
|
318
|
+
' L' + W + ',' + lo + ' L' + 0 + ',' + lo + ' Z');
|
|
319
|
+
svg.appendChild(rect);
|
|
320
|
+
} else if (isClock(brick)) {
|
|
321
|
+
const step = d.createElementNS(SVGNS, 'path');
|
|
322
|
+
step.setAttribute('class', 'ed-wave-line ed-wave-clock');
|
|
323
|
+
step.setAttribute('d', clockStepPath(brick, 0, W, hi, lo));
|
|
324
|
+
svg.appendChild(step);
|
|
325
|
+
const arrowD = clockArrowPath(level, 0, W / 2, hi, lo);
|
|
326
|
+
if (arrowD !== null) {
|
|
327
|
+
const arrow = d.createElementNS(SVGNS, 'path');
|
|
328
|
+
arrow.setAttribute('class', 'ed-wave-clock-arrow');
|
|
329
|
+
arrow.setAttribute('d', arrowD);
|
|
330
|
+
svg.appendChild(arrow);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// Plain levels only: 0/1/h/l/z/u/d. `bandOf` says which single height
|
|
334
|
+
// this brick paints at; `brickPath` is only asked to draw a flat line
|
|
335
|
+
// AT that height, via whichever `yFor`-recognised char reaches it —
|
|
336
|
+
// '1' and '0' for the top/bottom bands (covers 1/h and uuu, 0/l and
|
|
337
|
+
// ddd alike), the level itself for mid (z already lands there through
|
|
338
|
+
// `yFor`'s own default, same as `bandOf`'s zzz case).
|
|
339
|
+
const band = bandOf(brick, hi, lo, mid);
|
|
340
|
+
const repLevel = band[0] === hi ? '1' : band[0] === lo ? '0' : level;
|
|
341
|
+
const line = d.createElementNS(SVGNS, 'path');
|
|
342
|
+
line.setAttribute('class', (brick === 'uuu' || brick === 'ddd')
|
|
343
|
+
? 'ed-wave-line ed-wave-weak ed-wave-level-icon-path'
|
|
344
|
+
: 'ed-wave-line ed-wave-level-icon-path');
|
|
345
|
+
line.setAttribute('d', brickPath(repLevel, repLevel, W, H));
|
|
346
|
+
svg.appendChild(line);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return svg;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The hand-drawn waveform.
|
|
354
|
+
*
|
|
355
|
+
* Every cycle's picture comes from `codec.levelsOf` through `brickOf`, and
|
|
356
|
+
* from nowhere else — constraint 3. The levels and the bricks are both
|
|
357
|
+
* published on the element (`data-levels-N`, `data-bricks-N`) so the
|
|
358
|
+
* agreement with the preview beside it is something a test can measure
|
|
359
|
+
* rather than something this comment can promise: the preview's own SVG
|
|
360
|
+
* names its bricks in its `<use xlink:href="#…">` list.
|
|
361
|
+
*
|
|
362
|
+
* v3.6.0 Task 1: this function WRITES BACK through two of its own
|
|
363
|
+
* parameters, not only through the DOM it builds — `state.cursor` and
|
|
364
|
+
* `state.selection` are clamped in place (a document that got shorter can
|
|
365
|
+
* leave either pointing past the end), and the return value is a brand new
|
|
366
|
+
* `<svg>` that replaces the one `canvas` named on the way in. The caller in
|
|
367
|
+
* wave-ui.js is expected to read `state.cursor`/`state.selection` back into
|
|
368
|
+
* its own locals and reassign its own `canvas` from the return value —
|
|
369
|
+
* see every `wds`/`canvas = drawer.renderCanvas(...)` call site there. The
|
|
370
|
+
* round trip is the price of the split: the alternative is this drawing
|
|
371
|
+
* layer holding a live reference to the dialog's own variables, which is
|
|
372
|
+
* exactly the coupling moving it into its own module is meant to remove.
|
|
373
|
+
*/
|
|
374
|
+
/**
|
|
375
|
+
* The name column, drawn on the canvas itself.
|
|
376
|
+
*
|
|
377
|
+
* The lane rail (`wave-panels.js`) already shows every name — this is
|
|
378
|
+
* deliberate duplication, not an oversight. The rail is the OPERATING
|
|
379
|
+
* surface (select, reorder, rename); the canvas is a MIRROR of what the
|
|
380
|
+
* engine's own preview renders. The engine's output has names on it, so a
|
|
381
|
+
* user comparing the hand-drawn canvas against the preview beside it
|
|
382
|
+
* should not have to align two separate columns by eye to know which row
|
|
383
|
+
* is which.
|
|
384
|
+
*
|
|
385
|
+
* v3.6.0 Task 8 (R? — see task-8-report.md): these are flat children of
|
|
386
|
+
* the canvas `<svg>`, appended in the same paint pass as every brick and
|
|
387
|
+
* edge, which is exactly what makes them a landmine for
|
|
388
|
+
* `test/editor-journey.test.js`'s T6b: its `paintedCounts` classifies a
|
|
389
|
+
* shape into a lane's row purely by the shape's vertical center (`cy`),
|
|
390
|
+
* with NO x-bound — so any shape whose `cy` falls inside a lane's row
|
|
391
|
+
* band counts toward that lane's painted-shape total, wherever it sits on
|
|
392
|
+
* the x axis. A lane-name label is drawn at exactly that lane's row
|
|
393
|
+
* center, so without an exclusion it would inflate every lane's count by
|
|
394
|
+
* one and desync `paintedCounts` from `expectedCounts` (the engine's own
|
|
395
|
+
* run count) on every fixture that has named lanes — which is all of
|
|
396
|
+
* them. `.ed-wave-lane-label` / `.ed-wave-group-label` are NOT in T6b's
|
|
397
|
+
* `shapes` exclusion list (`ed-wave-grid` / `-selection` / `-cursor` /
|
|
398
|
+
* `-gap` / `-buslabel` / `-edge`) as of this commit — T6b's filter needs
|
|
399
|
+
* to grow those two classes before that suite can pass. Per this task's
|
|
400
|
+
* brief: "Do not change T6b yourself" — flagged in task-8-report.md
|
|
401
|
+
* instead of edited here.
|
|
402
|
+
*/
|
|
403
|
+
const NAME_GAP = 10;
|
|
404
|
+
const GROUP_INDENT = 12;
|
|
405
|
+
|
|
406
|
+
function drawNames(svg, doc, layout) {
|
|
407
|
+
for (let i = 0; i < layout.lanes.length; i += 1) {
|
|
408
|
+
const row = layout.lanes[i];
|
|
409
|
+
const name = typeof row.lane.name === 'string' ? row.lane.name : '';
|
|
410
|
+
if (name === '') continue;
|
|
411
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
412
|
+
t.setAttribute('class', 'ed-wave-lane-label');
|
|
413
|
+
t.setAttribute('x', layout.nameColWidth - NAME_GAP);
|
|
414
|
+
t.setAttribute('y', row.y + row.height / 2);
|
|
415
|
+
t.setAttribute('text-anchor', 'end');
|
|
416
|
+
t.setAttribute('dominant-baseline', 'middle');
|
|
417
|
+
t.appendChild(d.createTextNode(name));
|
|
418
|
+
svg.appendChild(t);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The group titles — v3.5.0 measured against the pinned engine (and
|
|
424
|
+
* `wave-geometry.js`'s own `groupSpansOf` comment repeats it): a group
|
|
425
|
+
* title occupies horizontal space in the name column, never a row of its
|
|
426
|
+
* own. So each title here is one vertical (`writing-mode: vertical-rl`,
|
|
427
|
+
* matching the rail's own `.ed-wave-group` styling in `lib/md2doc.js`)
|
|
428
|
+
* label spanning its lanes' rows, indented from the LEFT edge of the name
|
|
429
|
+
* column by nesting depth — depth 1 (outermost) sits in the leftmost
|
|
430
|
+
* `GROUP_INDENT`-wide strip, each deeper level one strip further right,
|
|
431
|
+
* the same left-to-right order the rail's own tags use. A lane's own name
|
|
432
|
+
* always right-aligns at the same `nameColWidth - NAME_GAP` regardless of
|
|
433
|
+
* depth (see `drawNames`); Task 8's measurement (task-8-report.md) checks
|
|
434
|
+
* the longest fixture name still clears that fixed edge even at the
|
|
435
|
+
* deepest strip a group label reserves.
|
|
436
|
+
*/
|
|
437
|
+
function drawGroupLabels(svg, doc, layout) {
|
|
438
|
+
const spans = geometry.groupSpansOf(doc, layout);
|
|
439
|
+
for (let i = 0; i < spans.length; i += 1) {
|
|
440
|
+
const span = spans[i];
|
|
441
|
+
const from = layout.lanes[span.from];
|
|
442
|
+
const to = layout.lanes[span.to];
|
|
443
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
444
|
+
t.setAttribute('class', 'ed-wave-group-label');
|
|
445
|
+
t.setAttribute('x', (span.depth - 0.5) * GROUP_INDENT);
|
|
446
|
+
t.setAttribute('y', (from.y + to.y + to.height) / 2);
|
|
447
|
+
t.setAttribute('text-anchor', 'middle');
|
|
448
|
+
t.setAttribute('dominant-baseline', 'middle');
|
|
449
|
+
t.appendChild(d.createTextNode(span.title));
|
|
450
|
+
svg.appendChild(t);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The cycle ruler and the head/foot bands.
|
|
456
|
+
*
|
|
457
|
+
* v3.5.0 shipped eight WaveJSON banner fields (`head.text`/`tick`/`tock`/
|
|
458
|
+
* `every` and the four `foot.*` equivalents) that the hand-drawn canvas
|
|
459
|
+
* never painted — filling one in changed nothing a user could see without
|
|
460
|
+
* opening the preview. v3.6.0 Task 6 already made room for this (`layout`'s
|
|
461
|
+
* `originY`/`rulerHeight`/`headTextHeight`/`footHeight`); this task paints
|
|
462
|
+
* into that room.
|
|
463
|
+
*
|
|
464
|
+
* v3.6.0 Task 9 fix round 1 (ruling R29): foot gets TWO stacked bands, the
|
|
465
|
+
* same as head — `footHeight` (the tick band, adjacent to the last lane)
|
|
466
|
+
* and `footTextHeight` (the title band, stacked beyond it). Before this
|
|
467
|
+
* fix `footHeight` was the only band and `foot.text`/`foot.tick` shared
|
|
468
|
+
* one y, so a document setting both drew them on top of each other. The
|
|
469
|
+
* engine agrees a document can set both at once (`parse-config.js`'s
|
|
470
|
+
* `yf0`/`yf1`, mirroring `yh0`/`yh1` for head) — see
|
|
471
|
+
* `wave-geometry.js`'s own comment above `FOOT_TEXT_HEIGHT` for why the
|
|
472
|
+
* STRUCTURE (two bands) mirrors the engine while the pixel heights do not.
|
|
473
|
+
*
|
|
474
|
+
* `tick` labels sit ON cycle boundaries (`cycles + 1` of them); `tock`
|
|
475
|
+
* labels sit at cycle CENTRES (`cycles` of them) — MEASURED against
|
|
476
|
+
* `node_modules/wavedrom` 3.5.0's `lib/render-marks.js` `renderMarks()`:
|
|
477
|
+
* `ticktock(lane, 'head', 'tick', 0, mmstep, -5, marks + 1)` vs
|
|
478
|
+
* `ticktock(lane, 'head', 'tock', mmstep / 2, mmstep, -5, marks)` — the
|
|
479
|
+
* `mmstep / 2` starting x and the `marks` (not `marks + 1`) length are the
|
|
480
|
+
* engine's own distinction, not one invented here.
|
|
481
|
+
*
|
|
482
|
+
* `every` is "every N cycles" — but MEASURED against that same
|
|
483
|
+
* `ticktock()`, the engine's own filter is
|
|
484
|
+
* `(i + offset) % cxt[ref1].every != 0`, anchored to the SAME base the
|
|
485
|
+
* label values are built from (`offset = Number(head.tick)`, etc), not a
|
|
486
|
+
* bare `i % every`. Live-rendered check (dumped, not recalled):
|
|
487
|
+
* `{signal:[{name:'a',wave:'0101'}],head:{tick:3,every:2}}` keeps exactly
|
|
488
|
+
* two ticks, `"4"` and `"6"` (`i=1,3`; `(1+3)%2===0`) — `i=0,2,4` all drop
|
|
489
|
+
* (`(0+3)%2===1`). A bare `c % every` would instead keep `i=0,2,4`
|
|
490
|
+
* (values 3/5/7) — three ticks, not two, and the wrong ones. Pinned by
|
|
491
|
+
* `test/wave-draw.test.js`'s "every 錨在 (index+base)" case.
|
|
492
|
+
*
|
|
493
|
+
* Read-only with respect to the document: an absent field means no mark,
|
|
494
|
+
* full stop. `setBannerField` (wave-codec.js) is the only place that
|
|
495
|
+
* writes these keys, and it already deletes the key rather than writing
|
|
496
|
+
* `''` — the engine's own `captext` uses a truthy test while its
|
|
497
|
+
* `ticktock` uses `=== undefined`, so a stray `''` here would make the
|
|
498
|
+
* latter think the field was "set, draw the default ruler". This layer
|
|
499
|
+
* must not "fix" that by normalizing anything itself.
|
|
500
|
+
*
|
|
501
|
+
* v3.6.0 Task 9 fix round 2 (ruling R30): the tick/tock boundary count and
|
|
502
|
+
* pitch come from `diagramCycleCount`/`diagramPitch` below — the DIAGRAM's
|
|
503
|
+
* own extent — never from a lane's own `cycles`/`cycleWidth`/`originX`.
|
|
504
|
+
* See those two functions' own comment for why: `layout.cycles` counts
|
|
505
|
+
* CELLS, not diagram cycles, and a lane's own `cycleWidth`/`originX` are
|
|
506
|
+
* deliberately distorted by THAT lane's `period`/`phase`/`hscale` (Task 4)
|
|
507
|
+
* — right for painting that one lane's bricks, wrong for an overlay drawn
|
|
508
|
+
* once for the whole canvas.
|
|
509
|
+
*/
|
|
510
|
+
function drawRuler(svg, doc, layout) {
|
|
511
|
+
drawBand(svg, doc, layout, 'head');
|
|
512
|
+
drawBand(svg, doc, layout, 'foot');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The diagram-wide cycle pitch (px per diagram cycle) and how many
|
|
517
|
+
* boundaries that pitch divides `layout.width` into — shared by the ruler
|
|
518
|
+
* (`drawBand`, below) and the cycle grid (`renderCanvas`'s own "cycle
|
|
519
|
+
* grid" section), so there is exactly one place that computes it rather
|
|
520
|
+
* than two that can drift apart.
|
|
521
|
+
*
|
|
522
|
+
* MEASURED (`period: 4`, `head:{tick:0}`, against `node_modules/wavedrom`
|
|
523
|
+
* 3.5.0): the engine draws 17 ticks, values 0..16, 40px apart, on a 700px
|
|
524
|
+
* canvas — not `layout.cycles + 1` (5), which is what a lane's own CELL
|
|
525
|
+
* count gives. `layout.cycles` is `codec.levelsOf(wave).length`, i.e. a
|
|
526
|
+
* count of CELLS, and with `period: 4` one cell IS four diagram cycles —
|
|
527
|
+
* the same failure family this repo's CLAUDE.md already names ("node
|
|
528
|
+
* string indices are CELLS, while the drawing thinks in CYCLES"),
|
|
529
|
+
* invisible before Task 4 because cells and diagram cycles were the same
|
|
530
|
+
* number for every period-1 document. A per-lane `row.cycleWidth`/
|
|
531
|
+
* `row.originX` does not fix it either (it is a PITCH bug, not just a
|
|
532
|
+
* COUNT bug) — those already carry that same lane's `period`, so reading
|
|
533
|
+
* lane 0's alone is right for lane 0's own bricks and wrong for a ruler
|
|
534
|
+
* drawn once, diagram-wide.
|
|
535
|
+
*
|
|
536
|
+
* Derived from `layout.width` — never a second cycle-count field of its
|
|
537
|
+
* own — so the ruler is structurally incapable of disagreeing with the
|
|
538
|
+
* canvas's own extent: `(width - nameColWidth) / (cycleWidth * hscale)`.
|
|
539
|
+
* Checked against the engine on two fixtures before landing:
|
|
540
|
+
* `period: 4` → (888 − 120) / 48 + 1 = 17 ticks (engine: 17)
|
|
541
|
+
* `hscale: 2, period: 1` → (504 − 120) / 96 + 1 = 5 ticks (engine: 5)
|
|
542
|
+
* `hscale` scales the PITCH (the engine's own `mmstep = 2 * hscale * xs`),
|
|
543
|
+
* not the count, which is why it multiplies `cycleWidth` here rather than
|
|
544
|
+
* dividing the span by it. `Math.round` absorbs the float noise a
|
|
545
|
+
* fractional `laneHeight`/`cycleWidth`/`nameColWidth` size leaves in
|
|
546
|
+
* `layout.width` (each lane's own `cycleWidth`/`originX` is one
|
|
547
|
+
* multiplication away from exact) — the true count is structurally an
|
|
548
|
+
* integer (`layoutOf` only ever widens `width` by whole `period`-scaled
|
|
549
|
+
* cells), so rounding cannot paper over a real off-by-one the way it
|
|
550
|
+
* would for a value with no reason to land on an integer.
|
|
551
|
+
*/
|
|
552
|
+
function diagramPitch(layout) {
|
|
553
|
+
return layout.cycleWidth * layout.hscale;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function diagramCycleCount(layout) {
|
|
557
|
+
const pitch = diagramPitch(layout);
|
|
558
|
+
if (!(pitch > 0)) return 0;
|
|
559
|
+
const span = layout.width - layout.nameColWidth;
|
|
560
|
+
if (!(span > 0)) return 0;
|
|
561
|
+
return Math.round(span / pitch);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function drawBand(svg, doc, layout, which) {
|
|
565
|
+
const b = (doc !== null && typeof doc === 'object' &&
|
|
566
|
+
doc[which] !== null && typeof doc[which] === 'object') ? doc[which] : null;
|
|
567
|
+
if (b === null) return;
|
|
568
|
+
const isHead = which === 'head';
|
|
569
|
+
// Head: title band (0..headTextHeight) stacked ABOVE the ruler band
|
|
570
|
+
// (headTextHeight..originY). Foot: tick band (originY+lanes*laneHeight
|
|
571
|
+
// .. that + footHeight) stacked BELOW the title band beyond it (R29) —
|
|
572
|
+
// the mirror image, title band on the outer edge, tick band against the
|
|
573
|
+
// lanes, matching the engine's own captext y (further from the lanes
|
|
574
|
+
// than its ticktock y, both for head and for foot).
|
|
575
|
+
const textY = isHead
|
|
576
|
+
? layout.headTextHeight / 2
|
|
577
|
+
: layout.height - layout.footTextHeight / 2;
|
|
578
|
+
const scaleY = isHead
|
|
579
|
+
? layout.headTextHeight + layout.rulerHeight / 2
|
|
580
|
+
: layout.height - layout.footTextHeight - layout.footHeight / 2;
|
|
581
|
+
|
|
582
|
+
if (typeof b.text === 'string' && b.text !== '') {
|
|
583
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
584
|
+
t.setAttribute('class', isHead ? 'ed-wave-head-text' : 'ed-wave-foot-text');
|
|
585
|
+
t.setAttribute('x', layout.width / 2);
|
|
586
|
+
t.setAttribute('y', textY);
|
|
587
|
+
t.setAttribute('text-anchor', 'middle');
|
|
588
|
+
t.appendChild(d.createTextNode(b.text));
|
|
589
|
+
svg.appendChild(t);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const every = (typeof b.every === 'number' && isFinite(b.every) && b.every > 0)
|
|
593
|
+
? Math.floor(b.every) : 1;
|
|
594
|
+
// No lanes, no diagram to rule off of — same "draw nothing" this branch
|
|
595
|
+
// already had before round 2 (the guard used to be `row === null`, a
|
|
596
|
+
// per-lane check; the diagram-wide replacement is `layout.lanes.length`
|
|
597
|
+
// itself, since `diagramCycleCount` has no lane to read at all here).
|
|
598
|
+
if (layout.lanes.length === 0) return;
|
|
599
|
+
const pitch = diagramPitch(layout);
|
|
600
|
+
const boundaries = diagramCycleCount(layout);
|
|
601
|
+
|
|
602
|
+
if (b.tick !== undefined) {
|
|
603
|
+
const base = Number(b.tick);
|
|
604
|
+
for (let c = 0; c <= boundaries; c += 1) {
|
|
605
|
+
if ((c + base) % every !== 0) continue;
|
|
606
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
607
|
+
t.setAttribute('class', 'ed-wave-ruler-tick');
|
|
608
|
+
t.setAttribute('x', layout.nameColWidth + c * pitch);
|
|
609
|
+
t.setAttribute('y', scaleY);
|
|
610
|
+
t.setAttribute('text-anchor', 'middle');
|
|
611
|
+
t.appendChild(d.createTextNode(String(base + c)));
|
|
612
|
+
svg.appendChild(t);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (b.tock !== undefined) {
|
|
616
|
+
const base = Number(b.tock);
|
|
617
|
+
for (let c = 0; c < boundaries; c += 1) {
|
|
618
|
+
if ((c + base) % every !== 0) continue;
|
|
619
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
620
|
+
t.setAttribute('class', 'ed-wave-ruler-tock');
|
|
621
|
+
t.setAttribute('x', layout.nameColWidth + (c + 0.5) * pitch);
|
|
622
|
+
t.setAttribute('y', scaleY);
|
|
623
|
+
t.setAttribute('text-anchor', 'middle');
|
|
624
|
+
t.appendChild(d.createTextNode(String(base + c)));
|
|
625
|
+
svg.appendChild(t);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function renderCanvas(canvas, doc, layout, state) {
|
|
631
|
+
// v3.5.0 Task 11: the data-label field's node sits beside the canvas
|
|
632
|
+
// SVG inside `canvasWrap` (see `openDataEdit`), and the wipe two lines
|
|
633
|
+
// below (`canvasWrap.textContent = ''`) would tear it out from under
|
|
634
|
+
// the user on the very next cursor move if this did not retire it
|
|
635
|
+
// first — every path that reaches this function counts, not only
|
|
636
|
+
// `render()`, which is why this lives HERE and not only next to
|
|
637
|
+
// `groupRename.retire()` in `render()` itself.
|
|
638
|
+
if (state.dataEdit !== null) state.dataEdit.retire();
|
|
639
|
+
// Every repaint REPLACES the canvas element, so a canvas that had the
|
|
640
|
+
// keyboard loses it to `document.body` the instant it is detached —
|
|
641
|
+
// exactly the state F7 and T6h exist to prevent, and here it would fire
|
|
642
|
+
// on every arrow key. `render()`'s own `data-focus-key` restore covers
|
|
643
|
+
// the full-repaint path; this covers the three that call renderCanvas
|
|
644
|
+
// directly (a drag's two, and the keyboard's own), so the property holds
|
|
645
|
+
// for the canvas no matter which road the repaint came down.
|
|
646
|
+
const hadFocus = canvas !== null && d.activeElement === canvas;
|
|
647
|
+
// The cursor is an index pair, and the document under it can get shorter:
|
|
648
|
+
// deleting cycles or removing the bottom lane leaves it pointing past the
|
|
649
|
+
// end. Clamped HERE, where the layout that decides what "past the end"
|
|
650
|
+
// means is already in hand — so nothing downstream ever has to ask.
|
|
651
|
+
if (state.cursor !== null) {
|
|
652
|
+
if (layout.lanes.length === 0 || layout.cycles === 0) {
|
|
653
|
+
state.cursor = null;
|
|
654
|
+
} else {
|
|
655
|
+
state.cursor = {
|
|
656
|
+
laneIndex: Math.max(0, Math.min(state.cursor.laneIndex, layout.lanes.length - 1)),
|
|
657
|
+
cycle: Math.max(0, Math.min(state.cursor.cycle, layout.cycles - 1)),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// …and `selection` with it, which is the other half of the same fact and
|
|
662
|
+
// was left out of the first cut. Measured before this: Shift-select four
|
|
663
|
+
// cycles, press 刪除 cycle, and a one-cycle document was left with the
|
|
664
|
+
// cursor on cycle 1, no visible box, 複製 reporting「複製了 4 個 cycle」
|
|
665
|
+
// and 貼上(插入)growing the document back to five. The toolbar was
|
|
666
|
+
// acting on a range the drawing no longer had anywhere to put.
|
|
667
|
+
//
|
|
668
|
+
// The two axes are not treated alike, and that is the point: a CYCLE
|
|
669
|
+
// range that overshoots is the same range in a shorter document, so it
|
|
670
|
+
// clamps; a LANE that is gone is not a lane the user can have meant, so
|
|
671
|
+
// the selection goes rather than silently moving to whichever lane
|
|
672
|
+
// inherited the index. (`lane ✕` already nulls it on its own path; this
|
|
673
|
+
// catches the undo/redo of that.)
|
|
674
|
+
//
|
|
675
|
+
// It does NOT catch every route, and the earlier wording here said it
|
|
676
|
+
// did. MEASURED: `+` at 0, drag-select cycles 1-3 on the new lane 0,
|
|
677
|
+
// then Ctrl+Z — the selection box and the rail highlight both stay on
|
|
678
|
+
// row 0, which is now `clk`. The trace-derived marks above fix the
|
|
679
|
+
// CURSOR for that case; the selection is still carried by row number,
|
|
680
|
+
// so it lands on whoever inherited the row. The cost is confined to the
|
|
681
|
+
// highlight and to where the next Shift+arrow anchors, because every
|
|
682
|
+
// cycle operation is document-wide — but it is a real gap, not a
|
|
683
|
+
// covered one.
|
|
684
|
+
if (state.selection !== null) {
|
|
685
|
+
if (layout.lanes.length === 0 || layout.cycles === 0 ||
|
|
686
|
+
state.selection.laneIndex < 0 || state.selection.laneIndex >= layout.lanes.length) {
|
|
687
|
+
state.selection = null;
|
|
688
|
+
} else {
|
|
689
|
+
// v3.6.0 Task 14: `laneTo` — the far end of the lane range — is
|
|
690
|
+
// rebuilt here, not merely copied. This function REPLACES the
|
|
691
|
+
// caller's selection object (`wave-ui.js` reads it back out of the
|
|
692
|
+
// state bag afterwards), so a key left out here is a key deleted
|
|
693
|
+
// from the live selection, and `laneRow`'s `i <= laneTo` test would
|
|
694
|
+
// then be comparing against `undefined` for every row — no row
|
|
695
|
+
// highlighted, and 建立群組 disabled, after any repaint at all.
|
|
696
|
+
// Clamped into the shortened document the same way `laneIndex` is
|
|
697
|
+
// checked above, and never allowed below `laneIndex`, so the
|
|
698
|
+
// normalised invariant the whole file relies on survives a delete.
|
|
699
|
+
const lastLane = layout.lanes.length - 1;
|
|
700
|
+
const far = typeof state.selection.laneTo === 'number'
|
|
701
|
+
? Math.min(state.selection.laneTo, lastLane) : state.selection.laneIndex;
|
|
702
|
+
state.selection = {
|
|
703
|
+
laneIndex: state.selection.laneIndex,
|
|
704
|
+
laneTo: Math.max(state.selection.laneIndex, far),
|
|
705
|
+
from: Math.max(0, Math.min(state.selection.from, layout.cycles - 1)),
|
|
706
|
+
to: Math.max(0, Math.min(state.selection.to, layout.cycles - 1)),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
state.canvasWrap.textContent = '';
|
|
711
|
+
canvas = d.createElementNS(SVGNS, 'svg');
|
|
712
|
+
canvas.setAttribute('class', 'ed-wave-canvas');
|
|
713
|
+
// In the Tab cycle, and therefore in the focus trap, like every other
|
|
714
|
+
// control in here: the drawing is this dialog's main surface and was the
|
|
715
|
+
// one thing in it a keyboard could not reach.
|
|
716
|
+
canvas.setAttribute('tabindex', '0');
|
|
717
|
+
canvas.setAttribute('data-focus-key', 'canvas');
|
|
718
|
+
// This label is the one keyboard manual a screen-reader user ever gets
|
|
719
|
+
// for this surface, so every claim in it has to be true in every state.
|
|
720
|
+
//
|
|
721
|
+
// v3.6.0 Task 16 fix round 3 REPLACES what round 1 put here. Round 1 had
|
|
722
|
+
// to say "while the 關聯線 gesture is armed, ← / → stop walking one cell
|
|
723
|
+
// and Shift stops extending", because that is what round 0's binding did.
|
|
724
|
+
// Round 3 took the jump off the unmodified arrows (see
|
|
725
|
+
// `handleDrawingKey`'s own comment and T9b), so the first two clauses of
|
|
726
|
+
// this label are unconditionally true again and the mode-dependent
|
|
727
|
+
// sentence is gone with the mode-dependence. What remains is an EXTRA
|
|
728
|
+
// key, Alt+←/→, which is named because a key nobody can discover is a
|
|
729
|
+
// key that does not exist for a keyboard user. Alt+↑/↓ is still
|
|
730
|
+
// deliberately absent: it moves a LANE, is dialog-wide rather than a
|
|
731
|
+
// drawing key, and is named in the opening status line instead.
|
|
732
|
+
canvas.setAttribute('aria-label',
|
|
733
|
+
'波形繪圖區:方向鍵移動游標,Shift+左右鍵選一段 cycle,' +
|
|
734
|
+
'按電位字元(' + BRUSHES.join(' ') + ')塗上去;' +
|
|
735
|
+
'武裝關聯線之後,Alt+左右鍵可以在轉態點之間跳');
|
|
736
|
+
const width = Math.max(layout.width, SIZES.cycleWidth);
|
|
737
|
+
canvas.setAttribute('width', String(width));
|
|
738
|
+
canvas.setAttribute('height', String(Math.max(layout.height, layout.laneHeight)));
|
|
739
|
+
canvas.setAttribute('viewBox', '0 0 ' + width + ' ' +
|
|
740
|
+
Math.max(layout.height, layout.laneHeight));
|
|
741
|
+
canvas.setAttribute('data-lane-count', String(layout.lanes.length));
|
|
742
|
+
canvas.setAttribute('data-cycle-count', String(layout.cycles));
|
|
743
|
+
// v3.6.0 Task 6 (ruling R18): the geometry the layout actually used, so a
|
|
744
|
+
// caller measuring the rendered SVG (a puppeteer test, in particular)
|
|
745
|
+
// reads these rather than re-deriving lane pitch / cycle pitch by
|
|
746
|
+
// dividing `getBoundingClientRect()` by a count — that division silently
|
|
747
|
+
// breaks the moment the canvas carries a ruler band or a name column,
|
|
748
|
+
// because the SVG's total width/height then includes space no lane or
|
|
749
|
+
// cycle occupies. Written straight from `layout`, never recomputed here.
|
|
750
|
+
// `originX`/`cycleWidth` are lane 0's own (Task 4 made both per-lane via
|
|
751
|
+
// `period`/`phase`/`hscale`); a document with no lanes falls back to the
|
|
752
|
+
// layout-level `nameColWidth`/`cycleWidth` so the attributes are still
|
|
753
|
+
// present (and still correct — an empty canvas has no cell to disagree).
|
|
754
|
+
const originRow = layout.lanes.length > 0 ? layout.lanes[0] : null;
|
|
755
|
+
canvas.setAttribute('data-origin-x', String(originRow !== null ? originRow.originX : layout.nameColWidth));
|
|
756
|
+
canvas.setAttribute('data-origin-y', String(layout.originY));
|
|
757
|
+
canvas.setAttribute('data-lane-height', String(layout.laneHeight));
|
|
758
|
+
canvas.setAttribute('data-cycle-width', String(originRow !== null ? originRow.cycleWidth : layout.cycleWidth));
|
|
759
|
+
|
|
760
|
+
// cycle grid
|
|
761
|
+
// v3.6.0 Task 6: shifted by `layout.nameColWidth` — this grid is drawn
|
|
762
|
+
// once for the whole canvas at the diagram's default (phase 0) cycle
|
|
763
|
+
// pitch, the same base `layout.cycleWidth` every lane's own `cycleWidth`
|
|
764
|
+
// is derived from, so `nameColWidth` (not a per-lane `originX`) is the
|
|
765
|
+
// matching x origin for it. Left at x=0 this grid would sit under the
|
|
766
|
+
// name column instead of under the cells it is meant to mark — the
|
|
767
|
+
// "make room" the geometry layer already does for hit-testing has to be
|
|
768
|
+
// visible too, or a click on a drawn cell lands in what `cellAt` now
|
|
769
|
+
// calls the name column and does nothing.
|
|
770
|
+
//
|
|
771
|
+
// v3.6.0 Task 9 fix round 2 (R30): the boundary count and pitch now come
|
|
772
|
+
// from `diagramCycleCount`/`diagramPitch` (see their own comment above
|
|
773
|
+
// `drawBand`) instead of `layout.cycles`/`layout.cycleWidth` directly —
|
|
774
|
+
// `layout.cycles` is a CELL count and `layout.cycleWidth` carries no
|
|
775
|
+
// `hscale`, so under `period` or `config.hscale` this grid used to sit
|
|
776
|
+
// at the wrong pitch (and the wrong count, under `period`) while the
|
|
777
|
+
// bricks it is meant to mark painted at the per-lane, `hscale`-scaled
|
|
778
|
+
// one. The engine's own grid (`render-marks.js`'s `gmarkLines`) is the
|
|
779
|
+
// same `marks + 1` diagram-cycle boundary count the ruler uses, not a
|
|
780
|
+
// second definition — sharing `diagramCycleCount` here is not a
|
|
781
|
+
// convenience, it is the two DOM elements agreeing with the one thing
|
|
782
|
+
// the engine agrees with itself.
|
|
783
|
+
const gridPitch = diagramPitch(layout);
|
|
784
|
+
const gridBoundaries = diagramCycleCount(layout);
|
|
785
|
+
for (let c = 0; c <= gridBoundaries; c++) {
|
|
786
|
+
const g = d.createElementNS(SVGNS, 'line');
|
|
787
|
+
g.setAttribute('class', 'ed-wave-grid');
|
|
788
|
+
g.setAttribute('x1', String(layout.nameColWidth + c * gridPitch));
|
|
789
|
+
g.setAttribute('x2', String(layout.nameColWidth + c * gridPitch));
|
|
790
|
+
// v3.6.0 Task 6 fix round 1: `layout.originY` (not `'0'`), bounded to
|
|
791
|
+
// the lane rows' own extent (not `layout.height`, which also carries
|
|
792
|
+
// `footHeight`) — same reasoning as the x-axis fix just above, missed
|
|
793
|
+
// in the first pass. Left at `'0'`/`layout.height` this line bled into
|
|
794
|
+
// the ruler band above lane 0 and the foot band below the last lane;
|
|
795
|
+
// `test/editor-journey.test.js`'s T6b measures this exact line's
|
|
796
|
+
// `y1`/`y2` to find the row band, so a grid starting at 0 made T6b's
|
|
797
|
+
// own row-height arithmetic wrong too (R19, same fix round).
|
|
798
|
+
g.setAttribute('y1', String(layout.originY));
|
|
799
|
+
g.setAttribute('y2', String(layout.originY + layout.lanes.length * layout.laneHeight));
|
|
800
|
+
canvas.appendChild(g);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// v3.6.0 Task 9: the ruler/head/foot bands sit strictly above `originY`
|
|
804
|
+
// and strictly below the last lane's row (see `drawRuler`'s own
|
|
805
|
+
// comment), so paint order against the name column and the lanes below
|
|
806
|
+
// does not matter — drawn first only because it is the topmost band on
|
|
807
|
+
// screen.
|
|
808
|
+
drawRuler(canvas, doc, layout);
|
|
809
|
+
|
|
810
|
+
// v3.6.0 Task 8: names before lanes, so the bricks paint OVER the name
|
|
811
|
+
// column's right edge in the unlikely case a lane's own cell art ever
|
|
812
|
+
// reached that far left — not that it should, but paint order should
|
|
813
|
+
// not be the only thing preventing it.
|
|
814
|
+
drawNames(canvas, doc, layout);
|
|
815
|
+
drawGroupLabels(canvas, doc, layout);
|
|
816
|
+
|
|
817
|
+
for (let i = 0; i < layout.lanes.length; i++) {
|
|
818
|
+
drawLane(canvas, layout, i);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const edges = renderEdges(canvas, doc, layout, state);
|
|
822
|
+
|
|
823
|
+
// v3.6.0 Task 11: 轉態標記——只在 `edgeMode !== 'idle'` 時畫。平常畫面
|
|
824
|
+
// 不該多這一排點,而且它同時是「你現在在連線模式」這個狀態的唯一視覺
|
|
825
|
+
// 證據——在這之前只有工具列那顆按鈕變亮,畫布上看不出來。
|
|
826
|
+
//
|
|
827
|
+
// 落在 `geometry.anchorOfCell`,不是格子中心:這批標記的整個存在理由
|
|
828
|
+
// 是讓使用者瞄準跟畫線用的同一個點,一個畫在格中心的標記會教使用者瞄
|
|
829
|
+
// 準錯的地方,比完全不畫還糟。
|
|
830
|
+
//
|
|
831
|
+
// 資料來自 `geometry.transitionsOf`,不是「這條 lane 的每一格」:它只
|
|
832
|
+
// 問 `codec.expandWave` 已展開的真實資料格,`reachOf` 允許一條 lane 的
|
|
833
|
+
// 可觸及範圍遠超過它自己的 `cycles`(畫過去是延伸 lane 的手段,見
|
|
834
|
+
// `reachOf`/`cellAt` 自己的註解),但那些延伸格沒有資料、也就沒有轉
|
|
835
|
+
// 態——`transitionsOf` 天生不會把它們算進來,這裡不需要另外擋。
|
|
836
|
+
drawTransitions(canvas, doc, layout, state);
|
|
837
|
+
|
|
838
|
+
// v3.5.0 Task 8: the in-flight endpoint drag's own preview — UI state
|
|
839
|
+
// only, never a document read (rule 3: nothing is written until
|
|
840
|
+
// `finishEndpointDrag` commits it, and Escape mid-drag never reaches
|
|
841
|
+
// that far). `renderEdges` above already faded the dragged edge's OWN,
|
|
842
|
+
// still-current path (`.is-drag-source`, read from this same
|
|
843
|
+
// `endpointDrag`); this draws the other half — a dashed line from the
|
|
844
|
+
// edge's UNMOVED end to wherever the drag is currently over. Reuses
|
|
845
|
+
// `.ed-wave-edge-pending`, the exact dashed style Task 7's own
|
|
846
|
+
// not-yet-written preview line already uses, for the identical
|
|
847
|
+
// reason: this line is not `doc.edge` either, until the drop.
|
|
848
|
+
//
|
|
849
|
+
// Looked up in `edges` — the SAME array `renderEdges` just built this
|
|
850
|
+
// pass, not a second call to `geometry.edgeLayout` — so the two can
|
|
851
|
+
// never draw from different snapshots of the layout. `render()`'s own
|
|
852
|
+
// clamp guarantees `endpointDrag.index === selectedEdge` whenever
|
|
853
|
+
// `endpointDrag` is non-null, so a lookup miss here would mean that
|
|
854
|
+
// invariant broke, not that the edge is merely unselected — the `!==
|
|
855
|
+
// undefined` check is defensive rather than expected to matter.
|
|
856
|
+
let endpointDragMark = '';
|
|
857
|
+
if (state.endpointDrag !== null) {
|
|
858
|
+
const dragged = edges.filter(function (e) { return e.index === state.endpointDrag.index; })[0];
|
|
859
|
+
if (dragged !== undefined) {
|
|
860
|
+
const otherEnd = state.endpointDrag.end === 'from' ? 'to' : 'from';
|
|
861
|
+
const from = dragged[otherEnd];
|
|
862
|
+
// Falls back to the dragged end's OWN current position — the
|
|
863
|
+
// degenerate "no visible change yet" line — until a `pointermove`
|
|
864
|
+
// has actually landed on a real cell (`endpointDrag.at !== null`).
|
|
865
|
+
let to = dragged[state.endpointDrag.end];
|
|
866
|
+
if (state.endpointDrag.at !== null) {
|
|
867
|
+
const cell = geometry.cellRect(layout, state.endpointDrag.at, state.endpointDrag.cell);
|
|
868
|
+
if (cell !== null) {
|
|
869
|
+
to = { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 };
|
|
870
|
+
endpointDragMark = state.endpointDrag.index + ':' + state.endpointDrag.end + '@' +
|
|
871
|
+
state.endpointDrag.at + ',' + state.endpointDrag.cell;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
875
|
+
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
876
|
+
line.setAttribute('x1', String(from.x));
|
|
877
|
+
line.setAttribute('y1', String(from.y));
|
|
878
|
+
line.setAttribute('x2', String(to.x));
|
|
879
|
+
line.setAttribute('y2', String(to.y));
|
|
880
|
+
canvas.appendChild(line);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
state.overlay.setAttribute('data-wave-endpoint-drag', endpointDragMark);
|
|
884
|
+
|
|
885
|
+
if (state.selection !== null) {
|
|
886
|
+
// v3.6.0 Task 6: this lane's OWN row (`originX`/`y`/`cycleWidth`), not
|
|
887
|
+
// `layout.cycleWidth`/`i * layout.laneHeight` from x=0/y=0 — the
|
|
888
|
+
// selection box has to land on the same cell `cellRect` (and the
|
|
889
|
+
// cursor box below, which already asks `cellRect`) says it is, and
|
|
890
|
+
// `state.selection.laneIndex` is guaranteed a valid index into
|
|
891
|
+
// `layout.lanes` by the clamp above this function.
|
|
892
|
+
const selRow = layout.lanes[state.selection.laneIndex];
|
|
893
|
+
const from = Math.min(state.selection.from, state.selection.to);
|
|
894
|
+
const to = Math.max(state.selection.from, state.selection.to);
|
|
895
|
+
const box = d.createElementNS(SVGNS, 'rect');
|
|
896
|
+
box.setAttribute('class', 'ed-wave-selection');
|
|
897
|
+
box.setAttribute('x', String(selRow.originX + from * selRow.cycleWidth));
|
|
898
|
+
box.setAttribute('y', String(selRow.y));
|
|
899
|
+
box.setAttribute('width', String((to - from + 1) * selRow.cycleWidth));
|
|
900
|
+
box.setAttribute('height', String(selRow.height));
|
|
901
|
+
canvas.appendChild(box);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// v3.5.0 Task 7: the pending edge — `pendingFrom` to wherever `cursor`
|
|
905
|
+
// is now — drawn from UI state alone, never from the document (rule 2:
|
|
906
|
+
// nothing about the doc changes until the drop). `cursor` is shared
|
|
907
|
+
// with the keyboard cell below it in paint order for the same reason
|
|
908
|
+
// the two are drawn from the same variable: a mouse drag and the
|
|
909
|
+
// keyboard's two-Enter version are one gesture wearing two input
|
|
910
|
+
// devices, so there is exactly one line to preview, however it is
|
|
911
|
+
// being aimed at this instant. Skipped once `pendingFrom` is null,
|
|
912
|
+
// which is every state except "armed with a marked start" and
|
|
913
|
+
// "dragging" — see `setEdgeMode`.
|
|
914
|
+
let pendingCell = '';
|
|
915
|
+
if (state.pendingFrom !== null && state.cursor !== null) {
|
|
916
|
+
const from = geometry.cellRect(layout, state.pendingFrom.at, state.pendingFrom.cell);
|
|
917
|
+
const to = geometry.cellRect(layout, state.cursor.laneIndex, state.cursor.cycle);
|
|
918
|
+
if (from !== null && to !== null) {
|
|
919
|
+
pendingCell = state.pendingFrom.at + ',' + state.pendingFrom.cell;
|
|
920
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
921
|
+
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
922
|
+
line.setAttribute('x1', String(from.x + from.width / 2));
|
|
923
|
+
line.setAttribute('y1', String(from.y + from.height / 2));
|
|
924
|
+
line.setAttribute('x2', String(to.x + to.width / 2));
|
|
925
|
+
line.setAttribute('y2', String(to.y + to.height / 2));
|
|
926
|
+
canvas.appendChild(line);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
state.overlay.setAttribute('data-wave-edge-pending', pendingCell);
|
|
930
|
+
|
|
931
|
+
// The keyboard's cell, drawn last so it is over the selection tint and
|
|
932
|
+
// over the trace. Its geometry comes from `wave-geometry.cellRect` — the
|
|
933
|
+
// inverse of the hit test the pointer uses — so the cell the cursor is
|
|
934
|
+
// DRAWN on and the cell an edit lands in cannot drift apart.
|
|
935
|
+
let cursorCell = '';
|
|
936
|
+
if (state.cursor !== null) {
|
|
937
|
+
const cell = geometry.cellRect(layout, state.cursor.laneIndex, state.cursor.cycle);
|
|
938
|
+
if (cell !== null) {
|
|
939
|
+
cursorCell = state.cursor.laneIndex + ',' + state.cursor.cycle;
|
|
940
|
+
const cur = d.createElementNS(SVGNS, 'rect');
|
|
941
|
+
cur.setAttribute('class', 'ed-wave-cursor');
|
|
942
|
+
cur.setAttribute('data-ed-wave-cursor', '');
|
|
943
|
+
cur.setAttribute('data-cell', cursorCell);
|
|
944
|
+
cur.setAttribute('x', String(cell.x));
|
|
945
|
+
cur.setAttribute('y', String(cell.y));
|
|
946
|
+
cur.setAttribute('width', String(cell.width));
|
|
947
|
+
cur.setAttribute('height', String(cell.height));
|
|
948
|
+
canvas.appendChild(cur);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// The same value on the overlay: what a scenario reads when the rect is
|
|
952
|
+
// not the thing under test. It is assigned INSIDE the branch that draws
|
|
953
|
+
// the rect and defaults to empty, so "there is no rect" and "the overlay
|
|
954
|
+
// says none" are one decision rather than two that happen to agree —
|
|
955
|
+
// they did not, in the first cut: this line read `cursor` directly, so a
|
|
956
|
+
// cursor the clamp had not caught published a cell that `cellRect`
|
|
957
|
+
// refused to give a rectangle to, and a build with the clamp removed
|
|
958
|
+
// reported `{cell: null, published: "5,4"}`.
|
|
959
|
+
state.overlay.setAttribute('data-wave-cursor', cursorCell);
|
|
960
|
+
|
|
961
|
+
canvas.addEventListener('mousedown', state.onCanvasDown);
|
|
962
|
+
// v3.6.0 Task 11: hover, unlike the document-level drag pair
|
|
963
|
+
// `wave-ui.js` adds/removes around `onCanvasDown`/`onCanvasUp`, has to
|
|
964
|
+
// be live from the moment the canvas exists — a plain hover with no
|
|
965
|
+
// button down never reaches a mousedown handler at all. Optional
|
|
966
|
+
// (`state.onCanvasHover` may be absent, e.g. `test/wave-draw.test.js`'s
|
|
967
|
+
// `fakeCanvasState()`, which never exercises pointer movement): guarded
|
|
968
|
+
// the same way `state.dataEdit`/`state.cursor` above tolerate a minimal
|
|
969
|
+
// test fixture, rather than requiring every caller to supply a no-op.
|
|
970
|
+
if (state.onCanvasHover) canvas.addEventListener('mousemove', state.onCanvasHover);
|
|
971
|
+
// Tabbing onto the drawing is an entry like any other, so it puts the
|
|
972
|
+
// cursor on screen rather than leaving a focused surface with no visible
|
|
973
|
+
// cursor in it. Re-entrant only once: `enterDrawing()` repaints, the
|
|
974
|
+
// repaint re-focuses the new canvas and fires this again, and by then
|
|
975
|
+
// `cursor` is set.
|
|
976
|
+
canvas.addEventListener('focus', function () {
|
|
977
|
+
if (state.cursor === null) state.enterDrawing();
|
|
978
|
+
});
|
|
979
|
+
state.canvasWrap.appendChild(canvas);
|
|
980
|
+
if (hadFocus) canvas.focus();
|
|
981
|
+
return canvas;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** 轉態標記的半徑:一般 / 游標懸停在上面那一顆。 */
|
|
985
|
+
const TRANSITION_R = 3;
|
|
986
|
+
const TRANSITION_R_HOT = 5;
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 轉態標記——使用者瞄準 edge 端點的那些點,畫在 `geometry.anchorOfCell`
|
|
990
|
+
* 上,不是格中心(見上方 `renderCanvas` 呼叫處的註解)。
|
|
991
|
+
*
|
|
992
|
+
* `state.hoverBoundary` 是 `wave-ui.js` 的 `boundaryAt` 原生形狀
|
|
993
|
+
* `{laneIndex, cell}`(不是 `cellFromEvent` 對外一致改叫的 `cycle`——
|
|
994
|
+
* 這裡直接比對 `cell`,兩處欄位名故意不同,混用會在這裡悄悄比對失敗)。
|
|
995
|
+
*/
|
|
996
|
+
function drawTransitions(svg, doc, layout, state) {
|
|
997
|
+
if (!state || state.edgeMode === undefined || state.edgeMode === 'idle') return;
|
|
998
|
+
for (let i = 0; i < layout.lanes.length; i += 1) {
|
|
999
|
+
const cells = geometry.transitionsOf(doc, i);
|
|
1000
|
+
for (const c of cells) {
|
|
1001
|
+
const a = geometry.anchorOfCell(layout, i, c);
|
|
1002
|
+
if (a === null) continue;
|
|
1003
|
+
const hot = state.hoverBoundary !== undefined && state.hoverBoundary !== null &&
|
|
1004
|
+
state.hoverBoundary.laneIndex === i && state.hoverBoundary.cell === c;
|
|
1005
|
+
const dot = d.createElementNS(SVGNS, 'circle');
|
|
1006
|
+
dot.setAttribute('class', 'ed-wave-transition');
|
|
1007
|
+
dot.setAttribute('cx', a.x);
|
|
1008
|
+
dot.setAttribute('cy', a.y);
|
|
1009
|
+
dot.setAttribute('r', hot ? TRANSITION_R_HOT : TRANSITION_R);
|
|
1010
|
+
if (hot) dot.setAttribute('data-hot', '');
|
|
1011
|
+
svg.appendChild(dot);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function drawLane(svg, layout, i) {
|
|
1017
|
+
const row = layout.lanes[i];
|
|
1018
|
+
const lane = row.lane;
|
|
1019
|
+
// An ABSENT `wave` and an EMPTY one are two different pictures, and
|
|
1020
|
+
// collapsing them into one was the regression the previous round shipped
|
|
1021
|
+
// on the very axis it was fixing. MEASURED against the pinned engine,
|
|
1022
|
+
// second half-brick per cycle:
|
|
1023
|
+
//
|
|
1024
|
+
// {name:'a', wave:''} ["xxx","xxx"] ONE x cycle
|
|
1025
|
+
// {name:'a'} [] NOTHING
|
|
1026
|
+
// {} (spacer) [] NOTHING
|
|
1027
|
+
// {name:'a', wave:1234} [] NOTHING
|
|
1028
|
+
// {name:'a', wave:null} [] NOTHING
|
|
1029
|
+
//
|
|
1030
|
+
// and in a mixed diagram the engine simply emits no bricks for that row:
|
|
1031
|
+
// `{signal:[{a,'0101'},{},{b,'1010'}]}` is 16 uses, i.e. 2 lanes × 4 × 2.
|
|
1032
|
+
// So the rule is exactly "a lane draws cycles only when `wave` is a
|
|
1033
|
+
// string"; `''` is a string, which is why it gets its one cycle, and
|
|
1034
|
+
// `{}` — the canonical WaveDrom blank-row spacer — gets none.
|
|
1035
|
+
//
|
|
1036
|
+
// The empty-string case is not a corner either: `deleteCycles` covering
|
|
1037
|
+
// every cycle sets every lane's wave to `''`, two clicks away.
|
|
1038
|
+
const hasWave = typeof lane.wave === 'string';
|
|
1039
|
+
const wave = hasWave ? lane.wave : '';
|
|
1040
|
+
const cells = hasWave ? codec.expandWave(wave) : [];
|
|
1041
|
+
const levels = !hasWave ? [] : (wave === '' ? ['x'] : codec.levelsOf(cells));
|
|
1042
|
+
const bricks = levels.map(brickOf);
|
|
1043
|
+
// Which cycles carry the engine's discontinuity marker. `levelsOf`
|
|
1044
|
+
// deliberately erases `|` — it answers what a cycle SHOWS, and a gap
|
|
1045
|
+
// shows whatever is in front of it — so the marker has to be read off the
|
|
1046
|
+
// cells. Measured: `01|10` renders one `<use xlink:href="#gap">` at
|
|
1047
|
+
// translate(100), which is the centre of cycle 2, the `|` cycle.
|
|
1048
|
+
const gaps = [];
|
|
1049
|
+
for (let c = 0; c < cells.length; c++) {
|
|
1050
|
+
if (cells[c].held !== true && cells[c].ch === '|') gaps.push(c);
|
|
1051
|
+
}
|
|
1052
|
+
// v3.5.0 Task 10 fix round 2: which cycles carry the P/N explicit-edge
|
|
1053
|
+
// arrow, and which one. Read off `levels` directly rather than off
|
|
1054
|
+
// `bricks`, for the same reason `gaps` above reads off `cells` rather
|
|
1055
|
+
// than off `bricks`: `brickOf` collapses `P` into the SAME brick id as
|
|
1056
|
+
// `p` (that is what makes the arrow the one thing `brickOf`'s own
|
|
1057
|
+
// model cannot carry — see its comment, and the measurement above the
|
|
1058
|
+
// arrow's own drawing code below), so asking `bricks` here would
|
|
1059
|
+
// always come back empty. One entry per cycle that has an arrow,
|
|
1060
|
+
// `"<cycle>:<P|N>"`, in the same index-not-one-slot-per-cycle shape
|
|
1061
|
+
// `data-gaps-<i>` already uses — an empty lane (no `P`/`N` anywhere,
|
|
1062
|
+
// e.g. every existing fixture's plain `p`/`n` clocks) reads as `''`,
|
|
1063
|
+
// which is the ABSENCE half of the contract this attribute exists to
|
|
1064
|
+
// let a test pin: a `P` lane must show an entry here, a `p` lane must
|
|
1065
|
+
// not, and drawing the arrow on every clock (the regression this
|
|
1066
|
+
// attribute is for) would make the second half false.
|
|
1067
|
+
const clockEdges = [];
|
|
1068
|
+
for (let c = 0; c < levels.length; c++) {
|
|
1069
|
+
if (levels[c] === 'P' || levels[c] === 'N') clockEdges.push(c + ':' + levels[c]);
|
|
1070
|
+
}
|
|
1071
|
+
// `data-wave-<i>` cannot carry the difference between "no `wave` key" and
|
|
1072
|
+
// `wave: ''` — both are the empty string — and that difference is exactly
|
|
1073
|
+
// what the engine draws differently, so it gets its own attribute.
|
|
1074
|
+
svg.setAttribute('data-haswave-' + i, hasWave ? '1' : '0');
|
|
1075
|
+
// v3.6.0 Task 6 (ruling R18 point 3): this lane's OWN cycle width, not
|
|
1076
|
+
// the canvas-level `data-cycle-width` (lane 0's) — Task 4 made
|
|
1077
|
+
// `cycleWidth` per lane via `period`/`hscale`, so a caller that wants to
|
|
1078
|
+
// trust the single canvas-level attribute has to be able to check every
|
|
1079
|
+
// lane actually agrees with it. `editor-journey.test.js`'s T6i reads
|
|
1080
|
+
// exactly this attribute to prove `period`/`config.hscale` reach the
|
|
1081
|
+
// canvas at all (spec A5): its `period: 2` lane under a global
|
|
1082
|
+
// `hscale: 2` must measure 4× the same lane in the control document,
|
|
1083
|
+
// and the `period`-less sibling 2×.
|
|
1084
|
+
svg.setAttribute('data-cycle-width-' + i, String(row.cycleWidth));
|
|
1085
|
+
svg.setAttribute('data-wave-' + i, wave);
|
|
1086
|
+
svg.setAttribute('data-levels-' + i, levels.join(''));
|
|
1087
|
+
svg.setAttribute('data-bricks-' + i, bricks.join(' '));
|
|
1088
|
+
svg.setAttribute('data-gaps-' + i, gaps.join(' '));
|
|
1089
|
+
svg.setAttribute('data-clock-edge-' + i, clockEdges.join(' '));
|
|
1090
|
+
|
|
1091
|
+
const hi = row.y + 7;
|
|
1092
|
+
const lo = row.y + row.height - 9;
|
|
1093
|
+
const mid = (hi + lo) / 2;
|
|
1094
|
+
// v3.6.0 Task 6: this row's OWN `cycleWidth` (Task 4 made it per-lane via
|
|
1095
|
+
// `period`/`hscale`), not the diagram-wide `layout.cycleWidth` — and
|
|
1096
|
+
// every x built from it below starts at `row.originX`, not 0. Before
|
|
1097
|
+
// this task `row.originX` was always exactly 0 (SIZES.nameColWidth was
|
|
1098
|
+
// 0 and no fixture set `phase`), so the two were indistinguishable and
|
|
1099
|
+
// this file never had to care; `nameColWidth` going to 120 makes the gap
|
|
1100
|
+
// between "drawn at x=0" and "hit-tested from `row.originX`" the whole
|
|
1101
|
+
// width of the name column — a cell would still LOOK painted, in the
|
|
1102
|
+
// wrong 120px, while `cellAt` reported it was in the name column.
|
|
1103
|
+
const cw = row.cycleWidth;
|
|
1104
|
+
const labels = labelsOf(lane);
|
|
1105
|
+
// Which cycles carry a `data` label: the explicit value cycles, in order.
|
|
1106
|
+
// `levelsOf` repeats a bus's level through its continuation cycles, so the
|
|
1107
|
+
// label belongs to the cycle that STARTS the run, which is what the run
|
|
1108
|
+
// grouping below already knows.
|
|
1109
|
+
let labelSlot = 0;
|
|
1110
|
+
|
|
1111
|
+
// Runs of one picture. Clock bricks never merge — each `p` cycle is a
|
|
1112
|
+
// whole clock period, and a merged run would draw one long one.
|
|
1113
|
+
const runs = [];
|
|
1114
|
+
for (let c = 0; c < bricks.length; c++) {
|
|
1115
|
+
const last = runs.length === 0 ? null : runs[runs.length - 1];
|
|
1116
|
+
if (last !== null && last.brick === bricks[c] && !isClock(bricks[c])) last.to = c;
|
|
1117
|
+
else runs.push({ brick: bricks[c], from: c, to: c });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// `bandOf` (module scope, v3.6.0 Task 10 — also driving `levelIcon`)
|
|
1121
|
+
// takes hi/lo/mid explicitly rather than closing over this function's
|
|
1122
|
+
// locals, so it is the one belief this file has about which height a
|
|
1123
|
+
// brick paints at, not a copy of it.
|
|
1124
|
+
|
|
1125
|
+
for (let r = 0; r < runs.length; r++) {
|
|
1126
|
+
const run = runs[r];
|
|
1127
|
+
const x0 = row.originX + run.from * cw;
|
|
1128
|
+
const x1 = row.originX + (run.to + 1) * cw;
|
|
1129
|
+
const brick = run.brick;
|
|
1130
|
+
if (isBus(brick)) {
|
|
1131
|
+
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
1132
|
+
const s = Math.min(6, cw / 4);
|
|
1133
|
+
poly.setAttribute('class', 'ed-wave-bus ed-wave-bus-' + brick.slice(4));
|
|
1134
|
+
poly.setAttribute('points', busPolygonPoints(x0, x1, hi, lo, mid, s));
|
|
1135
|
+
svg.appendChild(poly);
|
|
1136
|
+
const text = labels[labelSlot];
|
|
1137
|
+
if (typeof text === 'string' && text !== '') {
|
|
1138
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
1139
|
+
t.setAttribute('class', 'ed-wave-buslabel');
|
|
1140
|
+
t.setAttribute('x', String((x0 + x1) / 2));
|
|
1141
|
+
t.setAttribute('y', String(mid + 4));
|
|
1142
|
+
t.setAttribute('text-anchor', 'middle');
|
|
1143
|
+
t.textContent = text;
|
|
1144
|
+
svg.appendChild(t);
|
|
1145
|
+
} else {
|
|
1146
|
+
// v3.5.0 Task 11 fix round 3: an UNLABELLED bus run has no
|
|
1147
|
+
// `<text>` for a press to land on — fix round 2's own rule
|
|
1148
|
+
// ("open the field only on a press that hits the label text")
|
|
1149
|
+
// otherwise leaves the single most ordinary flow in this editor
|
|
1150
|
+
// (paint a `=`, then name it) mouse-unreachable. This dot is
|
|
1151
|
+
// that run's hit target when there is no label yet: same
|
|
1152
|
+
// class as the text (`ed-wave-buslabel`, so `onCanvasDown`'s
|
|
1153
|
+
// existing hit test needs no change at all — it already asks
|
|
1154
|
+
// for that one class, not for an element type) plus
|
|
1155
|
+
// `ed-wave-buslabel-empty` for the dim, small styling
|
|
1156
|
+
// (`lib/md2doc.js`) that keeps a diagram full of unlabelled
|
|
1157
|
+
// buses from turning into a field of marks. Radius bounded by
|
|
1158
|
+
// the run's own box on both axes, not a bare constant, so the
|
|
1159
|
+
// box stays MOSTLY paintable even at the narrowest cycle width
|
|
1160
|
+
// this editor draws, and a press anywhere else in the box still
|
|
1161
|
+
// falls through to the ordinary paint dispatch exactly as
|
|
1162
|
+
// round 2 established.
|
|
1163
|
+
const r = Math.min(3.5, (x1 - x0) / 6, (lo - hi) / 6);
|
|
1164
|
+
const dot = d.createElementNS(SVGNS, 'circle');
|
|
1165
|
+
dot.setAttribute('class', 'ed-wave-buslabel ed-wave-buslabel-empty');
|
|
1166
|
+
dot.setAttribute('cx', String((x0 + x1) / 2));
|
|
1167
|
+
dot.setAttribute('cy', String(mid));
|
|
1168
|
+
dot.setAttribute('r', String(r));
|
|
1169
|
+
svg.appendChild(dot);
|
|
1170
|
+
}
|
|
1171
|
+
labelSlot++;
|
|
1172
|
+
} else if (brick === 'xxx') {
|
|
1173
|
+
const rect = d.createElementNS(SVGNS, 'rect');
|
|
1174
|
+
rect.setAttribute('class', 'ed-wave-x');
|
|
1175
|
+
rect.setAttribute('x', String(x0));
|
|
1176
|
+
rect.setAttribute('y', String(hi));
|
|
1177
|
+
rect.setAttribute('width', String(x1 - x0));
|
|
1178
|
+
rect.setAttribute('height', String(lo - hi));
|
|
1179
|
+
svg.appendChild(rect);
|
|
1180
|
+
} else if (isClock(brick)) {
|
|
1181
|
+
const xm = (x0 + x1) / 2;
|
|
1182
|
+
const p = d.createElementNS(SVGNS, 'path');
|
|
1183
|
+
p.setAttribute('class', 'ed-wave-line ed-wave-clock');
|
|
1184
|
+
p.setAttribute('d', clockStepPath(brick, x0, x1, hi, lo));
|
|
1185
|
+
svg.appendChild(p);
|
|
1186
|
+
|
|
1187
|
+
// v3.5.0 Task 10: `P`/`N` are explicit-edge clocks. `brickOf` maps
|
|
1188
|
+
// both to the SAME brick id as `p`/`n` (`nclk`/`pclk` — see its own
|
|
1189
|
+
// comment), which is correct for the level the engine SHOWS but
|
|
1190
|
+
// erases the one thing that makes `P`/`N` look different: an
|
|
1191
|
+
// arrowhead. MEASURED against the pinned wavedrom 3.5.0 — rendered
|
|
1192
|
+
// `{signal:[{name:'a',wave:'zpp'}]}` next to `{wave:'zPP'}` via
|
|
1193
|
+
// `wd.renderAny(0, doc, wd.waveSkin)` and diffed the `<use
|
|
1194
|
+
// xlink:href>` list:
|
|
1195
|
+
//
|
|
1196
|
+
// p: #zzz #zzz #pclk #nclk #pclk #nclk
|
|
1197
|
+
// P: #zzz #zzz #Pclk #nclk #Pclk #nclk
|
|
1198
|
+
// n: #zzz #zzz #nclk #pclk #nclk #pclk
|
|
1199
|
+
// N: #zzz #zzz #Nclk #pclk #Nclk #pclk
|
|
1200
|
+
//
|
|
1201
|
+
// — same SECOND half-brick every time (that is what `brickOf`'s own
|
|
1202
|
+
// measurement already pins), but the FIRST half-brick gets its own
|
|
1203
|
+
// symbol. Read straight out of `node_modules/wavedrom/skins/default.js`:
|
|
1204
|
+
//
|
|
1205
|
+
// pclk 'M0,20 0,0 20,0' (just the edge)
|
|
1206
|
+
// Pclk same edge PLUS 'M-3,12 0,3 3,12 C 1,11 -1,11 -3,12 z' (filled, class s6)
|
|
1207
|
+
// nclk 'm0,0 0,20 20,0'
|
|
1208
|
+
// Nclk same edge PLUS 'M-3,8 0,17 3,8 C 1,9 -1,9 -3,8 z' (filled, class s6)
|
|
1209
|
+
//
|
|
1210
|
+
// i.e. in the engine's own 20-unit symbol space (y=0 top, y=20
|
|
1211
|
+
// bottom, matching this file's hi/lo), a small filled triangle
|
|
1212
|
+
// straddling local x=0 — `P`'s apex at local y=3 pointing UP
|
|
1213
|
+
// (toward hi), `N`'s apex at local y=17 pointing DOWN (toward lo).
|
|
1214
|
+
// Both sit on the FIRST half-brick, which in this drawing's
|
|
1215
|
+
// one-brick-per-period model (a whole `p`/`P` cycle is ONE `nclk`
|
|
1216
|
+
// path, not two half-bricks — see `isClock`'s own comment) is
|
|
1217
|
+
// always the vertical stroke at x0, the edge INTO this cycle. So
|
|
1218
|
+
// the level char (not the brick id, which is identical for
|
|
1219
|
+
// `p`/`P`) is what has to be read to know an arrow is owed, and
|
|
1220
|
+
// `levels[run.from]` is it — clock bricks never merge (the `!
|
|
1221
|
+
// isClock` guard below), so `run.from === run.to` and this is
|
|
1222
|
+
// exactly the cycle being drawn. `data-clock-edge-<i>`, set above
|
|
1223
|
+
// (this function's own top, next to `data-gaps-<i>`) from the same
|
|
1224
|
+
// `levels` array, is the one place this fact is exposed for a test
|
|
1225
|
+
// to pin — nothing here duplicates it onto the shape itself, so
|
|
1226
|
+
// there is one belief about which cycles have an arrow, not two.
|
|
1227
|
+
//
|
|
1228
|
+
// Reproduced as a plain filled triangle, not the engine's
|
|
1229
|
+
// bezier-curved one: every other brick this file draws is already
|
|
1230
|
+
// a simplified redraw (the bus is a hexagon, the gap is a pair of
|
|
1231
|
+
// chevrons), not a pixel copy of wavedrom's own glyphs. Scaled out
|
|
1232
|
+
// of the engine's 20-unit symbol into this cell's own geometry —
|
|
1233
|
+
// local x by (xm - x0) / 20, local y by (lo - hi) / 20, both
|
|
1234
|
+
// anchored at (x0, hi) — rather than hand-picked pixel offsets, so
|
|
1235
|
+
// it tracks `SIZES.cycleWidth`/`laneHeight` if those ever change.
|
|
1236
|
+
const edgeLevel = levels[run.from];
|
|
1237
|
+
const arrowD = clockArrowPath(edgeLevel, x0, xm, hi, lo);
|
|
1238
|
+
if (arrowD !== null) {
|
|
1239
|
+
const arrow = d.createElementNS(SVGNS, 'path');
|
|
1240
|
+
arrow.setAttribute('class', 'ed-wave-clock-arrow');
|
|
1241
|
+
arrow.setAttribute('d', arrowD);
|
|
1242
|
+
svg.appendChild(arrow);
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
const y = bandOf(brick, hi, lo, mid)[0];
|
|
1246
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
1247
|
+
line.setAttribute('class', brick === 'uuu' || brick === 'ddd'
|
|
1248
|
+
? 'ed-wave-line ed-wave-weak' : 'ed-wave-line');
|
|
1249
|
+
line.setAttribute('x1', String(x0));
|
|
1250
|
+
line.setAttribute('x2', String(x1));
|
|
1251
|
+
line.setAttribute('y1', String(y));
|
|
1252
|
+
line.setAttribute('y2', String(y));
|
|
1253
|
+
svg.appendChild(line);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// The edge into this run. Clock bricks draw their own.
|
|
1257
|
+
if (r > 0 && !isClock(brick) && !isClock(runs[r - 1].brick)) {
|
|
1258
|
+
const prevBrick = runs[r - 1].brick;
|
|
1259
|
+
if (isRampPair(brick, prevBrick)) {
|
|
1260
|
+
// v3.6.0 Task 7: a real level change draws the ramp the engine
|
|
1261
|
+
// draws, not a vertical instant edge — a vertical edge left
|
|
1262
|
+
// Task 5's `anchorOfCell` marker floating `anchorRatio` (0.15) of
|
|
1263
|
+
// a cycle to the right of anything painted (48px pitch -> 7.2px)
|
|
1264
|
+
// because nothing was drawn there yet. See `brickPath`'s own
|
|
1265
|
+
// comment for the measured ramp geometry.
|
|
1266
|
+
const local = brickPath(lineLevelOf(brick), lineLevelOf(prevBrick), cw, lo - hi);
|
|
1267
|
+
const edge = d.createElementNS(SVGNS, 'path');
|
|
1268
|
+
edge.setAttribute('class', 'ed-wave-edge');
|
|
1269
|
+
edge.setAttribute('d', translatePath(local, x0, hi));
|
|
1270
|
+
svg.appendChild(edge);
|
|
1271
|
+
} else {
|
|
1272
|
+
const a = bandOf(prevBrick, hi, lo, mid);
|
|
1273
|
+
const b = bandOf(brick, hi, lo, mid);
|
|
1274
|
+
const top = Math.min(a[0], a[1], b[0], b[1]);
|
|
1275
|
+
const bot = Math.max(a[0], a[1], b[0], b[1]);
|
|
1276
|
+
if (bot > top) {
|
|
1277
|
+
const edge = d.createElementNS(SVGNS, 'line');
|
|
1278
|
+
edge.setAttribute('class', 'ed-wave-edge');
|
|
1279
|
+
edge.setAttribute('x1', String(x0));
|
|
1280
|
+
edge.setAttribute('x2', String(x0));
|
|
1281
|
+
edge.setAttribute('y1', String(top));
|
|
1282
|
+
edge.setAttribute('y2', String(bot));
|
|
1283
|
+
svg.appendChild(edge);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// The gap marker, last so it sits over the trace it interrupts. The
|
|
1290
|
+
// engine draws one per `|`; drawing none made the two pictures disagree
|
|
1291
|
+
// on a lane whose levels agreed exactly, which is the quietest form of
|
|
1292
|
+
// the disagreement this overlay exists to surface.
|
|
1293
|
+
for (const c of gaps) {
|
|
1294
|
+
const xm = row.originX + (c + 0.5) * cw;
|
|
1295
|
+
const mark = d.createElementNS(SVGNS, 'path');
|
|
1296
|
+
mark.setAttribute('class', 'ed-wave-gap');
|
|
1297
|
+
mark.setAttribute('d', gapMarkPath(xm, hi, lo, mid));
|
|
1298
|
+
svg.appendChild(mark);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Every `edge` entry, drawn after every lane: one `<path>` (`d` from
|
|
1304
|
+
* `geometry.edgeLayout`), a label `<text>` when the entry has one, and —
|
|
1305
|
+
* for `selectedEdge` only — two endpoint handles from
|
|
1306
|
+
* `geometry.edgeHandleRect`, exactly the rectangles `onCanvasDown` below
|
|
1307
|
+
* hit-tests against, so the drawn handle and the clickable one cannot
|
|
1308
|
+
* drift apart.
|
|
1309
|
+
*
|
|
1310
|
+
* A second, invisible, wide-stroke `<path>` (`class="ed-wave-edge-hit"`)
|
|
1311
|
+
* is layered over each visible one purely so it can be clicked: the
|
|
1312
|
+
* visible path is 1px wide, and `document.elementFromPoint` (what
|
|
1313
|
+
* `onCanvasDown` reads to answer "did this press land on a line") would
|
|
1314
|
+
* otherwise miss any press that is not on that exact pixel.
|
|
1315
|
+
* `lib/md2doc.js`'s `.ed-wave-edge-hit` rule sets `pointer-events: stroke`
|
|
1316
|
+
* — what makes a `stroke: transparent` path hit-testable at all, without
|
|
1317
|
+
* which a transparent stroke paints nothing and "nothing painted" is what
|
|
1318
|
+
* `visiblePainted` (the SVG default) refuses to hit.
|
|
1319
|
+
*
|
|
1320
|
+
* Arrow markers are picked from the shape string, MEASURED against
|
|
1321
|
+
* `node_modules/wavedrom/lib/arc-shape.js`'s own `style` table (every
|
|
1322
|
+
* `case` there that sets a `style` string): a shape containing `<` sets
|
|
1323
|
+
* `marker-start:url(#arrowtail)`, one containing `>` sets
|
|
1324
|
+
* `marker-end:url(#arrowhead)`, and `+` is the one shape that sets BOTH
|
|
1325
|
+
* markers, to `url(#tee)` — the seven plain shapes (`-`, `~`, `-~`, `~-`,
|
|
1326
|
+
* `-|`, `|-`, `-|-`) set no `style` at all and draw no arrowhead.
|
|
1327
|
+
* (`applyEdgeMarkers` below is unaffected by the exact count either way —
|
|
1328
|
+
* it tests `indexOf('<')` / `indexOf('>')` rather than enumerating a
|
|
1329
|
+
* list — but this comment's own count should still match the pinned
|
|
1330
|
+
* engine.) `edgeArrowMarker` below reproduces the head/tail split with
|
|
1331
|
+
* ONE shared marker definition rather than two: `orient="auto-start-reverse"`
|
|
1332
|
+
* is what flips a `marker-start` 180° from a `marker-end` automatically,
|
|
1333
|
+
* so the single triangle already points the right way at both ends.
|
|
1334
|
+
*/
|
|
1335
|
+
function renderEdges(svg, doc, layout, state) {
|
|
1336
|
+
const edges = geometry.edgeLayout(doc, layout);
|
|
1337
|
+
state.overlay.setAttribute('data-wave-edge-count', String(edges.length));
|
|
1338
|
+
if (edges.length === 0) return edges;
|
|
1339
|
+
|
|
1340
|
+
const defs = d.createElementNS(SVGNS, 'defs');
|
|
1341
|
+
defs.appendChild(edgeArrowMarker());
|
|
1342
|
+
defs.appendChild(edgeTeeMarker());
|
|
1343
|
+
svg.appendChild(defs);
|
|
1344
|
+
|
|
1345
|
+
for (const e of edges) {
|
|
1346
|
+
const isSelected = e.index === state.selectedEdge;
|
|
1347
|
+
// v3.5.0 Task 8: the edge an in-flight endpoint drag is dragging
|
|
1348
|
+
// keeps drawing at its ACTUAL (still-committed) position — the
|
|
1349
|
+
// document is untouched until the drop (rule 3) — but is faded so
|
|
1350
|
+
// it reads as "not where this will end up" once the dashed preview
|
|
1351
|
+
// (`renderCanvas`, below `renderEdges`'s own call) is standing next
|
|
1352
|
+
// to it. `endpointDrag` can only ever name `selectedEdge` itself
|
|
1353
|
+
// (`render()`'s own clamp), so `isSelected` is already true here too
|
|
1354
|
+
// and this only ever adds a class, never substitutes for `.is-on`.
|
|
1355
|
+
const isDragSource = state.endpointDrag !== null && e.index === state.endpointDrag.index;
|
|
1356
|
+
const path = d.createElementNS(SVGNS, 'path');
|
|
1357
|
+
// Every visual property (stroke, fill, width) lives in
|
|
1358
|
+
// `lib/md2doc.js`'s `.ed-wave-edge-path` / `.is-on` CSS, the same
|
|
1359
|
+
// mechanism the rest of this drawing's colours already go through
|
|
1360
|
+
// (`.ed-wave-line`, `.ed-wave-selection`, `.ed-wave-cursor`, …) —
|
|
1361
|
+
// one place to repaint the editor, not two. Only genuinely geometric
|
|
1362
|
+
// attributes (`d`, the marker refs) stay here as attributes.
|
|
1363
|
+
path.setAttribute('class', 'ed-wave-edge-path' + (isSelected ? ' is-on' : '') +
|
|
1364
|
+
(isDragSource ? ' is-drag-source' : ''));
|
|
1365
|
+
path.setAttribute('data-edge-index', String(e.index));
|
|
1366
|
+
path.setAttribute('d', e.d);
|
|
1367
|
+
applyEdgeMarkers(path, e.edge.shape);
|
|
1368
|
+
svg.appendChild(path);
|
|
1369
|
+
|
|
1370
|
+
const hit = d.createElementNS(SVGNS, 'path');
|
|
1371
|
+
hit.setAttribute('class', 'ed-wave-edge-hit');
|
|
1372
|
+
hit.setAttribute('data-edge-index', String(e.index));
|
|
1373
|
+
hit.setAttribute('d', e.d);
|
|
1374
|
+
svg.appendChild(hit);
|
|
1375
|
+
|
|
1376
|
+
if (e.edge.label !== '') {
|
|
1377
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
1378
|
+
t.setAttribute('class', 'ed-wave-edge-label');
|
|
1379
|
+
t.setAttribute('x', String(e.labelPos.x));
|
|
1380
|
+
t.setAttribute('y', String(e.labelPos.y - 4));
|
|
1381
|
+
// `text-anchor` positions the glyphs against (x, y) — geometry, not
|
|
1382
|
+
// colour — so it stays an attribute, exactly as the existing
|
|
1383
|
+
// `.ed-wave-buslabel` text already does a few lines up.
|
|
1384
|
+
t.setAttribute('text-anchor', 'middle');
|
|
1385
|
+
t.textContent = e.edge.label;
|
|
1386
|
+
svg.appendChild(t);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (isSelected) {
|
|
1390
|
+
for (const end of ['from', 'to']) {
|
|
1391
|
+
const r = geometry.edgeHandleRect(e[end]);
|
|
1392
|
+
const h = d.createElementNS(SVGNS, 'rect');
|
|
1393
|
+
h.setAttribute('class', 'ed-wave-edge-handle');
|
|
1394
|
+
h.setAttribute('data-edge-handle', end);
|
|
1395
|
+
h.setAttribute('data-edge-index', String(e.index));
|
|
1396
|
+
h.setAttribute('x', String(r.x));
|
|
1397
|
+
h.setAttribute('y', String(r.y));
|
|
1398
|
+
h.setAttribute('width', String(r.width));
|
|
1399
|
+
h.setAttribute('height', String(r.height));
|
|
1400
|
+
svg.appendChild(h);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return edges;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** `marker-start` / `marker-end` for one shape — see `renderEdges`'s own
|
|
1408
|
+
* comment for the measurement behind this split. */
|
|
1409
|
+
function applyEdgeMarkers(path, shape) {
|
|
1410
|
+
if (shape === '+') {
|
|
1411
|
+
path.setAttribute('marker-start', 'url(#ed-wave-tee)');
|
|
1412
|
+
path.setAttribute('marker-end', 'url(#ed-wave-tee)');
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (shape.indexOf('<') !== -1) path.setAttribute('marker-start', 'url(#ed-wave-arrowhead)');
|
|
1416
|
+
if (shape.indexOf('>') !== -1) path.setAttribute('marker-end', 'url(#ed-wave-arrowhead)');
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/** One triangle, reused for both ends via `auto-start-reverse`. */
|
|
1420
|
+
function edgeArrowMarker() {
|
|
1421
|
+
const m = d.createElementNS(SVGNS, 'marker');
|
|
1422
|
+
m.setAttribute('id', 'ed-wave-arrowhead');
|
|
1423
|
+
m.setAttribute('viewBox', '0 0 8 8');
|
|
1424
|
+
m.setAttribute('markerWidth', '8');
|
|
1425
|
+
m.setAttribute('markerHeight', '8');
|
|
1426
|
+
m.setAttribute('refX', '7');
|
|
1427
|
+
m.setAttribute('refY', '4');
|
|
1428
|
+
m.setAttribute('orient', 'auto-start-reverse');
|
|
1429
|
+
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
1430
|
+
poly.setAttribute('points', '0,0 8,4 0,8');
|
|
1431
|
+
poly.setAttribute('class', 'ed-wave-edge-arrow');
|
|
1432
|
+
m.appendChild(poly);
|
|
1433
|
+
return m;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/** The `+` shape's tee: a crossbar perpendicular to the path at either
|
|
1437
|
+
* end, symmetric under a 180° flip so `orient="auto"` needs no reverse. */
|
|
1438
|
+
function edgeTeeMarker() {
|
|
1439
|
+
const m = d.createElementNS(SVGNS, 'marker');
|
|
1440
|
+
m.setAttribute('id', 'ed-wave-tee');
|
|
1441
|
+
m.setAttribute('viewBox', '0 0 8 8');
|
|
1442
|
+
m.setAttribute('markerWidth', '8');
|
|
1443
|
+
m.setAttribute('markerHeight', '8');
|
|
1444
|
+
m.setAttribute('refX', '4');
|
|
1445
|
+
m.setAttribute('refY', '4');
|
|
1446
|
+
m.setAttribute('orient', 'auto');
|
|
1447
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
1448
|
+
line.setAttribute('x1', '4');
|
|
1449
|
+
line.setAttribute('y1', '0');
|
|
1450
|
+
line.setAttribute('x2', '4');
|
|
1451
|
+
line.setAttribute('y2', '8');
|
|
1452
|
+
line.setAttribute('class', 'ed-wave-edge-tee');
|
|
1453
|
+
m.appendChild(line);
|
|
1454
|
+
return m;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
return {
|
|
1459
|
+
renderCanvas: renderCanvas,
|
|
1460
|
+
drawLane: drawLane,
|
|
1461
|
+
renderEdges: renderEdges,
|
|
1462
|
+
brickOf: brickOf,
|
|
1463
|
+
isClock: isClock,
|
|
1464
|
+
isBus: isBus,
|
|
1465
|
+
brickPath: brickPath,
|
|
1466
|
+
levelIcon: levelIcon,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
module.exports = { createDrawer: createDrawer };
|