@helping-ai-workflow/md2doc 3.12.1 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/md2doc.js +6 -0
- package/lib/editor/client.js +1015 -194
- package/lib/editor/inline-md.js +45 -1
- package/lib/editor/list-md.js +8 -1
- package/lib/editor/server.js +5 -0
- package/lib/editor/wave-canvas.js +301 -0
- package/lib/editor/wave-codec.js +417 -11
- package/lib/editor/wave-draw.js +209 -1202
- package/lib/editor/wave-geometry.js +145 -10
- package/lib/editor/wave-panels.js +907 -918
- package/lib/editor/wave-store.js +486 -91
- package/lib/editor/wave-ui.js +3588 -2232
- package/lib/md2doc.js +765 -534
- package/lib/theme/runtime.js +296 -29
- package/lib/theme/tokens.js +3 -0
- package/lib/wave-overflow.js +107 -0
- package/package.json +3 -3
- package/scripts/release-check.js +60 -0
package/lib/editor/wave-draw.js
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The editor's own marks, and the brush glyphs.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Since the wave redesign (spec §3) the WaveDrom engine draws the waveform
|
|
7
|
+
* itself (lib/editor/wave-canvas.js). This module draws only what sits on
|
|
8
|
+
* top of it — `createLayer`, the interaction layer — plus the toolbar's
|
|
9
|
+
* per-brush glyphs (`createDrawer().levelIcon`) and the small brick model
|
|
10
|
+
* those glyphs and wave-ui.js's bus test share (`brickOf`, `isBus`,
|
|
11
|
+
* `isClock`, `brickPath`). The hand-drawn canvas that used to live here
|
|
12
|
+
* (renderCanvas and its lane, name, ruler, band and edge painters) was
|
|
13
|
+
* removed in Task 6a: four of the seven defects in spec §5 were that second
|
|
14
|
+
* picture disagreeing with the engine's.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
16
|
+
* Every dependency is handed in (the document, geometry, the SVG namespace);
|
|
17
|
+
* this module names no global and requires nothing.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
function createDrawer(deps) {
|
|
21
21
|
const d = deps.d;
|
|
22
22
|
const geometry = deps.geometry;
|
|
23
|
-
const codec = deps.codec;
|
|
24
|
-
const SIZES = deps.SIZES;
|
|
25
23
|
const SVGNS = deps.SVGNS;
|
|
26
|
-
const BRUSHES = deps.BRUSHES;
|
|
27
|
-
const labelsOf = deps.labelsOf;
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
26
|
* What wavedrom 3.5.0 actually draws for one level, by its own brick id.
|
|
@@ -74,8 +70,9 @@ function createDrawer(deps) {
|
|
|
74
70
|
|
|
75
71
|
/**
|
|
76
72
|
* The SVG path for one cell's picture, in LOCAL coordinates (0,0 is the
|
|
77
|
-
* cell's own top-left
|
|
78
|
-
*
|
|
73
|
+
* cell's own top-left). Its one production caller since Task 6a is
|
|
74
|
+
* `levelIcon`, which only ever asks for the flat case; the ramp case is
|
|
75
|
+
* kept because test/wave-draw.test.js pins it against the engine's skin.
|
|
79
76
|
*
|
|
80
77
|
* A real level change is not a vertical line: the engine holds the OLD
|
|
81
78
|
* level until `slewStartRatio` into the cell, ramps to the NEW level, and
|
|
@@ -122,76 +119,9 @@ function createDrawer(deps) {
|
|
|
122
119
|
' L' + width + ',' + yNow;
|
|
123
120
|
}
|
|
124
121
|
|
|
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
122
|
/**
|
|
193
123
|
* Which vertical band a brick's own flat run sits at, in PIXELS. Lifted
|
|
194
|
-
* out of
|
|
124
|
+
* out of the old hand-drawn lane painter (v3.6.0 Task 10) to drive `levelIcon`'s
|
|
195
125
|
* height decision — `uuu`/`ddd` painting at the SAME height as `111`/`000`
|
|
196
126
|
* (not at mid, which is `brickPath`'s own `yFor` default for any char it
|
|
197
127
|
* does not explicitly recognise) is a fact this function already owns,
|
|
@@ -204,17 +134,14 @@ function createDrawer(deps) {
|
|
|
204
134
|
return [hi, lo];
|
|
205
135
|
}
|
|
206
136
|
|
|
207
|
-
/** The bus hexagon's point list
|
|
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. */
|
|
137
|
+
/** The bus hexagon's point list (v3.6.0 Task 10), `levelIcon`'s bus glyph. */
|
|
210
138
|
function busPolygonPoints(x0, x1, hi, lo, mid, s) {
|
|
211
139
|
return (x0 + s) + ',' + hi + ' ' + (x1 - s) + ',' + hi + ' ' +
|
|
212
140
|
x1 + ',' + mid + ' ' + (x1 - s) + ',' + lo + ' ' +
|
|
213
141
|
(x0 + s) + ',' + lo + ' ' + x0 + ',' + mid;
|
|
214
142
|
}
|
|
215
143
|
|
|
216
|
-
/** The clock brick's stepped edge
|
|
217
|
-
* branch (v3.6.0 Task 10), same reasoning as `busPolygonPoints`. */
|
|
144
|
+
/** The clock brick's stepped edge (v3.6.0 Task 10), `levelIcon`'s clock glyph. */
|
|
218
145
|
function clockStepPath(brick, x0, x1, hi, lo) {
|
|
219
146
|
const xm = (x0 + x1) / 2;
|
|
220
147
|
return brick === 'nclk'
|
|
@@ -224,9 +151,8 @@ function createDrawer(deps) {
|
|
|
224
151
|
' L' + xm + ',' + hi + ' L' + x1 + ',' + hi;
|
|
225
152
|
}
|
|
226
153
|
|
|
227
|
-
/** The `P`/`N` explicit-edge arrowhead
|
|
228
|
-
*
|
|
229
|
-
* same as the inline `if` it replaces. */
|
|
154
|
+
/** The `P`/`N` explicit-edge arrowhead (v3.6.0 Task 10). `null` for any
|
|
155
|
+
* other level char. */
|
|
230
156
|
function clockArrowPath(edgeLevel, x0, xm, hi, lo) {
|
|
231
157
|
if (edgeLevel !== 'P' && edgeLevel !== 'N') return null;
|
|
232
158
|
const sx = (xm - x0) / 20;
|
|
@@ -238,8 +164,7 @@ function createDrawer(deps) {
|
|
|
238
164
|
' L' + (x0 + 3 * sx) + ',' + baseY + ' Z';
|
|
239
165
|
}
|
|
240
166
|
|
|
241
|
-
/** The gap `|` marker's double chevron
|
|
242
|
-
* loop (v3.6.0 Task 10), same reasoning as `busPolygonPoints`. */
|
|
167
|
+
/** The gap `|` marker's double chevron (v3.6.0 Task 10), `levelIcon`'s gap glyph. */
|
|
243
168
|
function gapMarkPath(xm, hi, lo, mid) {
|
|
244
169
|
return 'M' + (xm - 5) + ',' + (lo + 3) + ' L' + (xm + 1) + ',' + mid +
|
|
245
170
|
' L' + (xm - 5) + ',' + (hi - 3) +
|
|
@@ -250,13 +175,12 @@ function createDrawer(deps) {
|
|
|
250
175
|
/**
|
|
251
176
|
* The toolbar's per-brush glyph — v3.6.0 Task 10.
|
|
252
177
|
*
|
|
253
|
-
* Every shape below is built from the
|
|
254
|
-
*
|
|
255
|
-
* `
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* that appearance is decided.
|
|
178
|
+
* Every shape below is built from the brick helpers above (`brickOf`,
|
|
179
|
+
* `bandOf`, `brickPath`, `isBus`, `isClock`, `busPolygonPoints`,
|
|
180
|
+
* `clockStepPath`, `clockArrowPath`, `gapMarkPath`). They were the old
|
|
181
|
+
* hand-drawn canvas's painters; since Task 6a the engine draws the canvas,
|
|
182
|
+
* so these glyphs are a simplified picture of each brush, not a copy of
|
|
183
|
+
* what the canvas shows.
|
|
260
184
|
*
|
|
261
185
|
* A glyph is a single cell with no previous level, so `brickPath` is only
|
|
262
186
|
* called here with the SAME char as both `level` and `prevLevel` — its
|
|
@@ -297,8 +221,7 @@ function createDrawer(deps) {
|
|
|
297
221
|
if (level === '|') {
|
|
298
222
|
// `brickOf('|')` itself falls through to its own `'xxx'` default (it
|
|
299
223
|
// has no case for `|` — production code never asks it: `levelsOf`
|
|
300
|
-
// erases `|` before `brickOf` ever sees it
|
|
301
|
-
// `gaps` comment above). So this has to be checked ahead of the
|
|
224
|
+
// erases `|` before `brickOf` ever sees it). So this has to be checked ahead of the
|
|
302
225
|
// `brick === 'xxx'` branch below, or the gap brush would silently
|
|
303
226
|
// draw the `x` hatch instead of its own chevron marker.
|
|
304
227
|
const gap = d.createElementNS(SVGNS, 'path');
|
|
@@ -349,1122 +272,206 @@ function createDrawer(deps) {
|
|
|
349
272
|
return svg;
|
|
350
273
|
}
|
|
351
274
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
275
|
+
return {
|
|
276
|
+
brickOf: brickOf,
|
|
277
|
+
isClock: isClock,
|
|
278
|
+
isBus: isBus,
|
|
279
|
+
brickPath: brickPath,
|
|
280
|
+
levelIcon: levelIcon,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
591
283
|
|
|
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
284
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
285
|
+
/**
|
|
286
|
+
* The interaction layer: only the editor's own marks, painted into a separate
|
|
287
|
+
* SVG laid over the engine's drawing. Everything is in LAYER-LOCAL PIXELS;
|
|
288
|
+
* the caller builds `view.cellRect` / `view.anchor` and subtracts the layer's
|
|
289
|
+
* own client rect, so this module needs neither the canvas nor the geometry.
|
|
290
|
+
*
|
|
291
|
+
* view.cells[i] = { laneIndex, cycles, transitions?: number[] } -- `transitions`
|
|
292
|
+
* lists the cell indices (as `view.anchor` takes them) where a dot belongs.
|
|
293
|
+
* state.selection = { laneIndex, from, to } in cycles; state.cursor and
|
|
294
|
+
* state.hover = { laneIndex, cycle }; state.hotDot = { laneIndex, cell }.
|
|
295
|
+
* state.rulerPlus = the cycle boundary (0..cycles) the ruler's + sits on.
|
|
296
|
+
* state.rowHover = a lane row lit whole; state.rowHoverTo (optional) the last
|
|
297
|
+
* row of a lit span (a group's). state.dropLine = { y } in layer px.
|
|
298
|
+
* view.bodyTop (optional, default 0) = where the lanes start in the layer.
|
|
299
|
+
*
|
|
300
|
+
* Task 9, the edges' hit targets (spec section 4.5), all carrying
|
|
301
|
+
* `data-edge-index` (the entry's index in `doc.edge`):
|
|
302
|
+
* state.edgeHits = [{ index, d, transform, width }] -- the engine's own path
|
|
303
|
+
* `d` with the matrix that maps it into the layer and the stroke width in
|
|
304
|
+
* that path's units, drawn as a transparent wide stroke UNDER the dots, so
|
|
305
|
+
* an anchor an edge already uses still starts a new drag from its dot;
|
|
306
|
+
* state.labelHits = [{ index, x, y, width, height }] -- the engine's label
|
|
307
|
+
* boxes (an edge's label, a point note's text), OVER the dots;
|
|
308
|
+
* state.loopHits = [{ index, x, y }] -- an unlabelled self-loop's anchor: its
|
|
309
|
+
* path has zero length and no hit area, so a small ring stands in for it.
|
|
310
|
+
* state.selectedEdge = { d, transform?, from, to } draws the selection and
|
|
311
|
+
* its two end handles, each a transparent ring (`ed-wave-edge-end-hit`,
|
|
312
|
+
* the press target) under its visible mark; state.selectedNote = { x, y,
|
|
313
|
+
* width, height } outlines a selected point note.
|
|
314
|
+
* `paint` replaces the layer's children on every call.
|
|
315
|
+
*/
|
|
316
|
+
function createLayer(deps) {
|
|
317
|
+
const d = deps.d;
|
|
318
|
+
const SVGNS = deps.SVGNS;
|
|
319
|
+
const DOT_R = 3;
|
|
320
|
+
const DOT_R_HOT = 5;
|
|
321
|
+
const HIT_R = 8;
|
|
322
|
+
const END_R = 4;
|
|
323
|
+
const END_HIT_R = 8;
|
|
324
|
+
const LOOP_HIT_R = 4;
|
|
325
|
+
const PLUS_HIT_R = 9;
|
|
326
|
+
const PLUS_LIFT = 5;
|
|
327
|
+
|
|
328
|
+
function mk(tag, cls, attrs) {
|
|
329
|
+
const el = d.createElementNS(SVGNS, tag);
|
|
330
|
+
if (cls) el.setAttribute('class', cls);
|
|
331
|
+
Object.keys(attrs || {}).forEach(function (k) { el.setAttribute(k, String(attrs[k])); });
|
|
332
|
+
return el;
|
|
628
333
|
}
|
|
629
334
|
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
};
|
|
335
|
+
function paint(layer, view, state) {
|
|
336
|
+
while (layer.firstChild) layer.removeChild(layer.firstChild);
|
|
337
|
+
// Fake DOMs without removeChild/firstChild expose `children` only.
|
|
338
|
+
if (layer.children && layer.children.length) layer.children.length = 0;
|
|
339
|
+
const cells = view.cells || [];
|
|
340
|
+
const first = cells.length > 0 ? cells[0] : null;
|
|
341
|
+
let maxCycles = 0;
|
|
342
|
+
cells.forEach(function (c) { if (c.cycles > maxCycles) maxCycles = c.cycles; });
|
|
343
|
+
const on = new Set();
|
|
344
|
+
if (state.hover) on.add(state.hover.cycle);
|
|
345
|
+
if (state.cursor) on.add(state.cursor.cycle);
|
|
346
|
+
|
|
347
|
+
// ruler: one number per cycle, over the first lane's cell columns
|
|
348
|
+
if (first !== null) {
|
|
349
|
+
for (let c = 0; c < maxCycles; c += 1) {
|
|
350
|
+
const r = view.cellRect(first.laneIndex, c);
|
|
351
|
+
const t = mk('text', 'ed-wave-ruler-num' + (on.has(c) ? ' is-on' : ''), {
|
|
352
|
+
x: r.x + r.width / 2, y: view.rulerY, 'text-anchor': 'middle', 'data-cycle': c,
|
|
353
|
+
});
|
|
354
|
+
t.appendChild(d.createTextNode(String(c)));
|
|
355
|
+
layer.appendChild(t);
|
|
659
356
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
};
|
|
357
|
+
if (state.rulerPlus !== null && state.rulerPlus !== undefined) {
|
|
358
|
+
const r = view.cellRect(first.laneIndex, state.rulerPlus);
|
|
359
|
+
// A <g> ignores x/y: it is placed by its transform, and it carries
|
|
360
|
+
// its own visible glyph (styled by .ed-wave-ruler-plus in the CSS).
|
|
361
|
+
const plus = mk('g', 'ed-wave-ruler-plus', {
|
|
362
|
+
transform: 'translate(' + r.x + ',' + view.rulerY + ')', 'data-cycle': state.rulerPlus,
|
|
363
|
+
});
|
|
364
|
+
const glyph = mk('text', 'ed-wave-ruler-plus-glyph', { x: 0, y: 0, 'text-anchor': 'middle' });
|
|
365
|
+
glyph.appendChild(d.createTextNode('+'));
|
|
366
|
+
plus.appendChild(glyph);
|
|
367
|
+
// Task 7: the plus is pressed to insert a cycle, so it carries a
|
|
368
|
+
// transparent ring the size of a dot's hit circle, centred on the
|
|
369
|
+
// glyph (which sits on the ruler baseline, so its middle is
|
|
370
|
+
// PLUS_LIFT above it). The CSS gives only this ring presses.
|
|
371
|
+
plus.appendChild(mk('circle', 'ed-wave-ruler-plus-hit', {
|
|
372
|
+
cx: 0, cy: -PLUS_LIFT, r: PLUS_HIT_R, fill: 'transparent',
|
|
373
|
+
}));
|
|
374
|
+
layer.appendChild(plus);
|
|
708
375
|
}
|
|
709
376
|
}
|
|
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
377
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
}
|
|
378
|
+
if (state.rowHover !== null && state.rowHover !== undefined) {
|
|
379
|
+
// Task 8: `rowHoverTo` (optional) widens it to a group's span of rows.
|
|
380
|
+
const r = view.cellRect(state.rowHover, 0);
|
|
381
|
+
const last = typeof state.rowHoverTo === 'number' && state.rowHoverTo > state.rowHover
|
|
382
|
+
? view.cellRect(state.rowHoverTo, 0) : r;
|
|
383
|
+
layer.appendChild(mk('rect', 'ed-wave-row-hover', {
|
|
384
|
+
x: 0, y: r.y, width: view.width, height: last.y + last.height - r.y,
|
|
385
|
+
}));
|
|
882
386
|
}
|
|
883
|
-
state.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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);
|
|
387
|
+
if (state.hover) {
|
|
388
|
+
// The whole column under the lanes; `view.bodyTop` is where the lanes
|
|
389
|
+
// start when the layer also covers the ruler band above them.
|
|
390
|
+
const r = view.cellRect(state.hover.laneIndex, state.hover.cycle);
|
|
391
|
+
const top = typeof view.bodyTop === 'number' ? view.bodyTop : 0;
|
|
392
|
+
layer.appendChild(mk('rect', 'ed-wave-hover-col', { x: r.x, y: top, width: r.width, height: view.height - top }));
|
|
902
393
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
}
|
|
394
|
+
if (state.selection) {
|
|
395
|
+
const a = view.cellRect(state.selection.laneIndex, Math.min(state.selection.from, state.selection.to));
|
|
396
|
+
const b = view.cellRect(state.selection.laneIndex, Math.max(state.selection.from, state.selection.to));
|
|
397
|
+
layer.appendChild(mk('rect', 'ed-wave-selection', {
|
|
398
|
+
x: a.x, y: a.y, width: b.x + b.width - a.x, height: a.height,
|
|
399
|
+
}));
|
|
928
400
|
}
|
|
929
|
-
state.
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
401
|
+
(state.edgeHits || []).forEach(function (h) {
|
|
402
|
+
const attrs = { 'data-edge-index': h.index, d: h.d, fill: 'none', stroke: 'transparent', 'stroke-width': h.width };
|
|
403
|
+
if (h.transform) attrs.transform = h.transform;
|
|
404
|
+
layer.appendChild(mk('path', 'ed-wave-edge-hit', attrs));
|
|
405
|
+
});
|
|
406
|
+
if (state.dots && state.dots !== 'off') {
|
|
407
|
+
const lit = new Set();
|
|
408
|
+
if (state.hover) lit.add(state.hover.laneIndex);
|
|
409
|
+
if (state.rowHover !== null && state.rowHover !== undefined) lit.add(state.rowHover);
|
|
410
|
+
cells.forEach(function (c) {
|
|
411
|
+
if (state.dots === 'hover' && !lit.has(c.laneIndex)) return;
|
|
412
|
+
(c.transitions || []).forEach(function (cell) {
|
|
413
|
+
const a = view.anchor(c.laneIndex, cell);
|
|
414
|
+
const hot = !!state.hotDot && state.hotDot.laneIndex === c.laneIndex && state.hotDot.cell === cell;
|
|
415
|
+
const g = mk('g', 'ed-wave-dot' + (hot ? ' is-hot' : ''), { 'data-lane': c.laneIndex, 'data-cell': cell });
|
|
416
|
+
g.appendChild(mk('circle', 'ed-wave-dot-hit', { cx: a.x, cy: a.y, r: HIT_R, fill: 'transparent' }));
|
|
417
|
+
g.appendChild(mk('circle', 'ed-wave-dot-mark', { cx: a.x, cy: a.y, r: hot ? DOT_R_HOT : DOT_R }));
|
|
418
|
+
layer.appendChild(g);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
950
421
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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();
|
|
422
|
+
(state.labelHits || []).forEach(function (h) {
|
|
423
|
+
layer.appendChild(mk('rect', 'ed-wave-edge-hit ed-wave-edge-label-hit', {
|
|
424
|
+
'data-edge-index': h.index, x: h.x, y: h.y, width: h.width, height: h.height, fill: 'transparent',
|
|
425
|
+
}));
|
|
978
426
|
});
|
|
979
|
-
state.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
}
|
|
427
|
+
(state.loopHits || []).forEach(function (h) {
|
|
428
|
+
layer.appendChild(mk('circle', 'ed-wave-edge-hit ed-wave-edge-loop-hit', {
|
|
429
|
+
'data-edge-index': h.index, cx: h.x, cy: h.y, r: LOOP_HIT_R, fill: 'transparent',
|
|
430
|
+
}));
|
|
431
|
+
});
|
|
432
|
+
if (state.pending) {
|
|
433
|
+
layer.appendChild(mk('line', 'ed-wave-pending', {
|
|
434
|
+
x1: state.pending.from.x, y1: state.pending.from.y, x2: state.pending.to.x, y2: state.pending.to.y,
|
|
435
|
+
}));
|
|
1013
436
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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);
|
|
437
|
+
if (state.selectedEdge) {
|
|
438
|
+
const sel = { d: state.selectedEdge.d };
|
|
439
|
+
if (state.selectedEdge.transform) sel.transform = state.selectedEdge.transform;
|
|
440
|
+
layer.appendChild(mk('path', 'ed-wave-edge-sel', sel));
|
|
441
|
+
['from', 'to'].forEach(function (end) {
|
|
442
|
+
const p = state.selectedEdge[end];
|
|
443
|
+
layer.appendChild(mk('circle', 'ed-wave-edge-end-hit', {
|
|
444
|
+
'data-end': end, cx: p.x, cy: p.y, r: END_HIT_R, fill: 'transparent',
|
|
445
|
+
}));
|
|
446
|
+
layer.appendChild(mk('circle', 'ed-wave-edge-end', { 'data-end': end, cx: p.x, cy: p.y, r: END_R }));
|
|
447
|
+
});
|
|
1051
448
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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]);
|
|
449
|
+
if (state.selectedNote) {
|
|
450
|
+
const n = state.selectedNote;
|
|
451
|
+
layer.appendChild(mk('rect', 'ed-wave-note-sel', { x: n.x, y: n.y, width: n.width, height: n.height, rx: 2 }));
|
|
1070
452
|
}
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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 });
|
|
453
|
+
// Task 8: where a lane dragged by its grip will land (spec section 4.4),
|
|
454
|
+
// a line across the whole layer at that row boundary.
|
|
455
|
+
if (state.dropLine) {
|
|
456
|
+
layer.appendChild(mk('line', 'ed-wave-drop-line', {
|
|
457
|
+
x1: 0, y1: state.dropLine.y, x2: view.width, y2: state.dropLine.y,
|
|
458
|
+
}));
|
|
1118
459
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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;
|
|
460
|
+
(state.overflow || []).forEach(function (o) {
|
|
461
|
+
layer.appendChild(mk('rect', 'ed-wave-overflow', { x: o.left, y: o.top, width: o.width, height: o.height }));
|
|
462
|
+
});
|
|
463
|
+
// the cursor last, over everything else
|
|
464
|
+
if (state.cursor) {
|
|
465
|
+
const r = view.cellRect(state.cursor.laneIndex, state.cursor.cycle);
|
|
466
|
+
const cur = mk('rect', 'ed-wave-cursor', {
|
|
467
|
+
'data-ed-wave-cursor': '', 'data-cell': state.cursor.laneIndex + ',' + state.cursor.cycle,
|
|
468
|
+
x: r.x, y: r.y, width: r.width, height: r.height,
|
|
469
|
+
});
|
|
470
|
+
layer.appendChild(cur);
|
|
1414
471
|
}
|
|
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
472
|
}
|
|
1456
473
|
|
|
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
|
-
};
|
|
474
|
+
return { paint: paint };
|
|
1468
475
|
}
|
|
1469
476
|
|
|
1470
|
-
module.exports = { createDrawer: createDrawer };
|
|
477
|
+
module.exports = { createDrawer: createDrawer, createLayer: createLayer };
|