@helping-ai-workflow/md2doc 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/editor/client.js +142 -18
- package/lib/editor/paste-md.js +5 -1
- package/lib/editor/server.js +36 -2
- package/lib/editor/wave-codec.js +618 -1
- package/lib/editor/wave-geometry.js +183 -0
- package/lib/editor/wave-store.js +28 -1
- package/lib/editor/wave-ui.js +1960 -31
- package/lib/md2doc.js +143 -5
- package/package.json +1 -1
package/lib/editor/wave-ui.js
CHANGED
|
@@ -57,8 +57,50 @@
|
|
|
57
57
|
|
|
58
58
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The brush palette, exactly the set the spec names, in that order.
|
|
62
|
+
*
|
|
63
|
+
* v3.5.0 Task 10 widened this from 11 to 22: `P`/`N` (explicit-edge
|
|
64
|
+
* clocks — see the arrow measurement in `drawLane`'s isClock branch),
|
|
65
|
+
* `2`-`9` (bus levels beyond the plain `=`, already understood by
|
|
66
|
+
* `brickOf` and `codec.levelsOf` but never offered as a brush), and `|`
|
|
67
|
+
* (the gap marker) placed last because it is not a level at all —
|
|
68
|
+
* `codec.levelsOf('=.|')` measured it as "carry the previous level", so
|
|
69
|
+
* painting it does not change what a cycle shows, only that it shows a
|
|
70
|
+
* discontinuity.
|
|
71
|
+
*/
|
|
72
|
+
const BRUSHES = ['0', '1', 'x', 'z', 'p', 'n', 'P', 'N', 'h', 'l', 'u', 'd',
|
|
73
|
+
'=', '2', '3', '4', '5', '6', '7', '8', '9', '|'];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether a `KeyboardEvent.key` value both PICKS and PAINTS a brush —
|
|
77
|
+
* `handleDrawingKey` (below `enterDrawing`) tests this exact call, not a
|
|
78
|
+
* copy of it, so the admission rule and what the keyboard can actually
|
|
79
|
+
* reach cannot drift apart the way the module's own prologue warns about
|
|
80
|
+
* (rule 3: two beliefs about the same thing is the failure this batch
|
|
81
|
+
* keeps producing). Named and exported on its own so a test can pin
|
|
82
|
+
* "every brush is keyboard-reachable" against the real rule instead of a
|
|
83
|
+
* re-implementation of it — see `wave-codec.test.js`'s
|
|
84
|
+
* `isBrushKey`-roster assertion.
|
|
85
|
+
*
|
|
86
|
+
* `key.length === 1` is what actually excludes navigation: every
|
|
87
|
+
* `BRUSHES` entry is one character, while `ArrowLeft`/`ArrowRight`/
|
|
88
|
+
* `ArrowUp`/`ArrowDown`/`Home`/`End` are multi-character key NAMES, so
|
|
89
|
+
* `BRUSHES.indexOf(key)` is already `-1` for all six regardless of this
|
|
90
|
+
* length check — it is kept anyway so that invariant is spelled out at
|
|
91
|
+
* the point of use rather than left to rely on `BRUSHES` never growing a
|
|
92
|
+
* multi-character entry by mistake. Deliberately NOT keyed on
|
|
93
|
+
* `ev.shiftKey`: v3.5.0 Task 10 added `P`/`N` (and `|` sits next to `\`
|
|
94
|
+
* on a US layout) to `BRUSHES`, and all three are typed WITH Shift on an
|
|
95
|
+
* ordinary keyboard, so a shiftKey exclusion here would make them
|
|
96
|
+
* mouse-only — see Task 10's fix-round report for the full reasoning on
|
|
97
|
+
* why dropping it does not reopen the Shift+arrow risk the old comment
|
|
98
|
+
* named (that risk is excluded above, by `nav`, on `key` identity, not on
|
|
99
|
+
* `ev.shiftKey`).
|
|
100
|
+
*/
|
|
101
|
+
function isBrushKey(key) {
|
|
102
|
+
return typeof key === 'string' && key.length === 1 && BRUSHES.indexOf(key) !== -1;
|
|
103
|
+
}
|
|
62
104
|
|
|
63
105
|
/** Drawing sizes. Handed to `wave-geometry` so the hit test and the paint
|
|
64
106
|
* cannot drift; nothing here re-derives a row position. */
|
|
@@ -227,6 +269,29 @@
|
|
|
227
269
|
return out;
|
|
228
270
|
}
|
|
229
271
|
|
|
272
|
+
/**
|
|
273
|
+
* v3.5.0 Task 12: the lowest display position inside `span`'s range whose
|
|
274
|
+
* lane is a DIRECT member of `span`'s own group array — one nested exactly
|
|
275
|
+
* one level deeper than `span.path` — rather than one that lives inside a
|
|
276
|
+
* sub-group `span` merely contains. This is the one lane `codec.ungroupLanes`
|
|
277
|
+
* can be asked about to name THIS group and not some inner one: a lane's
|
|
278
|
+
* immediate parent is unambiguous (see that function's own comment), so a
|
|
279
|
+
* group is addressable through it exactly when it has at least one lane
|
|
280
|
+
* that belongs to it directly.
|
|
281
|
+
*
|
|
282
|
+
* `null` when it does not — a group built entirely out of sub-groups, with
|
|
283
|
+
* no lane of its own at any depth of one. That group's tag still renders
|
|
284
|
+
* (it has a title, and `groupSpansOf` shows every titled one), but nothing
|
|
285
|
+
* in the rail offers to dissolve it, because there is no `at` this codec
|
|
286
|
+
* shape can express that names it and only it.
|
|
287
|
+
*/
|
|
288
|
+
function directFirstLaneOf(layout, span) {
|
|
289
|
+
for (let i = span.from; i <= span.to; i++) {
|
|
290
|
+
if (layout.lanes[i].path.length === span.path.length + 1) return i;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
230
295
|
function valueAt(doc, path) {
|
|
231
296
|
let node = doc;
|
|
232
297
|
for (const seg of path) {
|
|
@@ -321,6 +386,102 @@
|
|
|
321
386
|
// never end up pointing at different cells: painting with `1` after a click
|
|
322
387
|
// paints the cell that was clicked.
|
|
323
388
|
let cursor = null;
|
|
389
|
+
// The `edge` array entry the inspector row edits — the codec's ORIGINAL
|
|
390
|
+
// index into `doc.edge`, exactly what `geometry.edgeLayout` reports as
|
|
391
|
+
// `index` (NOT the filtered array position; a dangling entry ahead of it
|
|
392
|
+
// is skipped there and would otherwise shift everything after it out of
|
|
393
|
+
// step). Null means no edge is selected, and that is the whole of the
|
|
394
|
+
// inspector row's visibility rule — see `renderEdgeBar`. `render()`
|
|
395
|
+
// clamps it to null the moment the edge it names stops being drawable
|
|
396
|
+
// (deleted, or its entry no longer parses), so a stale index can never
|
|
397
|
+
// reach `renderEdgeBar` or a shape/label commit.
|
|
398
|
+
let selectedEdge = null;
|
|
399
|
+
// v3.5.0 Task 7: the edge-CREATION gesture's own state, shared verbatim
|
|
400
|
+
// by the pointer and the keyboard — one state machine, two input sources.
|
|
401
|
+
// `edgeMode` is explicit rather than inferred from "is a key held" or "is
|
|
402
|
+
// the mouse button down", for the same reason the brush canvas keeps
|
|
403
|
+
// `drag` as its own variable instead of asking `ev.buttons`: a painting
|
|
404
|
+
// drag and an edge-creation drag must never be read as each other, and a
|
|
405
|
+
// named mode is what lets `onCanvasDown` choose between them up front.
|
|
406
|
+
// 'idle' — the 關聯線 button is off; every press paints or selects.
|
|
407
|
+
// 'armed' — pressed once; the NEXT thing (a press, or a cell plus
|
|
408
|
+
// Enter) marks the start of a line.
|
|
409
|
+
// 'dragging' — the mouse is down after that press; every move only
|
|
410
|
+
// repaints a preview, the document is untouched.
|
|
411
|
+
// `pendingFrom` is the marked start cell, `{at, cell}` in the same two
|
|
412
|
+
// numbers `codec.ensureNode`/`geometry.cellRect` already take — the
|
|
413
|
+
// codec's flattened lane index and the cycle inside it. It is cleared
|
|
414
|
+
// only when `setEdgeMode` moves all the way back to 'idle': going back to
|
|
415
|
+
// 'armed' after a drop that produced nothing (rule 1 below) deliberately
|
|
416
|
+
// LEAVES it standing, so a keyboard user who marked a start and then
|
|
417
|
+
// pressed Enter over an empty row does not have to re-mark it — the next
|
|
418
|
+
// Enter still means "finish from where I already stood".
|
|
419
|
+
let edgeMode = 'idle';
|
|
420
|
+
let pendingFrom = null;
|
|
421
|
+
/** The one place `edgeMode` changes. Keeps the arm button's own visual
|
|
422
|
+
* state, the overlay's `data-wave-edgemode` (what a test or a future
|
|
423
|
+
* task reads instead of reaching into this closure) and the "is a start
|
|
424
|
+
* cell marked" invariant all moving together. */
|
|
425
|
+
function setEdgeMode(next) {
|
|
426
|
+
edgeMode = next;
|
|
427
|
+
edgeBtn.setAttribute('aria-pressed', next === 'idle' ? 'false' : 'true');
|
|
428
|
+
edgeBtn.classList.toggle('is-on', next !== 'idle');
|
|
429
|
+
overlay.setAttribute('data-wave-edgemode', next);
|
|
430
|
+
if (next === 'idle') pendingFrom = null;
|
|
431
|
+
}
|
|
432
|
+
// v3.5.0 Task 8: dragging either END of the SELECTED edge to another
|
|
433
|
+
// cell. `{index, end, at, cell} | null` — `index` is the same original
|
|
434
|
+
// `doc.edge` index `selectedEdge` names (a drag can only start from that
|
|
435
|
+
// edge's own drawn handle, see `onCanvasDown`), `end` is `'from'` or
|
|
436
|
+
// `'to'`, and `at`/`cell` are the live drop target, `null` until the
|
|
437
|
+
// first `pointermove` lands on a real cell.
|
|
438
|
+
//
|
|
439
|
+
// Unlike `edgeMode`/`pendingFrom` — two separate variables that Task 7
|
|
440
|
+
// spent three fix rounds re-synchronising after one could go stale while
|
|
441
|
+
// the other stood — this is ONE variable that is both "is a drag
|
|
442
|
+
// happening" and "what does it know so far". There is no second flag to
|
|
443
|
+
// fall out of step with it: `onCanvasMove`/`onCanvasUp` test this value
|
|
444
|
+
// itself, so nulling it is the whole of cancelling the gesture, in one
|
|
445
|
+
// place, atomically. It still has to be threaded through the same two
|
|
446
|
+
// places Task 7's marks are, though, because the hazard those rounds
|
|
447
|
+
// found is not particular to `pendingFrom` — it is "a mark naming a lane
|
|
448
|
+
// outlives the document that lane numbering was valid in":
|
|
449
|
+
// - `carryMarks` below carries `at` by lane IDENTITY, exactly like
|
|
450
|
+
// `pendingFrom.at`, for the same reason: a structural undo/redo can
|
|
451
|
+
// land while this drag is in flight (the keyboard stays live while a
|
|
452
|
+
// mouse button is held — Ctrl+Z does not care what the mouse is
|
|
453
|
+
// doing), and a raw row number surviving that would let the eventual
|
|
454
|
+
// drop's `moveEdgeEnd` bounds-check successfully against a lane the
|
|
455
|
+
// user was never actually pointing at.
|
|
456
|
+
// - `render()`'s own `selectedEdge` clamp is widened to drop this too:
|
|
457
|
+
// `index` can only ever be reached from `selectedEdge`'s own drawn
|
|
458
|
+
// handle, so the two may never disagree — a drag whose edge stops
|
|
459
|
+
// being drawable (deleted, or its selection cleared by that same
|
|
460
|
+
// mid-drag undo) has to die with it rather than commit a move onto
|
|
461
|
+
// whatever entry now happens to sit at that array position.
|
|
462
|
+
let endpointDrag = null;
|
|
463
|
+
// v3.5.0 Task 12: which lane rows are Shift+click multi-selected on the
|
|
464
|
+
// rail, for「建立群組」— an array of display-position integers, in
|
|
465
|
+
// whatever order the clicks added them, or `null` when nothing is
|
|
466
|
+
// picked (never `[]`; the empty case is collapsed to `null` the moment
|
|
467
|
+
// the last member is toggled off, so every other mark's "is anything
|
|
468
|
+
// standing" check — a plain `!== null` — stays true of this one too).
|
|
469
|
+
//
|
|
470
|
+
// This is a fourth mark of the exact family `pendingFrom`/`endpointDrag`
|
|
471
|
+
// already are: a set of lanes the user has committed to for an upcoming
|
|
472
|
+
// STRUCTURAL edit, named by row number, which a reorder or an undo/redo
|
|
473
|
+
// can invalidate out from under it. So it is carried the same way —
|
|
474
|
+
// widening `carryMarks`'s guard and its `rowOf` lookup rather than
|
|
475
|
+
// growing a second belief about lane identity — and dropped the same
|
|
476
|
+
// way: an entry that cannot be traced to a surviving lane is removed
|
|
477
|
+
// from the set rather than left pointing at whatever now sits at that
|
|
478
|
+
// row. `cursor`'s "leave the stale number, let `renderCanvas` clamp it"
|
|
479
|
+
// is the wrong instinct here for the reason `pendingFrom`'s own comment
|
|
480
|
+
// gives — this mark feeds a structural op (`groupLanes`) the moment the
|
|
481
|
+
// button is pressed, not a screen position, and a stale member silently
|
|
482
|
+
// renumbering onto an unrelated lane would group a lane the user never
|
|
483
|
+
// selected with nothing on screen saying so.
|
|
484
|
+
let laneMultiSelect = null;
|
|
324
485
|
// The document the marks' lane numbers are numbers INTO.
|
|
325
486
|
//
|
|
326
487
|
// `cursor` and `selection` name a ROW, and a row number means a different
|
|
@@ -344,6 +505,14 @@
|
|
|
344
505
|
// a rename field ran `close('escape')` and threw the WHOLE session away,
|
|
345
506
|
// which is the most expensive answer available to「我改名改到一半不想改了」.
|
|
346
507
|
let groupRename = null;
|
|
508
|
+
// v3.5.0 Task 11: the bus data-label field while it is open — this
|
|
509
|
+
// dialog's OTHER sub-panel, same shape as `groupRename` above (a
|
|
510
|
+
// `{cancel, retire}` pair) and layered the same way in `onKeyDown`'s
|
|
511
|
+
// Escape handling. Unlike the rail's rename field, this one's node lives
|
|
512
|
+
// beside the canvas SVG that `renderCanvas` rebuilds on every cursor
|
|
513
|
+
// move, not only on a structural edit — so `retire()` is called from
|
|
514
|
+
// THERE, not only from `render()` (see `renderCanvas`'s own first line).
|
|
515
|
+
let dataEdit = null;
|
|
347
516
|
|
|
348
517
|
// ── the modal focus model ─────────────────────────────────────────────
|
|
349
518
|
//
|
|
@@ -461,6 +630,7 @@
|
|
|
461
630
|
if (closed) return;
|
|
462
631
|
closed = true;
|
|
463
632
|
drag = null;
|
|
633
|
+
endpointDrag = null;
|
|
464
634
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
465
635
|
d.removeEventListener('keydown', onKeyDown, true);
|
|
466
636
|
d.removeEventListener('mousemove', onCanvasMove, true);
|
|
@@ -530,6 +700,27 @@
|
|
|
530
700
|
// user had made, and the one Ctrl+Z that takes a discard back is not
|
|
531
701
|
// something the keystroke announces.
|
|
532
702
|
if (groupRename !== null) { groupRename.cancel(); return; }
|
|
703
|
+
// v3.5.0 Task 11: the bus data-label field, this dialog's OTHER
|
|
704
|
+
// text sub-panel — same layering reason as `groupRename` just
|
|
705
|
+
// above: without this, Escape typed while labelling a bus value
|
|
706
|
+
// discarded the whole editing session instead of just the field.
|
|
707
|
+
if (dataEdit !== null) { dataEdit.cancel(); return; }
|
|
708
|
+
// v3.5.0 Task 7: the armed/dragging edge gesture is this dialog's
|
|
709
|
+
// OTHER sub-panel-shaped thing — not a text field, but still a
|
|
710
|
+
// half-done action standing in front of the dialog's own Escape.
|
|
711
|
+
// Cancelling it never touches the document (rule 2: nothing was
|
|
712
|
+
// written yet), so there is no undo step to leave behind either.
|
|
713
|
+
if (edgeMode !== 'idle') { setEdgeMode('idle'); render(); return; }
|
|
714
|
+
// v3.5.0 Task 8: a third sub-panel-shaped thing, same rule — the
|
|
715
|
+
// document is untouched during an endpoint drag (rule 3), so
|
|
716
|
+
// cancelling it is nulling the one variable that both is and knows
|
|
717
|
+
// the drag, then a repaint. The mousemove/mouseup listeners are left
|
|
718
|
+
// attached exactly as `edgeMode`'s own cancel above leaves them:
|
|
719
|
+
// `onCanvasUp` unconditionally removes both at its very first line
|
|
720
|
+
// whenever the button eventually comes up, so a stray move in
|
|
721
|
+
// between finds `endpointDrag === null`, falls through every other
|
|
722
|
+
// branch and repaints nothing — no listener leak, no second rule.
|
|
723
|
+
if (endpointDrag !== null) { endpointDrag = null; render(); return; }
|
|
533
724
|
close('escape');
|
|
534
725
|
return;
|
|
535
726
|
}
|
|
@@ -756,6 +947,57 @@
|
|
|
756
947
|
afterStoreMoved('redo');
|
|
757
948
|
});
|
|
758
949
|
|
|
950
|
+
// v3.5.0 Task 12: wrap the current Shift+click multi-select (see
|
|
951
|
+
// `laneMultiSelect` above, and `laneRow` below for the gesture itself)
|
|
952
|
+
// into one new group. The codec is the one that decides contiguity —
|
|
953
|
+
// this button does not pre-filter what is selectable, it only decides
|
|
954
|
+
// whether there is enough of a selection to bother trying, so the
|
|
955
|
+
// refusal message the user actually needed ("these lanes are not next
|
|
956
|
+
// to each other") comes from `commit`'s own status line rather than
|
|
957
|
+
// being silently swallowed here.
|
|
958
|
+
//
|
|
959
|
+
// The group is created with an EMPTY label and the rename field is
|
|
960
|
+
// opened immediately — the same affordance `insertButton` already gives
|
|
961
|
+
// a brand new lane (empty `name`, keyboard straight into the field) —
|
|
962
|
+
// rather than a placeholder string the user has to notice and clear.
|
|
963
|
+
const groupBtn = toolButton('ed-wave-group-make', '建立群組',
|
|
964
|
+
'把左邊用 Shift+點 選起來的那幾條連續 lane 包成一個新群組', function () {
|
|
965
|
+
if (laneMultiSelect === null || laneMultiSelect.length < 2) {
|
|
966
|
+
say('先在左邊用 Shift+點 連續選至少兩條 lane,才有東西可以群組');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const ats = laneMultiSelect.slice().sort(function (a, b) { return a - b; });
|
|
970
|
+
const minAt = ats[0];
|
|
971
|
+
const committed = commit('group-lanes', function (doc2) {
|
|
972
|
+
return codec.groupLanes(doc2, ats, '');
|
|
973
|
+
});
|
|
974
|
+
if (!committed) {
|
|
975
|
+
say('這幾條 lane 不連續(或選取已經失效),WaveJSON 的群組只能是連續的一段');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
laneMultiSelect = null;
|
|
979
|
+
// The commit above already repainted with the (now stale) highlight
|
|
980
|
+
// showing; one more repaint clears it before handing the keyboard to
|
|
981
|
+
// the rename field, the same two-step `renameGroup` itself already
|
|
982
|
+
// is — a rebuild first, then the sub-panel laid over what it built.
|
|
983
|
+
render();
|
|
984
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
985
|
+
const spans = groupSpansOf(store.doc, layout);
|
|
986
|
+
for (const span of spans) {
|
|
987
|
+
if (span.from === minAt) { renameGroup(span); break; }
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// v3.5.0 Task 7: arm the edge-creation gesture. One-shot in the sense
|
|
992
|
+
// that a drag which PRODUCES an edge disarms it (`finishEdgeDrag` below);
|
|
993
|
+
// pressing the button itself is a plain toggle, so a change of mind
|
|
994
|
+
// before ever pressing on the canvas costs one more click, not Escape.
|
|
995
|
+
const edgeBtn = toolButton('ed-wave-edge-arm', '關聯線',
|
|
996
|
+
'按一下武裝,然後從一格拖到另一格畫出關聯線(Escape 取消)', function () {
|
|
997
|
+
setEdgeMode(edgeMode === 'idle' ? 'armed' : 'idle');
|
|
998
|
+
});
|
|
999
|
+
setEdgeMode('idle');
|
|
1000
|
+
|
|
759
1001
|
// head / foot — the document-level fields this drawing can honour.
|
|
760
1002
|
//
|
|
761
1003
|
// `config.hscale` USED to be a third field here and has been removed. The
|
|
@@ -802,9 +1044,139 @@
|
|
|
802
1044
|
|
|
803
1045
|
const headInput = metaField('ed-wave-head-text', 'head', '上方標題');
|
|
804
1046
|
const footInput = metaField('ed-wave-foot-text', 'foot', '下方標題');
|
|
1047
|
+
// v3.5.0 Task 13, fix round 1: the ruler fields — the COMPLETE set, not
|
|
1048
|
+
// the three Task 13's own brief happened to name. Measured directly
|
|
1049
|
+
// against the pinned engine rather than recalled: `renderMarks` in
|
|
1050
|
+
// `node_modules/wavedrom/lib/render-marks.js` calls
|
|
1051
|
+
// `ticktock(lane, 'head', 'tick', ...)`, `ticktock(lane, 'head', 'tock',
|
|
1052
|
+
// ...)`, `ticktock(lane, 'foot', 'tick', ...)` and
|
|
1053
|
+
// `ticktock(lane, 'foot', 'tock', ...)` — all four, not just
|
|
1054
|
+
// `head.tick`/`foot.tock` — and inside `ticktock` itself,
|
|
1055
|
+
// `cxt[ref1].every` is read off whichever of `head`/`foot` `ref1` is, so
|
|
1056
|
+
// `every` applies under BOTH, not only `head`. That is eight keys —
|
|
1057
|
+
// `{head, foot} × {text, tick, tock, every}` — of which the first Task
|
|
1058
|
+
// 13 round shipped five (`head.text`/`foot.text` from v3.4.0, plus
|
|
1059
|
+
// `head.tick`/`head.every`/`foot.tock`). The three missing ones
|
|
1060
|
+
// (`head.tock`/`foot.tick`/`foot.every`) are added here so the set is
|
|
1061
|
+
// complete rather than an arbitrary subset a user cannot predict — "why
|
|
1062
|
+
// can I put a tick above the diagram but not below it" is exactly the
|
|
1063
|
+
// shape of bug five-of-eight invites.
|
|
1064
|
+
//
|
|
1065
|
+
// Labels are the literal WaveJSON PATH (`head.tick`, not bare `tick`),
|
|
1066
|
+
// not just the leaf key the first round used — with only one of
|
|
1067
|
+
// tick/tock/every ever existing under `head` (and the mirror one under
|
|
1068
|
+
// `foot`), a bare leaf label could not be confused for its sibling; with
|
|
1069
|
+
// all four now under both, `tick` alone would leave two fields on screen
|
|
1070
|
+
// both labelled `tick` with no way to tell which of them draws above the
|
|
1071
|
+
// diagram and which below. `head`/`foot` on the two `.text` fields stay
|
|
1072
|
+
// bare, since those never had a same-named sibling to begin with.
|
|
1073
|
+
// Grouped head-then-foot in DOM order (four fields, then four fields)
|
|
1074
|
+
// rather than the first round's ad hoc growth order, for the same
|
|
1075
|
+
// readability reason.
|
|
1076
|
+
const headTickInput = metaField('ed-wave-head-tick', 'head.tick', '起始刻度,例如 0');
|
|
1077
|
+
const headTockInput = metaField('ed-wave-head-tock', 'head.tock', '半格刻度起始值,例如 0');
|
|
1078
|
+
const headEveryInput = metaField('ed-wave-head-every', 'head.every', '每幾格顯示一次刻度');
|
|
1079
|
+
const footTickInput = metaField('ed-wave-foot-tick', 'foot.tick', '下方起始刻度,例如 0');
|
|
1080
|
+
const footTockInput = metaField('ed-wave-foot-tock', 'foot.tock', '下方半格刻度起始值,例如 0');
|
|
1081
|
+
const footEveryInput = metaField('ed-wave-foot-every', 'foot.every', '下方每幾格顯示一次刻度');
|
|
1082
|
+
|
|
1083
|
+
headInput.addEventListener('change', function () { commitBanner('head', 'text', headInput.value); });
|
|
1084
|
+
footInput.addEventListener('change', function () { commitBanner('foot', 'text', footInput.value); });
|
|
1085
|
+
headTickInput.addEventListener('change', function () { commitBanner('head', 'tick', headTickInput.value); });
|
|
1086
|
+
headTockInput.addEventListener('change', function () {
|
|
1087
|
+
commitBanner('head', 'tock', headTockInput.value);
|
|
1088
|
+
});
|
|
1089
|
+
headEveryInput.addEventListener('change', function () { commitBanner('head', 'every', headEveryInput.value); });
|
|
1090
|
+
footTickInput.addEventListener('change', function () {
|
|
1091
|
+
commitBanner('foot', 'tick', footTickInput.value);
|
|
1092
|
+
});
|
|
1093
|
+
footTockInput.addEventListener('change', function () { commitBanner('foot', 'tock', footTockInput.value); });
|
|
1094
|
+
footEveryInput.addEventListener('change', function () {
|
|
1095
|
+
commitBanner('foot', 'every', footEveryInput.value);
|
|
1096
|
+
});
|
|
805
1097
|
|
|
806
|
-
|
|
807
|
-
|
|
1098
|
+
// ── the edge inspector row ──────────────────────────────────────────
|
|
1099
|
+
//
|
|
1100
|
+
// Same rule as `renderUnmodelled` below: HIDDEN, with no box on screen at
|
|
1101
|
+
// all, unless `selectedEdge` names one — `renderEdgeBar` is what flips
|
|
1102
|
+
// `edgeBar.hidden` and it is only ever called from `render()`, so the two
|
|
1103
|
+
// can never disagree. Built once here, like `headInput`/`footInput`
|
|
1104
|
+
// above; `render()` repaints its VALUES on every pass through
|
|
1105
|
+
// `renderEdgeBar`, and the usual `data-focus-key` / `restoreFocus` pair
|
|
1106
|
+
// is what keeps a half-typed label from losing the caret on that repaint.
|
|
1107
|
+
const edgeBar = d.createElement('div');
|
|
1108
|
+
edgeBar.className = 'ed-wave-edgebar';
|
|
1109
|
+
edgeBar.hidden = true;
|
|
1110
|
+
toolbar.appendChild(edgeBar);
|
|
1111
|
+
|
|
1112
|
+
const shapeWrap = d.createElement('label');
|
|
1113
|
+
shapeWrap.className = 'ed-wave-metafield';
|
|
1114
|
+
const shapeWrapText = d.createElement('span');
|
|
1115
|
+
shapeWrapText.textContent = '形狀';
|
|
1116
|
+
shapeWrap.appendChild(shapeWrapText);
|
|
1117
|
+
const shapeSel = d.createElement('select');
|
|
1118
|
+
shapeSel.className = 'ed-wave-edge-shape';
|
|
1119
|
+
shapeSel.setAttribute('data-focus-key', 'ed-wave-edge-shape');
|
|
1120
|
+
for (const s of codec.EDGE_SHAPES) {
|
|
1121
|
+
const o = d.createElement('option');
|
|
1122
|
+
o.value = s;
|
|
1123
|
+
o.textContent = s;
|
|
1124
|
+
shapeSel.appendChild(o);
|
|
1125
|
+
}
|
|
1126
|
+
shapeSel.addEventListener('change', function () {
|
|
1127
|
+
if (selectedEdge === null) return;
|
|
1128
|
+
commit('edge-shape', function (doc) {
|
|
1129
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1130
|
+
if (cur === null) return doc;
|
|
1131
|
+
return codec.updateEdge(doc, selectedEdge,
|
|
1132
|
+
{ from: cur.from, to: cur.to, shape: shapeSel.value, label: cur.label });
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
shapeWrap.appendChild(shapeSel);
|
|
1136
|
+
edgeBar.appendChild(shapeWrap);
|
|
1137
|
+
|
|
1138
|
+
const edgeLabelWrap = d.createElement('label');
|
|
1139
|
+
edgeLabelWrap.className = 'ed-wave-metafield';
|
|
1140
|
+
const edgeLabelWrapText = d.createElement('span');
|
|
1141
|
+
edgeLabelWrapText.textContent = '標籤';
|
|
1142
|
+
edgeLabelWrap.appendChild(edgeLabelWrapText);
|
|
1143
|
+
const edgeLabelInput = d.createElement('input');
|
|
1144
|
+
edgeLabelInput.type = 'text';
|
|
1145
|
+
edgeLabelInput.className = 'ed-wave-edge-label';
|
|
1146
|
+
edgeLabelInput.setAttribute('data-focus-key', 'ed-wave-edge-label');
|
|
1147
|
+
edgeLabelInput.placeholder = '這條線的標籤';
|
|
1148
|
+
edgeLabelInput.addEventListener('change', function () {
|
|
1149
|
+
if (selectedEdge === null) return;
|
|
1150
|
+
commit('edge-label', function (doc) {
|
|
1151
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1152
|
+
if (cur === null) return doc;
|
|
1153
|
+
return codec.updateEdge(doc, selectedEdge,
|
|
1154
|
+
{ from: cur.from, to: cur.to, shape: cur.shape, label: edgeLabelInput.value });
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
edgeLabelWrap.appendChild(edgeLabelInput);
|
|
1158
|
+
edgeBar.appendChild(edgeLabelWrap);
|
|
1159
|
+
|
|
1160
|
+
const edgeDeleteBtn = d.createElement('button');
|
|
1161
|
+
edgeDeleteBtn.type = 'button';
|
|
1162
|
+
edgeDeleteBtn.className = 'ed-wave-edge-delete';
|
|
1163
|
+
edgeDeleteBtn.setAttribute('data-focus-key', 'ed-wave-edge-delete');
|
|
1164
|
+
edgeDeleteBtn.textContent = '刪除這條線';
|
|
1165
|
+
edgeDeleteBtn.title = '刪掉這條關聯線(連帶清掉沒有其他 edge 再用的 anchor 字母)';
|
|
1166
|
+
edgeDeleteBtn.addEventListener('click', function () {
|
|
1167
|
+
if (selectedEdge === null) return;
|
|
1168
|
+
const at = selectedEdge;
|
|
1169
|
+
// Cleared BEFORE `commit`, not after: `commit` repaints synchronously,
|
|
1170
|
+
// and `removeEdge` shifts every later entry's index down by one — so an
|
|
1171
|
+
// index cleared only after `commit` returns would let `render()`'s
|
|
1172
|
+
// clamp see a DIFFERENT edge that happens to have landed on the same
|
|
1173
|
+
// number, and silently keep the inspector open on it.
|
|
1174
|
+
selectedEdge = null;
|
|
1175
|
+
commit('edge-remove', function (doc) {
|
|
1176
|
+
return codec.pruneNodes(codec.removeEdge(doc, at));
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
edgeBar.appendChild(edgeDeleteBtn);
|
|
808
1180
|
|
|
809
1181
|
// The "this drawing is not showing you everything" notice. Empty, and with
|
|
810
1182
|
// no box on screen, for the documents that carry none of the three.
|
|
@@ -822,15 +1194,34 @@
|
|
|
822
1194
|
noSignal.hidden = true;
|
|
823
1195
|
toolbar.appendChild(noSignal);
|
|
824
1196
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1197
|
+
// v3.5.0 Task 13: widened from `(which, value)` — always writing `.text`
|
|
1198
|
+
// — to `(which, key, value)` so the same function also carries the other
|
|
1199
|
+
// five ruler fields.
|
|
1200
|
+
//
|
|
1201
|
+
// Fix round 2: the write itself moved into `codec.setBannerField` rather
|
|
1202
|
+
// than staying inline here. The inline version's own no-op guard —
|
|
1203
|
+
// `value === '' && (hadObj[key] === undefined || hadObj[key] === null)`
|
|
1204
|
+
// — only refused to WRITE when the key had never been set; once a key
|
|
1205
|
+
// WAS set, clearing the field fell through to `next[which][key] = ''`,
|
|
1206
|
+
// an empty string rather than an absent key. `.text` hid that: the
|
|
1207
|
+
// engine's `captext` treats `''` the same as absent (a plain truthy
|
|
1208
|
+
// check). `tick`/`tock`/`every` do not — `ticktock` in
|
|
1209
|
+
// `node_modules/wavedrom/lib/render-marks.js` only skips a field that is
|
|
1210
|
+
// `=== undefined`, so `''` reached its array branch, `Number('')` is
|
|
1211
|
+
// `0`, and the engine drew a full default 0-based ruler on a field that
|
|
1212
|
+
// LOOKED empty on screen — the exact confident-wrong-state class this
|
|
1213
|
+
// file exists to avoid, reintroduced by the write side after the read
|
|
1214
|
+
// side (`tickTockText`) got it right. `setBannerField` deletes the key
|
|
1215
|
+
// (and the parent object, if that empties it) instead of writing `''`,
|
|
1216
|
+
// closing that gap once for all six fields rather than special-casing
|
|
1217
|
+
// `.text`'s accidental safety.
|
|
1218
|
+
//
|
|
1219
|
+
// This also gives these six fields their first test seam:
|
|
1220
|
+
// `commitBanner` itself is a closure with no unit-test access, but
|
|
1221
|
+
// `setBannerField` is a plain, exported, pure function — pinned in
|
|
1222
|
+
// `test/wave-codec.test.js`.
|
|
1223
|
+
function commitBanner(which, key, value) {
|
|
1224
|
+
commit(which + '.' + key, function (doc) { return codec.setBannerField(doc, which, key, value); });
|
|
834
1225
|
}
|
|
835
1226
|
|
|
836
1227
|
// ── body: lane list | drawing | preview ───────────────────────────────
|
|
@@ -932,10 +1323,55 @@
|
|
|
932
1323
|
if (marksDoc === nextDoc) return;
|
|
933
1324
|
const before = marksDoc;
|
|
934
1325
|
marksDoc = nextDoc;
|
|
935
|
-
|
|
1326
|
+
// Fix round 2: `pendingFrom` joins the guard's own "is there anything
|
|
1327
|
+
// to carry" question — widened rather than given a second guard of
|
|
1328
|
+
// its own, so there stays ONE call to `opts.laneTrace` and ONE
|
|
1329
|
+
// `rowOf` built from it, not a second belief about the lookup living
|
|
1330
|
+
// next to the first. Reachable with `cursor`/`selection` both null:
|
|
1331
|
+
// the keyboard's `armed` state is not locked to a document capture
|
|
1332
|
+
// the way a mouse drag is, so a user can mark a start cell with
|
|
1333
|
+
// Enter, delete every lane from the toolbar (which nulls `cursor`/
|
|
1334
|
+
// `selection` in `renderCanvas`'s own clamp but left `pendingFrom`
|
|
1335
|
+
// alone before this fix), add lanes back past the stale index, and
|
|
1336
|
+
// finish the gesture — silently anchoring an edge to a lane the
|
|
1337
|
+
// trace never actually confirmed, because the lookup below was
|
|
1338
|
+
// skipped entirely while `pendingFrom` was the only mark still
|
|
1339
|
+
// standing. `ensureNode` bounds-checks against whatever document is
|
|
1340
|
+
// in hand at THAT moment, not against the one `pendingFrom` was
|
|
1341
|
+
// marked in, so a stale-but-back-in-range index is silently valid to
|
|
1342
|
+
// it — this file's worst failure class, wrong data written with
|
|
1343
|
+
// nothing on screen that explains it.
|
|
1344
|
+
// v3.5.0 Task 8: `endpointDrag` widens this same guard rather than
|
|
1345
|
+
// getting one of its own, for the identical reason `pendingFrom` did
|
|
1346
|
+
// in fix round 2 — one call to `opts.laneTrace`, one `rowOf`, not a
|
|
1347
|
+
// second belief about the lookup. Reachable with the other three all
|
|
1348
|
+
// null: a keyboard undo/redo reached WHILE a mouse button is down
|
|
1349
|
+
// dragging an endpoint is exactly the same shape of gap `pendingFrom`
|
|
1350
|
+
// closed — the pointer holds no document capture of its own, only
|
|
1351
|
+
// this variable's `at` does.
|
|
1352
|
+
// v3.5.0 Task 12: `laneMultiSelect` joins the same guard for the same
|
|
1353
|
+
// reason again — a Shift+click multi-select standing while an unrelated
|
|
1354
|
+
// undo/redo or move lands underneath it is exactly this shape of gap,
|
|
1355
|
+
// just with a SET of row numbers instead of one.
|
|
1356
|
+
if (before === null ||
|
|
1357
|
+
(cursor === null && selection === null && pendingFrom === null &&
|
|
1358
|
+
endpointDrag === null && laneMultiSelect === null)) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
936
1361
|
const trace = typeof opts.laneTrace === 'function'
|
|
937
1362
|
? opts.laneTrace(before, nextDoc) : null;
|
|
938
|
-
|
|
1363
|
+
// No `laneTrace` injected: "I no longer know where you were" applies
|
|
1364
|
+
// to `pendingFrom` exactly as it already does to `cursor`/`selection`
|
|
1365
|
+
// — dropped, not guessed at. `endpointDrag` joins them: a drag naming
|
|
1366
|
+
// a lane nothing here can still vouch for is the same hazard either
|
|
1367
|
+
// way. `laneMultiSelect` is dropped wholesale here too, the same as
|
|
1368
|
+
// every other mark: with no trace at all there is nothing to check
|
|
1369
|
+
// any of its members against.
|
|
1370
|
+
if (trace === null) {
|
|
1371
|
+
cursor = null; selection = null; pendingFrom = null; endpointDrag = null;
|
|
1372
|
+
laneMultiSelect = null;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
939
1375
|
// Structural or not: a content operation traces as the identity, so this
|
|
940
1376
|
// is a no-op for a paint without having to know that it is one.
|
|
941
1377
|
const rowOf = function (was) {
|
|
@@ -954,6 +1390,64 @@
|
|
|
954
1390
|
selection = { laneIndex: at, from: selection.from, to: selection.to };
|
|
955
1391
|
}
|
|
956
1392
|
}
|
|
1393
|
+
// `pendingFrom` is a mark of the exact same shape as `cursor` (a row
|
|
1394
|
+
// number plus a cycle), carried the same way — by lane IDENTITY
|
|
1395
|
+
// through this same trace, not by raw row number (see the guard
|
|
1396
|
+
// above for why it must reach here even when `cursor`/`selection`
|
|
1397
|
+
// cannot). It deliberately does NOT copy cursor's "leave the number
|
|
1398
|
+
// alone when untraceable" half, though: `cursor` gets away with that
|
|
1399
|
+
// because `renderCanvas` re-clamps it into range on every repaint, and
|
|
1400
|
+
// because a cursor landing on "whatever now occupies that visual
|
|
1401
|
+
// position" after its own lane vanished is the right instinct for a
|
|
1402
|
+
// position marker. `pendingFrom` has no such clamp anywhere, and it is
|
|
1403
|
+
// not a position — it is a lane the user already committed to as one
|
|
1404
|
+
// fixed end of an edge. Fix round 2: leaving it at a stale raw index
|
|
1405
|
+
// is exactly the hazard the coordinator's review walked to the end —
|
|
1406
|
+
// delete the marked lane, add unrelated lanes back until the count
|
|
1407
|
+
// passes the stale number again, and `ensureNode` would bounds-check
|
|
1408
|
+
// successfully against a lane the user never chose. So an untraceable
|
|
1409
|
+
// `pendingFrom` is DROPPED here, the same as `trace === null` above
|
|
1410
|
+
// drops it wholesale — never left standing on a number nothing
|
|
1411
|
+
// vouches for.
|
|
1412
|
+
if (pendingFrom !== null) {
|
|
1413
|
+
const at = rowOf(pendingFrom.at);
|
|
1414
|
+
pendingFrom = at === null ? null : { at: at, cell: pendingFrom.cell };
|
|
1415
|
+
}
|
|
1416
|
+
// `endpointDrag.at` is a mark of the exact same shape as
|
|
1417
|
+
// `pendingFrom.at` — a lane the user is currently pointing a live
|
|
1418
|
+
// gesture at, not a position `renderCanvas` re-clamps on every
|
|
1419
|
+
// repaint — so it is carried the same way and dropped the same way
|
|
1420
|
+
// when the lane it named cannot be traced. Left alone while `at` is
|
|
1421
|
+
// still `null` (mousedown fired, no `pointermove` has landed on a
|
|
1422
|
+
// real cell yet): there is no row number there yet to go stale.
|
|
1423
|
+
// `index`/`end`/`cell` are not row numbers and need no retracing
|
|
1424
|
+
// here — `index` is guarded separately by `render()`'s widened
|
|
1425
|
+
// `selectedEdge` clamp (the drag dies with the selection it drags).
|
|
1426
|
+
if (endpointDrag !== null && endpointDrag.at !== null) {
|
|
1427
|
+
const at = rowOf(endpointDrag.at);
|
|
1428
|
+
endpointDrag = at === null ? null
|
|
1429
|
+
: { index: endpointDrag.index, end: endpointDrag.end, at: at, cell: endpointDrag.cell };
|
|
1430
|
+
}
|
|
1431
|
+
// `laneMultiSelect` is a SET of marks of the same shape as
|
|
1432
|
+
// `pendingFrom.at` — each one a lane the user has already committed to
|
|
1433
|
+
// for the group they are about to build — carried and dropped the same
|
|
1434
|
+
// way, member by member rather than all-or-nothing: a reorder that
|
|
1435
|
+
// only invalidates ONE of several selected rows should not throw away
|
|
1436
|
+
// the rest of a selection the user is still building. An entry that
|
|
1437
|
+
// cannot be traced is removed rather than left on a stale number, for
|
|
1438
|
+
// the identical reason `pendingFrom` never keeps one either — a
|
|
1439
|
+
// structural op reads this set directly, not a screen position
|
|
1440
|
+
// `renderCanvas` re-clamps on every repaint. Emptied down to nothing,
|
|
1441
|
+
// it collapses to `null` so every other guard's plain `!== null` check
|
|
1442
|
+
// stays the whole truth about "is anything still selected".
|
|
1443
|
+
if (laneMultiSelect !== null) {
|
|
1444
|
+
const kept = [];
|
|
1445
|
+
for (let m = 0; m < laneMultiSelect.length; m++) {
|
|
1446
|
+
const at = rowOf(laneMultiSelect[m]);
|
|
1447
|
+
if (at !== null) kept.push(at);
|
|
1448
|
+
}
|
|
1449
|
+
laneMultiSelect = kept.length === 0 ? null : kept;
|
|
1450
|
+
}
|
|
957
1451
|
}
|
|
958
1452
|
|
|
959
1453
|
function afterStoreMoved(name) {
|
|
@@ -1008,13 +1502,59 @@
|
|
|
1008
1502
|
const before = focusMark();
|
|
1009
1503
|
const doc = store.doc;
|
|
1010
1504
|
const layout = geometry.layoutOf(doc, SIZES);
|
|
1505
|
+
// Clamped BEFORE `renderCanvas`/`renderEdgeBar` read it, and against
|
|
1506
|
+
// `edgeLayout`'s own filtered list rather than `doc.edge.length` — an
|
|
1507
|
+
// edge whose anchor letter went missing is still an array element (so
|
|
1508
|
+
// a length check would miss it) but `edgeLayout` already will not draw
|
|
1509
|
+
// it, and an inspector row pointed at an edge nothing on screen shows
|
|
1510
|
+
// is exactly the stale-selection defect `carryMarks` above exists to
|
|
1511
|
+
// avoid for lanes.
|
|
1512
|
+
if (selectedEdge !== null && !edgeIsDrawable(doc, layout, selectedEdge)) {
|
|
1513
|
+
selectedEdge = null;
|
|
1514
|
+
}
|
|
1515
|
+
// v3.5.0 Task 8: `endpointDrag.index` can only ever be reached from
|
|
1516
|
+
// `selectedEdge`'s own drawn handle (see `onCanvasDown`), so the two
|
|
1517
|
+
// may never disagree — checked with `!==` rather than "is `index`
|
|
1518
|
+
// still drawable" on its own, because that would let a drag survive a
|
|
1519
|
+
// mid-drag undo that DESELECTS the edge without deleting it (the
|
|
1520
|
+
// clamp above nulls `selectedEdge` on its own timing). A drag that
|
|
1521
|
+
// outlives the selection it was dragging is exactly the class of
|
|
1522
|
+
// hazard Task 7 paid three rounds to close for `pendingFrom`/
|
|
1523
|
+
// `edgeMode`; this is the same rule applied to this task's own mark.
|
|
1524
|
+
if (endpointDrag !== null && endpointDrag.index !== selectedEdge) {
|
|
1525
|
+
endpointDrag = null;
|
|
1526
|
+
}
|
|
1527
|
+
// v3.5.0 Task 12: the same defensive clamp `selectedEdge` gets just
|
|
1528
|
+
// above, for a mark of its own row-number shape — belt to
|
|
1529
|
+
// `carryMarks`'s suspenders, not a replacement for it: `carryMarks`
|
|
1530
|
+
// is what keeps a member POINTING AT THE RIGHT LANE across an edit,
|
|
1531
|
+
// and runs before this on every path that reaches `render()`. This
|
|
1532
|
+
// only catches a member that is merely out of range for whatever
|
|
1533
|
+
// reason (there should not be one left standing; a repaint is cheap
|
|
1534
|
+
// insurance against a future caller of `render()` that forgets to
|
|
1535
|
+
// route through `afterStoreMoved` first).
|
|
1536
|
+
if (laneMultiSelect !== null) {
|
|
1537
|
+
const kept = [];
|
|
1538
|
+
for (let m = 0; m < laneMultiSelect.length; m++) {
|
|
1539
|
+
if (laneMultiSelect[m] >= 0 && laneMultiSelect[m] < layout.lanes.length) {
|
|
1540
|
+
kept.push(laneMultiSelect[m]);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
laneMultiSelect = kept.length === 0 ? null : kept;
|
|
1544
|
+
}
|
|
1011
1545
|
renderLanes(doc, layout);
|
|
1012
1546
|
renderCanvas(doc, layout);
|
|
1547
|
+
renderEdgeBar(doc);
|
|
1013
1548
|
renderPreview(doc);
|
|
1014
1549
|
renderMeta(doc);
|
|
1015
|
-
renderUnmodelled(doc);
|
|
1550
|
+
renderUnmodelled(doc, layout);
|
|
1016
1551
|
undoBtn.disabled = !store.canUndo();
|
|
1017
1552
|
redoBtn.disabled = !store.canRedo();
|
|
1553
|
+
groupBtn.disabled = laneMultiSelect === null || laneMultiSelect.length < 2;
|
|
1554
|
+
overlay.setAttribute('data-wave-multiselect',
|
|
1555
|
+
laneMultiSelect === null ? '' : laneMultiSelect.slice().sort(function (a, b) {
|
|
1556
|
+
return a - b;
|
|
1557
|
+
}).join(','));
|
|
1018
1558
|
overlay.setAttribute('data-wave-gestures', String(gestures));
|
|
1019
1559
|
overlay.setAttribute('data-wave-lanes', String(layout.lanes.length));
|
|
1020
1560
|
overlay.setAttribute('data-wave-cycles', String(layout.cycles));
|
|
@@ -1025,6 +1565,38 @@
|
|
|
1025
1565
|
restoreFocus(before);
|
|
1026
1566
|
}
|
|
1027
1567
|
|
|
1568
|
+
/** Whether `geometry.edgeLayout` would still draw the edge at `index` —
|
|
1569
|
+
* the same test the drawing itself applies, asked rather than repeated. */
|
|
1570
|
+
function edgeIsDrawable(doc, layout, index) {
|
|
1571
|
+
const edges = geometry.edgeLayout(doc, layout);
|
|
1572
|
+
for (const e of edges) {
|
|
1573
|
+
if (e.index === index) return true;
|
|
1574
|
+
}
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Show or hide the inspector row, and — when it is showing — fill it in
|
|
1580
|
+
* from the selected edge's own entry.
|
|
1581
|
+
*
|
|
1582
|
+
* The ONLY thing that may set `edgeBar.hidden`, so "is an edge selected"
|
|
1583
|
+
* and "is there a box on screen" cannot come apart: precondition T1 at
|
|
1584
|
+
* the top of this file's docstring for the overlay itself, applied to
|
|
1585
|
+
* this row. Called only from `render()`, after `selectedEdge` has already
|
|
1586
|
+
* been clamped, so the `codec.parseEdge` below is not expected to fail —
|
|
1587
|
+
* it is checked anyway because "expected to" is not "guaranteed to".
|
|
1588
|
+
*/
|
|
1589
|
+
function renderEdgeBar(doc) {
|
|
1590
|
+
overlay.setAttribute('data-wave-selected-edge',
|
|
1591
|
+
selectedEdge === null ? '' : String(selectedEdge));
|
|
1592
|
+
edgeBar.hidden = selectedEdge === null;
|
|
1593
|
+
if (selectedEdge === null) return;
|
|
1594
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1595
|
+
if (cur === null) { edgeBar.hidden = true; return; }
|
|
1596
|
+
shapeSel.value = cur.shape;
|
|
1597
|
+
edgeLabelInput.value = cur.label;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1028
1600
|
/** What has the keyboard now, as something that survives a rebuild. */
|
|
1029
1601
|
function focusMark() {
|
|
1030
1602
|
const el = d.activeElement;
|
|
@@ -1070,6 +1642,27 @@
|
|
|
1070
1642
|
} catch (e) { /* not a text field */ }
|
|
1071
1643
|
}
|
|
1072
1644
|
|
|
1645
|
+
/**
|
|
1646
|
+
* `tick`/`every`/`tock` are read by the pinned engine as a number, a
|
|
1647
|
+
* string, or an array of either (`ticktock` in
|
|
1648
|
+
* `node_modules/wavedrom/lib/render-marks.js`) — unlike `head`/`foot`'s
|
|
1649
|
+
* own `.text`, which is always a plain string. A text `<input>` can only
|
|
1650
|
+
* hold one shape, so a hand-authored numeric or array value is shown in
|
|
1651
|
+
* its string form (an array space-joined, the same shape the engine's
|
|
1652
|
+
* own `val.trim().split(/\s+/)` turns a plain string back into) rather
|
|
1653
|
+
* than blanked out — blanking a field that actually holds `tick: 0`
|
|
1654
|
+
* would show a confident wrong state, the same failure `renderUnmodelled`
|
|
1655
|
+
* exists to avoid elsewhere in this file. Editing the field always
|
|
1656
|
+
* commits a plain string back, which the engine treats identically to
|
|
1657
|
+
* whichever shape it replaced.
|
|
1658
|
+
*/
|
|
1659
|
+
function tickTockText(v) {
|
|
1660
|
+
if (typeof v === 'string') return v;
|
|
1661
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
1662
|
+
if (Array.isArray(v)) return v.join(' ');
|
|
1663
|
+
return '';
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1073
1666
|
function renderMeta(doc) {
|
|
1074
1667
|
const h = doc.head;
|
|
1075
1668
|
const f = doc.foot;
|
|
@@ -1077,6 +1670,12 @@
|
|
|
1077
1670
|
? h.text : '';
|
|
1078
1671
|
footInput.value = (f !== null && typeof f === 'object' && typeof f.text === 'string')
|
|
1079
1672
|
? f.text : '';
|
|
1673
|
+
headTickInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tick) : '';
|
|
1674
|
+
headEveryInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.every) : '';
|
|
1675
|
+
footTockInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tock) : '';
|
|
1676
|
+
headTockInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tock) : '';
|
|
1677
|
+
footTickInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tick) : '';
|
|
1678
|
+
footEveryInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.every) : '';
|
|
1080
1679
|
}
|
|
1081
1680
|
|
|
1082
1681
|
/**
|
|
@@ -1089,8 +1688,35 @@
|
|
|
1089
1688
|
* the failure this whole overlay is arranged to avoid, so a document that
|
|
1090
1689
|
* carries any of them says so, names them, and points at the preview —
|
|
1091
1690
|
* which IS faithful, because it is the engine.
|
|
1691
|
+
*
|
|
1692
|
+
* Edges join this same notice for the opposite reason. As of this task
|
|
1693
|
+
* they ARE drawn — `renderEdges` below — so the claim for them is not "the
|
|
1694
|
+
* drawing ignores this property" but "this specific edge cannot be
|
|
1695
|
+
* placed".
|
|
1696
|
+
*
|
|
1697
|
+
* final review finding 3: this used to re-derive only ONE of
|
|
1698
|
+
* `geometry.edgeLayout`'s three skip reasons (the dangling-letter one) by
|
|
1699
|
+
* walking `doc.edge` a second time with its own copy of that check —
|
|
1700
|
+
* which is also the one case the pinned engine agrees with us on
|
|
1701
|
+
* (measured: an edge naming an absent letter draws nothing there either).
|
|
1702
|
+
* The other two — an unparseable shape, and a `node` index that has
|
|
1703
|
+
* outlived its lane's length (reachable from the editor's own
|
|
1704
|
+
* insert/delete-cycles buttons, since neither reindexes `node`) — are
|
|
1705
|
+
* exactly where the engine DOES draw a line and we silently do not,
|
|
1706
|
+
* which is the one divergence this whole overlay exists to prevent, and
|
|
1707
|
+
* the old code said nothing about either.
|
|
1708
|
+
*
|
|
1709
|
+
* So this asks `geometry.edgeLayoutDetail` — the SAME walk `renderEdges`
|
|
1710
|
+
* draws from — which edges it could not place and why, instead of
|
|
1711
|
+
* re-deriving any check of its own. Skips are bucketed by whatever
|
|
1712
|
+
* `reason` string comes back, not by a hardcoded list of the two or three
|
|
1713
|
+
* reasons known today: a fourth `continue` added to `edgeLayoutDetail`
|
|
1714
|
+
* later shows up here as its own bucket (worded generically if
|
|
1715
|
+
* `REASON_TEXT` has not been taught a nicer sentence for it yet) with no
|
|
1716
|
+
* change needed in this function, rather than escaping the notice the way
|
|
1717
|
+
* the third one did until now.
|
|
1092
1718
|
*/
|
|
1093
|
-
function renderUnmodelled(doc) {
|
|
1719
|
+
function renderUnmodelled(doc, layout) {
|
|
1094
1720
|
const found = [];
|
|
1095
1721
|
const cfg = doc.config;
|
|
1096
1722
|
if (cfg !== null && typeof cfg === 'object' && cfg.hscale !== undefined) {
|
|
@@ -1102,11 +1728,67 @@
|
|
|
1102
1728
|
if (lane.period !== undefined && found.indexOf('period') === -1) found.push('period');
|
|
1103
1729
|
if (lane.phase !== undefined && found.indexOf('phase') === -1) found.push('phase');
|
|
1104
1730
|
}
|
|
1731
|
+
|
|
1732
|
+
const detail = geometry.edgeLayoutDetail(doc, layout);
|
|
1733
|
+
// Bucket every skip by its own reported `reason` — discovered from what
|
|
1734
|
+
// came back, not enumerated in advance, so nothing here has to change
|
|
1735
|
+
// for a reason this function has never heard of.
|
|
1736
|
+
const buckets = new Map();
|
|
1737
|
+
for (const skip of detail.skipped) {
|
|
1738
|
+
if (!buckets.has(skip.reason)) buckets.set(skip.reason, []);
|
|
1739
|
+
buckets.get(skip.reason).push(skip);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function letters(list) {
|
|
1743
|
+
const out = [];
|
|
1744
|
+
for (const skip of list) {
|
|
1745
|
+
if (skip.edge === null) continue;
|
|
1746
|
+
if (out.indexOf(skip.edge.from) === -1) out.push(skip.edge.from);
|
|
1747
|
+
if (out.indexOf(skip.edge.to) === -1) out.push(skip.edge.to);
|
|
1748
|
+
}
|
|
1749
|
+
return out;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Known reasons get a sentence that says what a user can act on.
|
|
1753
|
+
// `dangling` matches the engine (nothing to reconcile against the
|
|
1754
|
+
// preview); `unparseable` and `out-of-range` are the two the engine
|
|
1755
|
+
// draws and we do not, so both point at the preview as the tie-breaker.
|
|
1756
|
+
const REASON_TEXT = {
|
|
1757
|
+
dangling: function (list) {
|
|
1758
|
+
return '這個區塊有 ' + list.length + ' 條關聯線引用了不存在的 anchor(' +
|
|
1759
|
+
letters(list).join('、') + '),左邊畫不出它們。';
|
|
1760
|
+
},
|
|
1761
|
+
unparseable: function (list) {
|
|
1762
|
+
return '這個區塊有 ' + list.length +
|
|
1763
|
+
' 條關聯線的寫法連引擎也認不得,左邊畫不出它們(跟右邊的 WaveDrom 預覽一致)。';
|
|
1764
|
+
},
|
|
1765
|
+
'out-of-range': function (list) {
|
|
1766
|
+
return '這個區塊有 ' + list.length + ' 條關聯線的 anchor(' +
|
|
1767
|
+
letters(list).join('、') + ')落在那條 lane 目前的長度之外,左邊畫不出它們' +
|
|
1768
|
+
'——但右邊的 WaveDrom 預覽仍然會畫,兩邊會不一致,以預覽為準。';
|
|
1769
|
+
},
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
const sentences = [];
|
|
1773
|
+
if (found.length > 0) {
|
|
1774
|
+
sentences.push('這份文件用了 ' + found.join('、') +
|
|
1775
|
+
',左邊的手繪波形不表現它們(寬度與相位會不一樣)——以右邊的 WaveDrom 預覽為準。');
|
|
1776
|
+
}
|
|
1777
|
+
for (const [reason, list] of buckets) {
|
|
1778
|
+
const text = REASON_TEXT[reason];
|
|
1779
|
+
sentences.push(text ? text(list) :
|
|
1780
|
+
'這個區塊有 ' + list.length + ' 條關聯線因為「' + reason + '」畫不出來,' +
|
|
1781
|
+
'以右邊的 WaveDrom 預覽為準。');
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const danglingCount = buckets.has('dangling') ? buckets.get('dangling').length : 0;
|
|
1105
1785
|
overlay.setAttribute('data-wave-unmodelled', found.join(' '));
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1786
|
+
overlay.setAttribute('data-wave-dangling-edges', String(danglingCount));
|
|
1787
|
+
// The total the dangling-only count above cannot say: every edge entry
|
|
1788
|
+
// `edgeLayoutDetail` could not place, for any reason at all.
|
|
1789
|
+
overlay.setAttribute('data-wave-skipped-edges', String(detail.skipped.length));
|
|
1790
|
+
unmodelled.hidden = sentences.length === 0;
|
|
1791
|
+
unmodelled.textContent = sentences.join(' ');
|
|
1110
1792
|
|
|
1111
1793
|
// …and the bigger version of the same honesty. This editor edits
|
|
1112
1794
|
// `signal:` and nothing else, so a `reg:` or `assign:` block opens a
|
|
@@ -1145,9 +1827,19 @@
|
|
|
1145
1827
|
rows.style.height = layout.height + 'px';
|
|
1146
1828
|
laneCol.appendChild(rows);
|
|
1147
1829
|
|
|
1830
|
+
const spans = groupSpansOf(doc, layout);
|
|
1831
|
+
// v3.5.0 Task 12: which row, if any, hosts THIS group's own 解散
|
|
1832
|
+
// button — see `directFirstLaneOf`'s comment for why at most one row
|
|
1833
|
+
// can, and why some groups (made entirely of sub-groups) have none.
|
|
1834
|
+
const ungroupAt = new Map();
|
|
1835
|
+
for (const span of spans) {
|
|
1836
|
+
const at = directFirstLaneOf(layout, span);
|
|
1837
|
+
if (at !== null) ungroupAt.set(at, span);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1148
1840
|
// Group titles first, so they sit UNDER the rows in paint order and a
|
|
1149
1841
|
// lane's own controls stay clickable over them.
|
|
1150
|
-
for (const span of
|
|
1842
|
+
for (const span of spans) {
|
|
1151
1843
|
const tag = d.createElement('button');
|
|
1152
1844
|
tag.type = 'button';
|
|
1153
1845
|
tag.className = 'ed-wave-group';
|
|
@@ -1164,7 +1856,7 @@
|
|
|
1164
1856
|
}
|
|
1165
1857
|
|
|
1166
1858
|
for (let i = 0; i < layout.lanes.length; i++) {
|
|
1167
|
-
rows.appendChild(laneRow(doc, layout, i));
|
|
1859
|
+
rows.appendChild(laneRow(doc, layout, i, ungroupAt.get(i)));
|
|
1168
1860
|
}
|
|
1169
1861
|
// The tail insert. `laneInsertPath(doc, laneCount)` goes on the end of
|
|
1170
1862
|
// `signal` itself, which is what makes the group asymmetry visible here
|
|
@@ -1172,7 +1864,13 @@
|
|
|
1172
1864
|
rows.appendChild(insertButton(doc, layout.lanes.length, layout, true));
|
|
1173
1865
|
}
|
|
1174
1866
|
|
|
1175
|
-
|
|
1867
|
+
/**
|
|
1868
|
+
* `ungroupSpan` is the group THIS row's 解散 button dissolves, or
|
|
1869
|
+
* `undefined` on every row that is not a group's own direct-first-lane —
|
|
1870
|
+
* `renderLanes` hands it over from the one `directFirstLaneOf` pass it
|
|
1871
|
+
* already made rather than this function re-deriving it per row.
|
|
1872
|
+
*/
|
|
1873
|
+
function laneRow(doc, layout, i, ungroupSpan) {
|
|
1176
1874
|
const row = d.createElement('div');
|
|
1177
1875
|
row.className = 'ed-wave-lane-row';
|
|
1178
1876
|
row.setAttribute('data-lane', String(i));
|
|
@@ -1180,6 +1878,54 @@
|
|
|
1180
1878
|
row.style.height = layout.laneHeight + 'px';
|
|
1181
1879
|
row.style.paddingLeft = (layout.lanes[i].depth * 16) + 'px';
|
|
1182
1880
|
if (selection !== null && selection.laneIndex === i) row.classList.add('is-selected');
|
|
1881
|
+
if (laneMultiSelect !== null && laneMultiSelect.indexOf(i) !== -1) {
|
|
1882
|
+
row.classList.add('is-multi-selected');
|
|
1883
|
+
}
|
|
1884
|
+
// v3.5.0 Task 12: Shift+click anywhere on the row toggles it into or
|
|
1885
|
+
// out of `laneMultiSelect`, the set「建立群組」reads. Capture phase, so
|
|
1886
|
+
// this decides BEFORE the row's own children do — a Shift+click landing
|
|
1887
|
+
// on ▲/▼/✕/the name field is claimed here rather than moving the lane,
|
|
1888
|
+
// deleting it, or opening its name for editing, the same reason
|
|
1889
|
+
// `onKeyDown` claims Escape/Tab in the capture phase at the dialog
|
|
1890
|
+
// level. An ordinary click (no Shift) is NOT claimed at all: it falls
|
|
1891
|
+
// through untouched to whichever child the user actually pressed,
|
|
1892
|
+
// which is what keeps every existing gesture in this row working
|
|
1893
|
+
// exactly as it did before this task.
|
|
1894
|
+
//
|
|
1895
|
+
// Fix round 1 (item 1): the name `<input>` is excluded from that catch.
|
|
1896
|
+
// `render()` below rebuilds the WHOLE lane rail from `store.doc`, and a
|
|
1897
|
+
// Shift+click that reached this listener while the user was still
|
|
1898
|
+
// typing in that field — no `change` fired yet, so nothing had been
|
|
1899
|
+
// committed — rebuilt the input from the OLD value, discarding
|
|
1900
|
+
// whatever they had typed with nothing on screen explaining it. That
|
|
1901
|
+
// is the exact failure family this batch has already paid for
|
|
1902
|
+
// (`pendingFrom`/`endpointDrag`'s own fix rounds), arriving by a route
|
|
1903
|
+
// THIS task opened: before it, `render()` never ran while the keyboard
|
|
1904
|
+
// was still in a field with no blur. Shift+click inside a text field
|
|
1905
|
+
// is also, on every platform, understood as "extend the text
|
|
1906
|
+
// selection" — leaving it alone here is not just the safe choice, it
|
|
1907
|
+
// is the expected one. MEASURED (a real Puppeteer page, the nested-
|
|
1908
|
+
// group / long-name / ungroup-button-present row — the most crowded
|
|
1909
|
+
// this rail ever gets): the input covers at most 45 of the row's
|
|
1910
|
+
// 200px; 129–155px of every row stays outside it. So there is always a
|
|
1911
|
+
// large, ordinary Shift+click target left — the ▲/▼/✕/+/解散群組
|
|
1912
|
+
// buttons and the row's own padding — and excluding the input costs
|
|
1913
|
+
// nothing a user could actually reach for.
|
|
1914
|
+
row.addEventListener('click', function (ev) {
|
|
1915
|
+
if (ev.shiftKey !== true) return;
|
|
1916
|
+
if (ev.target !== null && typeof ev.target.classList === 'object' &&
|
|
1917
|
+
ev.target.classList.contains('ed-wave-lane-name')) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
ev.preventDefault();
|
|
1921
|
+
ev.stopPropagation();
|
|
1922
|
+
if (laneMultiSelect === null) laneMultiSelect = [];
|
|
1923
|
+
const at = laneMultiSelect.indexOf(i);
|
|
1924
|
+
if (at === -1) laneMultiSelect.push(i);
|
|
1925
|
+
else laneMultiSelect.splice(at, 1);
|
|
1926
|
+
if (laneMultiSelect.length === 0) laneMultiSelect = null;
|
|
1927
|
+
render();
|
|
1928
|
+
}, true);
|
|
1183
1929
|
|
|
1184
1930
|
row.appendChild(insertButton(doc, i, layout, false));
|
|
1185
1931
|
|
|
@@ -1244,6 +1990,86 @@
|
|
|
1244
1990
|
commit('remove-lane', function (doc2) { return codec.removeLane(doc2, i); });
|
|
1245
1991
|
});
|
|
1246
1992
|
row.appendChild(del);
|
|
1993
|
+
|
|
1994
|
+
// v3.5.0 Task 13: duplicate this lane — a full deep copy (`data` / any
|
|
1995
|
+
// other key, via `codec.duplicateLane`) landing right after it at
|
|
1996
|
+
// `i + 1`. Same landing-awareness `insertButton` below already gives
|
|
1997
|
+
// the user for an ordinary insert: `duplicateLane` is built on the very
|
|
1998
|
+
// `addLane`/`laneInsertPath` door that button uses, so it inherits the
|
|
1999
|
+
// identical group-tail asymmetry, and the status line says so the same
|
|
2000
|
+
// way.
|
|
2001
|
+
//
|
|
2002
|
+
// final review finding 2: `node` is deliberately NOT among the copied
|
|
2003
|
+
// keys. It names anchor letters that resolve to THIS lane; carrying it
|
|
2004
|
+
// onto the duplicate would give two lanes the same letters and silently
|
|
2005
|
+
// relocate every edge already anchored here onto the copy instead. The
|
|
2006
|
+
// duplicate starts with no anchors of its own.
|
|
2007
|
+
const dup = d.createElement('button');
|
|
2008
|
+
dup.type = 'button';
|
|
2009
|
+
dup.className = 'ed-wave-lane-duplicate';
|
|
2010
|
+
dup.setAttribute('data-focus-key', 'lane-duplicate-' + i);
|
|
2011
|
+
dup.textContent = '複製';
|
|
2012
|
+
dup.title = '複製這條 lane,接在它後面(data 等欄位一起複製;node 錨點不會跟著複製,避免兩條 lane 搶同一個字母)';
|
|
2013
|
+
dup.addEventListener('click', function () {
|
|
2014
|
+
const landing = landingOf(doc, i + 1);
|
|
2015
|
+
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
2016
|
+
// The copy's own name field is the one control a duplicate exists
|
|
2017
|
+
// for the user to reach next — same reasoning as `insertButton`'s
|
|
2018
|
+
// `focusOverride` below.
|
|
2019
|
+
focusOverride = 'lane-name-' + (i + 1);
|
|
2020
|
+
const didDup = commit('duplicate-lane', function (doc2) { return codec.duplicateLane(doc2, i); });
|
|
2021
|
+
if (didDup) say('複製的 lane 落在' + where);
|
|
2022
|
+
});
|
|
2023
|
+
row.appendChild(dup);
|
|
2024
|
+
|
|
2025
|
+
// v3.5.0 Task 13: insert a spacer — WaveDrom's own blank row, `{}` —
|
|
2026
|
+
// right after this lane. There is deliberately no dedicated "remove
|
|
2027
|
+
// spacer" control: a spacer is a lane like any other, so the ✕ button
|
|
2028
|
+
// every row already has removes it exactly as it removes any other
|
|
2029
|
+
// lane. `codec.addLane` already accepts `{}` (`isLane` only requires a
|
|
2030
|
+
// non-array object), so this needs no new codec function of its own —
|
|
2031
|
+
// unlike `duplicateLane`, which needed one because nothing in the
|
|
2032
|
+
// codec already answers "the lane at this position, copied".
|
|
2033
|
+
const spacer = d.createElement('button');
|
|
2034
|
+
spacer.type = 'button';
|
|
2035
|
+
spacer.className = 'ed-wave-lane-spacer';
|
|
2036
|
+
spacer.setAttribute('data-focus-key', 'lane-spacer-' + i);
|
|
2037
|
+
spacer.textContent = '空白列';
|
|
2038
|
+
spacer.title = '在這條後面插入一個空白列(WaveJSON 的 {},用來空出間距;' +
|
|
2039
|
+
'刪除跟刪一般的 lane 一樣,用 ✕)';
|
|
2040
|
+
spacer.addEventListener('click', function () {
|
|
2041
|
+
const landing = landingOf(doc, i + 1);
|
|
2042
|
+
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
2043
|
+
// A spacer has no name to type, so the control the user is most
|
|
2044
|
+
// likely to reach for next is ✕ — on the new row itself, at i + 1.
|
|
2045
|
+
focusOverride = 'lane-remove-' + (i + 1);
|
|
2046
|
+
const didAdd = commit('add-spacer', function (doc2) { return codec.addLane(doc2, i + 1, {}); });
|
|
2047
|
+
if (didAdd) say('空白列落在' + where);
|
|
2048
|
+
});
|
|
2049
|
+
row.appendChild(spacer);
|
|
2050
|
+
|
|
2051
|
+
// v3.5.0 Task 12: 解散, only on the one row `renderLanes` handed a
|
|
2052
|
+
// span for — the group's own direct first lane (see
|
|
2053
|
+
// `directFirstLaneOf`). The lanes it held stay exactly where they
|
|
2054
|
+
// were; only the wrapping array and its title go.
|
|
2055
|
+
if (ungroupSpan !== undefined) {
|
|
2056
|
+
const ungroup = d.createElement('button');
|
|
2057
|
+
ungroup.type = 'button';
|
|
2058
|
+
ungroup.className = 'ed-wave-lane-ungroup';
|
|
2059
|
+
ungroup.setAttribute('data-focus-key', 'lane-ungroup-' + i);
|
|
2060
|
+
ungroup.textContent = '解散群組';
|
|
2061
|
+
ungroup.title = '解散「' + ungroupSpan.title +
|
|
2062
|
+
'」這個群組(裡面的 lane 都留著,只是不再是一組)';
|
|
2063
|
+
ungroup.addEventListener('click', function () {
|
|
2064
|
+
// The lane at `i` survives ungrouping at the same display position
|
|
2065
|
+
// (see `ungroupLanes`'s own comment: the flattened order and count
|
|
2066
|
+
// never change), so its own name field is exactly where the
|
|
2067
|
+
// keyboard should land.
|
|
2068
|
+
focusOverride = 'lane-name-' + i;
|
|
2069
|
+
commit('ungroup', function (doc2) { return codec.ungroupLanes(doc2, i); });
|
|
2070
|
+
});
|
|
2071
|
+
row.appendChild(ungroup);
|
|
2072
|
+
}
|
|
1247
2073
|
return row;
|
|
1248
2074
|
}
|
|
1249
2075
|
|
|
@@ -1412,6 +2238,187 @@
|
|
|
1412
2238
|
input.select();
|
|
1413
2239
|
}
|
|
1414
2240
|
|
|
2241
|
+
/**
|
|
2242
|
+
* The in-place editor for one bus cell's `data` label — this dialog's
|
|
2243
|
+
* OTHER sub-panel, same `{cancel, retire}` shape as `renameGroup` just
|
|
2244
|
+
* above and opened the same two ways every other gesture in this file
|
|
2245
|
+
* is: a pointer press (`onCanvasDown`) or the keyboard's own Enter
|
|
2246
|
+
* (`handleDrawingKey`), both of which have already asked
|
|
2247
|
+
* `codec.dataSlotOf` before calling this, so `slot` below is never
|
|
2248
|
+
* recomputed from a belief of its own about which cycles consume one.
|
|
2249
|
+
*
|
|
2250
|
+
* Positioned with `geometry.cellRect` — the exact numbers the keyboard
|
|
2251
|
+
* cursor and the pointer's own hit test already share — offset by
|
|
2252
|
+
* `canvas.offsetLeft`/`offsetTop` rather than the wrap's own padding
|
|
2253
|
+
* written out as a constant: the field is a plain sibling of the SVG
|
|
2254
|
+
* inside `.ed-wave-canvas-wrap` (now `position: relative`, see
|
|
2255
|
+
* `lib/md2doc.js`), so reading the SVG's OWN measured offset is what
|
|
2256
|
+
* keeps the two from drifting apart if that padding ever changes.
|
|
2257
|
+
*
|
|
2258
|
+
* Unlike `renameGroup`'s field, this one's node sits beside a canvas
|
|
2259
|
+
* `renderCanvas` rebuilds on every cursor move — see that function's own
|
|
2260
|
+
* first line for why `retire()` has to be called from there too, not
|
|
2261
|
+
* only from `render()`.
|
|
2262
|
+
*/
|
|
2263
|
+
function openDataEdit(laneIndex, cycle) {
|
|
2264
|
+
if (canvas === null) return;
|
|
2265
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
2266
|
+
const row = layout.lanes[laneIndex];
|
|
2267
|
+
const lane = row === undefined ? null : row.lane;
|
|
2268
|
+
const slot = lane === null ? null : codec.dataSlotOf(lane, cycle);
|
|
2269
|
+
if (slot === null) return;
|
|
2270
|
+
const box = geometry.cellRect(layout, laneIndex, cycle);
|
|
2271
|
+
if (box === null) return;
|
|
2272
|
+
// A press or an Enter that lands on a second bus cell while the field
|
|
2273
|
+
// is still open on the first one: the open field never got the blur
|
|
2274
|
+
// that a plain click elsewhere would have given it (the keyboard's
|
|
2275
|
+
// Enter path never moves focus at all), so it is cancelled here,
|
|
2276
|
+
// by hand, before this one opens. Unsaved text in the first field is
|
|
2277
|
+
// discarded — the same trade `renameGroup` makes for its own field on
|
|
2278
|
+
// every route that is not `finish()` itself.
|
|
2279
|
+
if (dataEdit !== null) dataEdit.cancel();
|
|
2280
|
+
|
|
2281
|
+
const current = labelsOf(lane)[slot];
|
|
2282
|
+
const startValue = typeof current === 'string' ? current : '';
|
|
2283
|
+
|
|
2284
|
+
const input = d.createElement('input');
|
|
2285
|
+
input.type = 'text';
|
|
2286
|
+
input.className = 'ed-wave-data-input';
|
|
2287
|
+
input.setAttribute('data-focus-key', 'data-input-' + laneIndex + '-' + cycle);
|
|
2288
|
+
input.value = startValue;
|
|
2289
|
+
input.style.left = (canvas.offsetLeft + box.x) + 'px';
|
|
2290
|
+
input.style.top = (canvas.offsetTop + box.y) + 'px';
|
|
2291
|
+
input.style.width = box.width + 'px';
|
|
2292
|
+
input.style.height = box.height + 'px';
|
|
2293
|
+
|
|
2294
|
+
/** The field off. A no-op once a repaint has already taken it away. */
|
|
2295
|
+
const takeDown = function () {
|
|
2296
|
+
if (input.parentNode !== null) input.parentNode.removeChild(input);
|
|
2297
|
+
};
|
|
2298
|
+
/** Whether the keyboard is IN the field right now. False on the
|
|
2299
|
+
* `blur` path — the browser has already moved it by then. */
|
|
2300
|
+
const holdsKeyboard = function () { return d.activeElement === input; };
|
|
2301
|
+
|
|
2302
|
+
const finish = function () {
|
|
2303
|
+
// Once. `blur` and this file's own explicit Enter handler can both
|
|
2304
|
+
// reach here for the one commit (Enter removes the field, which
|
|
2305
|
+
// fires a `blur` of its own) — the latch is the same "whoever gets
|
|
2306
|
+
// here first owns the gesture" rule `renameGroup.finish` uses.
|
|
2307
|
+
if (dataEdit === null) return;
|
|
2308
|
+
dataEdit = null;
|
|
2309
|
+
const hadKeyboard = holdsKeyboard();
|
|
2310
|
+
const value = input.value;
|
|
2311
|
+
takeDown();
|
|
2312
|
+
if (value === startValue) {
|
|
2313
|
+
// Nothing to write, so nothing to repaint — `canvas` is already
|
|
2314
|
+
// the element it was before this field opened.
|
|
2315
|
+
if (hadKeyboard) canvas.focus();
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
// The keyboard is only NAMED when it would otherwise be lost — same
|
|
2319
|
+
// condition `renameGroup.finish` uses: either this field had it, or
|
|
2320
|
+
// whatever DOES have it right now is not even inside the overlay
|
|
2321
|
+
// (both mean the coming repaint would otherwise leave it on
|
|
2322
|
+
// `document.body`). `focusOverride` already set by someone else's
|
|
2323
|
+
// more specific promise is left alone.
|
|
2324
|
+
if (focusOverride === null && (hadKeyboard || !overlay.contains(d.activeElement))) {
|
|
2325
|
+
focusOverride = 'canvas';
|
|
2326
|
+
}
|
|
2327
|
+
commit('data-label', function (doc) {
|
|
2328
|
+
return codec.setDataAt(doc, laneIndex, slot, value);
|
|
2329
|
+
});
|
|
2330
|
+
};
|
|
2331
|
+
/** The canvas is about to be rebuilt underneath this field by
|
|
2332
|
+
* `renderCanvas` — an undo, a redo, any gesture that repaints while
|
|
2333
|
+
* it is open. The node goes with that rebuild; what matters is
|
|
2334
|
+
* dropping the latch and naming where the keyboard lands, exactly
|
|
2335
|
+
* the same reason `renameGroup.retire` exists. */
|
|
2336
|
+
const retire = function () {
|
|
2337
|
+
if (dataEdit === null) return;
|
|
2338
|
+
dataEdit = null;
|
|
2339
|
+
if (focusOverride === null && holdsKeyboard()) focusOverride = 'canvas';
|
|
2340
|
+
};
|
|
2341
|
+
/** Escape's half: the field goes, the label does not change, the
|
|
2342
|
+
* session stands, and the keyboard goes back to the drawing — never
|
|
2343
|
+
* to `document.body`. No repaint: the canvas was never touched. */
|
|
2344
|
+
const cancel = function () {
|
|
2345
|
+
if (dataEdit === null) return;
|
|
2346
|
+
dataEdit = null;
|
|
2347
|
+
takeDown();
|
|
2348
|
+
canvas.focus();
|
|
2349
|
+
};
|
|
2350
|
+
// The explicit commit key: Enter. Left to a plain native `change`
|
|
2351
|
+
// event the way `renameGroup`'s field is would depend on whether this
|
|
2352
|
+
// browser fires `change` for a lone text input on Enter alone, which
|
|
2353
|
+
// is not a promise this file makes elsewhere — asked for outright
|
|
2354
|
+
// instead, the same way Escape is asked for explicitly one level up
|
|
2355
|
+
// in `onKeyDown` rather than left to a default.
|
|
2356
|
+
input.addEventListener('keydown', function (ev) {
|
|
2357
|
+
if (ev.key !== 'Enter') return;
|
|
2358
|
+
ev.preventDefault();
|
|
2359
|
+
ev.stopPropagation();
|
|
2360
|
+
finish();
|
|
2361
|
+
});
|
|
2362
|
+
input.addEventListener('change', finish);
|
|
2363
|
+
input.addEventListener('blur', finish);
|
|
2364
|
+
canvasWrap.appendChild(input);
|
|
2365
|
+
dataEdit = { cancel: cancel, retire: retire };
|
|
2366
|
+
input.focus();
|
|
2367
|
+
input.select();
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Where a press or an Enter on cycle `cycle` of `lane` should open the
|
|
2372
|
+
* data-label field — or whether it must refuse the gesture outright
|
|
2373
|
+
* instead of letting it fall through to an ordinary paint.
|
|
2374
|
+
*
|
|
2375
|
+
* v3.5.0 Task 11 fix round 1: a cycle INSIDE a bus run that itself owns
|
|
2376
|
+
* no slot (`codec.dataSlotOf` answers `null` — a `.` continuation or a
|
|
2377
|
+
* `|` gap) used to fall straight through to the paint-arming code with
|
|
2378
|
+
* nothing catching it. Reviewer-traced: the press silently repainted
|
|
2379
|
+
* that continuation with whatever brush was current, turning it into a
|
|
2380
|
+
* fresh EXPLICIT value cycle and shifting every LATER label in the lane
|
|
2381
|
+
* one slot to the right — silent data corruption, not an inert click,
|
|
2382
|
+
* because the box this cycle sits inside is exactly what this task now
|
|
2383
|
+
* invites the user to press.
|
|
2384
|
+
*
|
|
2385
|
+
* `isBus(brickOf(...))` is the same test `drawLane` uses to decide
|
|
2386
|
+
* whether a cycle is a bus box at all for the PICTURE, asked here to
|
|
2387
|
+
* decide it for the GESTURE — so a plain non-bus cycle (`0`, `1`, `x`, a
|
|
2388
|
+
* clock, …) is untouched and keeps painting exactly as it always has;
|
|
2389
|
+
* only a cycle INSIDE a bus box is ever diverted. Walking backward for
|
|
2390
|
+
* the owning cycle stops the moment the resolved LEVEL changes — two
|
|
2391
|
+
* adjacent bus runs of the identical value (`2.2.` — measured against
|
|
2392
|
+
* `codec`'s own doc comment: two separate labels, not one) must not
|
|
2393
|
+
* have a later cycle resolve into the EARLIER run's slot.
|
|
2394
|
+
*
|
|
2395
|
+
* Returns:
|
|
2396
|
+
* `{cycle, slot}` — open the field for THIS cycle's slot: either
|
|
2397
|
+
* `cycle` itself owns one, or the walk back to its
|
|
2398
|
+
* run's owning explicit cycle found one.
|
|
2399
|
+
* `'blocked'` — inside a bus box, but no owning cycle exists (a
|
|
2400
|
+
* leading repeater with nothing to continue —
|
|
2401
|
+
* MEASURED, `levelsOf` draws such a lane as `x`,
|
|
2402
|
+
* so this should be unreachable in practice, but
|
|
2403
|
+
* the check is kept rather than assumed so a
|
|
2404
|
+
* future change to that rule fails LOUD here
|
|
2405
|
+
* rather than silently reaching paint again).
|
|
2406
|
+
* Callers must refuse the gesture on this answer,
|
|
2407
|
+
* never fall through.
|
|
2408
|
+
* `null` — not a bus box; ordinary behaviour is unaffected.
|
|
2409
|
+
*/
|
|
2410
|
+
function resolveBusTarget(lane, cycle) {
|
|
2411
|
+
const slot = codec.dataSlotOf(lane, cycle);
|
|
2412
|
+
if (slot !== null) return { cycle: cycle, slot: slot };
|
|
2413
|
+
const levels = codec.levelsOf(typeof lane.wave === 'string' ? lane.wave : '');
|
|
2414
|
+
if (!isBus(brickOf(levels[cycle]))) return null;
|
|
2415
|
+
for (let i = cycle - 1; i >= 0 && levels[i] === levels[cycle]; i--) {
|
|
2416
|
+
const s = codec.dataSlotOf(lane, i);
|
|
2417
|
+
if (s !== null) return { cycle: i, slot: s };
|
|
2418
|
+
}
|
|
2419
|
+
return 'blocked';
|
|
2420
|
+
}
|
|
2421
|
+
|
|
1415
2422
|
/**
|
|
1416
2423
|
* A copy of `doc` with one scalar replaced, for the two fields the codec's
|
|
1417
2424
|
* lane operations do not cover (a group's own title).
|
|
@@ -1447,6 +2454,14 @@
|
|
|
1447
2454
|
* names its bricks in its `<use xlink:href="#…">` list.
|
|
1448
2455
|
*/
|
|
1449
2456
|
function renderCanvas(doc, layout) {
|
|
2457
|
+
// v3.5.0 Task 11: the data-label field's node sits beside the canvas
|
|
2458
|
+
// SVG inside `canvasWrap` (see `openDataEdit`), and the wipe two lines
|
|
2459
|
+
// below (`canvasWrap.textContent = ''`) would tear it out from under
|
|
2460
|
+
// the user on the very next cursor move if this did not retire it
|
|
2461
|
+
// first — every path that reaches this function counts, not only
|
|
2462
|
+
// `render()`, which is why this lives HERE and not only next to
|
|
2463
|
+
// `groupRename.retire()` in `render()` itself.
|
|
2464
|
+
if (dataEdit !== null) dataEdit.retire();
|
|
1450
2465
|
// Every repaint REPLACES the canvas element, so a canvas that had the
|
|
1451
2466
|
// keyboard loses it to `document.body` the instant it is detached —
|
|
1452
2467
|
// exactly the state F7 and T6h exist to prevent, and here it would fire
|
|
@@ -1538,6 +2553,55 @@
|
|
|
1538
2553
|
drawLane(canvas, layout, i);
|
|
1539
2554
|
}
|
|
1540
2555
|
|
|
2556
|
+
const edges = renderEdges(canvas, doc, layout);
|
|
2557
|
+
|
|
2558
|
+
// v3.5.0 Task 8: the in-flight endpoint drag's own preview — UI state
|
|
2559
|
+
// only, never a document read (rule 3: nothing is written until
|
|
2560
|
+
// `finishEndpointDrag` commits it, and Escape mid-drag never reaches
|
|
2561
|
+
// that far). `renderEdges` above already faded the dragged edge's OWN,
|
|
2562
|
+
// still-current path (`.is-drag-source`, read from this same
|
|
2563
|
+
// `endpointDrag`); this draws the other half — a dashed line from the
|
|
2564
|
+
// edge's UNMOVED end to wherever the drag is currently over. Reuses
|
|
2565
|
+
// `.ed-wave-edge-pending`, the exact dashed style Task 7's own
|
|
2566
|
+
// not-yet-written preview line already uses, for the identical
|
|
2567
|
+
// reason: this line is not `doc.edge` either, until the drop.
|
|
2568
|
+
//
|
|
2569
|
+
// Looked up in `edges` — the SAME array `renderEdges` just built this
|
|
2570
|
+
// pass, not a second call to `geometry.edgeLayout` — so the two can
|
|
2571
|
+
// never draw from different snapshots of the layout. `render()`'s own
|
|
2572
|
+
// clamp guarantees `endpointDrag.index === selectedEdge` whenever
|
|
2573
|
+
// `endpointDrag` is non-null, so a lookup miss here would mean that
|
|
2574
|
+
// invariant broke, not that the edge is merely unselected — the `!==
|
|
2575
|
+
// undefined` check is defensive rather than expected to matter.
|
|
2576
|
+
let endpointDragMark = '';
|
|
2577
|
+
if (endpointDrag !== null) {
|
|
2578
|
+
const dragged = edges.filter(function (e) { return e.index === endpointDrag.index; })[0];
|
|
2579
|
+
if (dragged !== undefined) {
|
|
2580
|
+
const otherEnd = endpointDrag.end === 'from' ? 'to' : 'from';
|
|
2581
|
+
const from = dragged[otherEnd];
|
|
2582
|
+
// Falls back to the dragged end's OWN current position — the
|
|
2583
|
+
// degenerate "no visible change yet" line — until a `pointermove`
|
|
2584
|
+
// has actually landed on a real cell (`endpointDrag.at !== null`).
|
|
2585
|
+
let to = dragged[endpointDrag.end];
|
|
2586
|
+
if (endpointDrag.at !== null) {
|
|
2587
|
+
const cell = geometry.cellRect(layout, endpointDrag.at, endpointDrag.cell);
|
|
2588
|
+
if (cell !== null) {
|
|
2589
|
+
to = { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 };
|
|
2590
|
+
endpointDragMark = endpointDrag.index + ':' + endpointDrag.end + '@' +
|
|
2591
|
+
endpointDrag.at + ',' + endpointDrag.cell;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
2595
|
+
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
2596
|
+
line.setAttribute('x1', String(from.x));
|
|
2597
|
+
line.setAttribute('y1', String(from.y));
|
|
2598
|
+
line.setAttribute('x2', String(to.x));
|
|
2599
|
+
line.setAttribute('y2', String(to.y));
|
|
2600
|
+
canvas.appendChild(line);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
overlay.setAttribute('data-wave-endpoint-drag', endpointDragMark);
|
|
2604
|
+
|
|
1541
2605
|
if (selection !== null) {
|
|
1542
2606
|
const from = Math.min(selection.from, selection.to);
|
|
1543
2607
|
const to = Math.max(selection.from, selection.to);
|
|
@@ -1550,6 +2614,33 @@
|
|
|
1550
2614
|
canvas.appendChild(box);
|
|
1551
2615
|
}
|
|
1552
2616
|
|
|
2617
|
+
// v3.5.0 Task 7: the pending edge — `pendingFrom` to wherever `cursor`
|
|
2618
|
+
// is now — drawn from UI state alone, never from the document (rule 2:
|
|
2619
|
+
// nothing about the doc changes until the drop). `cursor` is shared
|
|
2620
|
+
// with the keyboard cell below it in paint order for the same reason
|
|
2621
|
+
// the two are drawn from the same variable: a mouse drag and the
|
|
2622
|
+
// keyboard's two-Enter version are one gesture wearing two input
|
|
2623
|
+
// devices, so there is exactly one line to preview, however it is
|
|
2624
|
+
// being aimed at this instant. Skipped once `pendingFrom` is null,
|
|
2625
|
+
// which is every state except "armed with a marked start" and
|
|
2626
|
+
// "dragging" — see `setEdgeMode`.
|
|
2627
|
+
let pendingCell = '';
|
|
2628
|
+
if (pendingFrom !== null && cursor !== null) {
|
|
2629
|
+
const from = geometry.cellRect(layout, pendingFrom.at, pendingFrom.cell);
|
|
2630
|
+
const to = geometry.cellRect(layout, cursor.laneIndex, cursor.cycle);
|
|
2631
|
+
if (from !== null && to !== null) {
|
|
2632
|
+
pendingCell = pendingFrom.at + ',' + pendingFrom.cell;
|
|
2633
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
2634
|
+
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
2635
|
+
line.setAttribute('x1', String(from.x + from.width / 2));
|
|
2636
|
+
line.setAttribute('y1', String(from.y + from.height / 2));
|
|
2637
|
+
line.setAttribute('x2', String(to.x + to.width / 2));
|
|
2638
|
+
line.setAttribute('y2', String(to.y + to.height / 2));
|
|
2639
|
+
canvas.appendChild(line);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
overlay.setAttribute('data-wave-edge-pending', pendingCell);
|
|
2643
|
+
|
|
1553
2644
|
// The keyboard's cell, drawn last so it is over the selection tint and
|
|
1554
2645
|
// over the trace. Its geometry comes from `wave-geometry.cellRect` — the
|
|
1555
2646
|
// inverse of the hit test the pointer uses — so the cell the cursor is
|
|
@@ -1629,6 +2720,25 @@
|
|
|
1629
2720
|
for (let c = 0; c < cells.length; c++) {
|
|
1630
2721
|
if (cells[c].held !== true && cells[c].ch === '|') gaps.push(c);
|
|
1631
2722
|
}
|
|
2723
|
+
// v3.5.0 Task 10 fix round 2: which cycles carry the P/N explicit-edge
|
|
2724
|
+
// arrow, and which one. Read off `levels` directly rather than off
|
|
2725
|
+
// `bricks`, for the same reason `gaps` above reads off `cells` rather
|
|
2726
|
+
// than off `bricks`: `brickOf` collapses `P` into the SAME brick id as
|
|
2727
|
+
// `p` (that is what makes the arrow the one thing `brickOf`'s own
|
|
2728
|
+
// model cannot carry — see its comment, and the measurement above the
|
|
2729
|
+
// arrow's own drawing code below), so asking `bricks` here would
|
|
2730
|
+
// always come back empty. One entry per cycle that has an arrow,
|
|
2731
|
+
// `"<cycle>:<P|N>"`, in the same index-not-one-slot-per-cycle shape
|
|
2732
|
+
// `data-gaps-<i>` already uses — an empty lane (no `P`/`N` anywhere,
|
|
2733
|
+
// e.g. every existing fixture's plain `p`/`n` clocks) reads as `''`,
|
|
2734
|
+
// which is the ABSENCE half of the contract this attribute exists to
|
|
2735
|
+
// let a test pin: a `P` lane must show an entry here, a `p` lane must
|
|
2736
|
+
// not, and drawing the arrow on every clock (the regression this
|
|
2737
|
+
// attribute is for) would make the second half false.
|
|
2738
|
+
const clockEdges = [];
|
|
2739
|
+
for (let c = 0; c < levels.length; c++) {
|
|
2740
|
+
if (levels[c] === 'P' || levels[c] === 'N') clockEdges.push(c + ':' + levels[c]);
|
|
2741
|
+
}
|
|
1632
2742
|
// `data-wave-<i>` cannot carry the difference between "no `wave` key" and
|
|
1633
2743
|
// `wave: ''` — both are the empty string — and that difference is exactly
|
|
1634
2744
|
// what the engine draws differently, so it gets its own attribute.
|
|
@@ -1637,6 +2747,7 @@
|
|
|
1637
2747
|
svg.setAttribute('data-levels-' + i, levels.join(''));
|
|
1638
2748
|
svg.setAttribute('data-bricks-' + i, bricks.join(' '));
|
|
1639
2749
|
svg.setAttribute('data-gaps-' + i, gaps.join(' '));
|
|
2750
|
+
svg.setAttribute('data-clock-edge-' + i, clockEdges.join(' '));
|
|
1640
2751
|
|
|
1641
2752
|
const hi = row.y + 7;
|
|
1642
2753
|
const lo = row.y + row.height - 9;
|
|
@@ -1688,6 +2799,31 @@
|
|
|
1688
2799
|
t.setAttribute('text-anchor', 'middle');
|
|
1689
2800
|
t.textContent = text;
|
|
1690
2801
|
svg.appendChild(t);
|
|
2802
|
+
} else {
|
|
2803
|
+
// v3.5.0 Task 11 fix round 3: an UNLABELLED bus run has no
|
|
2804
|
+
// `<text>` for a press to land on — fix round 2's own rule
|
|
2805
|
+
// ("open the field only on a press that hits the label text")
|
|
2806
|
+
// otherwise leaves the single most ordinary flow in this editor
|
|
2807
|
+
// (paint a `=`, then name it) mouse-unreachable. This dot is
|
|
2808
|
+
// that run's hit target when there is no label yet: same
|
|
2809
|
+
// class as the text (`ed-wave-buslabel`, so `onCanvasDown`'s
|
|
2810
|
+
// existing hit test needs no change at all — it already asks
|
|
2811
|
+
// for that one class, not for an element type) plus
|
|
2812
|
+
// `ed-wave-buslabel-empty` for the dim, small styling
|
|
2813
|
+
// (`lib/md2doc.js`) that keeps a diagram full of unlabelled
|
|
2814
|
+
// buses from turning into a field of marks. Radius bounded by
|
|
2815
|
+
// the run's own box on both axes, not a bare constant, so the
|
|
2816
|
+
// box stays MOSTLY paintable even at the narrowest cycle width
|
|
2817
|
+
// this editor draws, and a press anywhere else in the box still
|
|
2818
|
+
// falls through to the ordinary paint dispatch exactly as
|
|
2819
|
+
// round 2 established.
|
|
2820
|
+
const r = Math.min(3.5, (x1 - x0) / 6, (lo - hi) / 6);
|
|
2821
|
+
const dot = d.createElementNS(SVGNS, 'circle');
|
|
2822
|
+
dot.setAttribute('class', 'ed-wave-buslabel ed-wave-buslabel-empty');
|
|
2823
|
+
dot.setAttribute('cx', String((x0 + x1) / 2));
|
|
2824
|
+
dot.setAttribute('cy', String(mid));
|
|
2825
|
+
dot.setAttribute('r', String(r));
|
|
2826
|
+
svg.appendChild(dot);
|
|
1691
2827
|
}
|
|
1692
2828
|
labelSlot++;
|
|
1693
2829
|
} else if (brick === 'xxx') {
|
|
@@ -1708,6 +2844,70 @@
|
|
|
1708
2844
|
: 'M' + x0 + ',' + hi + ' L' + x0 + ',' + lo + ' L' + xm + ',' + lo +
|
|
1709
2845
|
' L' + xm + ',' + hi + ' L' + x1 + ',' + hi);
|
|
1710
2846
|
svg.appendChild(p);
|
|
2847
|
+
|
|
2848
|
+
// v3.5.0 Task 10: `P`/`N` are explicit-edge clocks. `brickOf` maps
|
|
2849
|
+
// both to the SAME brick id as `p`/`n` (`nclk`/`pclk` — see its own
|
|
2850
|
+
// comment), which is correct for the level the engine SHOWS but
|
|
2851
|
+
// erases the one thing that makes `P`/`N` look different: an
|
|
2852
|
+
// arrowhead. MEASURED against the pinned wavedrom 3.5.0 — rendered
|
|
2853
|
+
// `{signal:[{name:'a',wave:'zpp'}]}` next to `{wave:'zPP'}` via
|
|
2854
|
+
// `wd.renderAny(0, doc, wd.waveSkin)` and diffed the `<use
|
|
2855
|
+
// xlink:href>` list:
|
|
2856
|
+
//
|
|
2857
|
+
// p: #zzz #zzz #pclk #nclk #pclk #nclk
|
|
2858
|
+
// P: #zzz #zzz #Pclk #nclk #Pclk #nclk
|
|
2859
|
+
// n: #zzz #zzz #nclk #pclk #nclk #pclk
|
|
2860
|
+
// N: #zzz #zzz #Nclk #pclk #Nclk #pclk
|
|
2861
|
+
//
|
|
2862
|
+
// — same SECOND half-brick every time (that is what `brickOf`'s own
|
|
2863
|
+
// measurement already pins), but the FIRST half-brick gets its own
|
|
2864
|
+
// symbol. Read straight out of `node_modules/wavedrom/skins/default.js`:
|
|
2865
|
+
//
|
|
2866
|
+
// pclk 'M0,20 0,0 20,0' (just the edge)
|
|
2867
|
+
// Pclk same edge PLUS 'M-3,12 0,3 3,12 C 1,11 -1,11 -3,12 z' (filled, class s6)
|
|
2868
|
+
// nclk 'm0,0 0,20 20,0'
|
|
2869
|
+
// Nclk same edge PLUS 'M-3,8 0,17 3,8 C 1,9 -1,9 -3,8 z' (filled, class s6)
|
|
2870
|
+
//
|
|
2871
|
+
// i.e. in the engine's own 20-unit symbol space (y=0 top, y=20
|
|
2872
|
+
// bottom, matching this file's hi/lo), a small filled triangle
|
|
2873
|
+
// straddling local x=0 — `P`'s apex at local y=3 pointing UP
|
|
2874
|
+
// (toward hi), `N`'s apex at local y=17 pointing DOWN (toward lo).
|
|
2875
|
+
// Both sit on the FIRST half-brick, which in this drawing's
|
|
2876
|
+
// one-brick-per-period model (a whole `p`/`P` cycle is ONE `nclk`
|
|
2877
|
+
// path, not two half-bricks — see `isClock`'s own comment) is
|
|
2878
|
+
// always the vertical stroke at x0, the edge INTO this cycle. So
|
|
2879
|
+
// the level char (not the brick id, which is identical for
|
|
2880
|
+
// `p`/`P`) is what has to be read to know an arrow is owed, and
|
|
2881
|
+
// `levels[run.from]` is it — clock bricks never merge (the `!
|
|
2882
|
+
// isClock` guard below), so `run.from === run.to` and this is
|
|
2883
|
+
// exactly the cycle being drawn. `data-clock-edge-<i>`, set above
|
|
2884
|
+
// (this function's own top, next to `data-gaps-<i>`) from the same
|
|
2885
|
+
// `levels` array, is the one place this fact is exposed for a test
|
|
2886
|
+
// to pin — nothing here duplicates it onto the shape itself, so
|
|
2887
|
+
// there is one belief about which cycles have an arrow, not two.
|
|
2888
|
+
//
|
|
2889
|
+
// Reproduced as a plain filled triangle, not the engine's
|
|
2890
|
+
// bezier-curved one: every other brick this file draws is already
|
|
2891
|
+
// a simplified redraw (the bus is a hexagon, the gap is a pair of
|
|
2892
|
+
// chevrons), not a pixel copy of wavedrom's own glyphs. Scaled out
|
|
2893
|
+
// of the engine's 20-unit symbol into this cell's own geometry —
|
|
2894
|
+
// local x by (xm - x0) / 20, local y by (lo - hi) / 20, both
|
|
2895
|
+
// anchored at (x0, hi) — rather than hand-picked pixel offsets, so
|
|
2896
|
+
// it tracks `SIZES.cycleWidth`/`laneHeight` if those ever change.
|
|
2897
|
+
const edgeLevel = levels[run.from];
|
|
2898
|
+
if (edgeLevel === 'P' || edgeLevel === 'N') {
|
|
2899
|
+
const sx = (xm - x0) / 20;
|
|
2900
|
+
const sy = (lo - hi) / 20;
|
|
2901
|
+
const apexY = edgeLevel === 'P' ? hi + 3 * sy : hi + 17 * sy;
|
|
2902
|
+
const baseY = edgeLevel === 'P' ? hi + 12 * sy : hi + 8 * sy;
|
|
2903
|
+
const arrow = d.createElementNS(SVGNS, 'path');
|
|
2904
|
+
arrow.setAttribute('class', 'ed-wave-clock-arrow');
|
|
2905
|
+
arrow.setAttribute('d',
|
|
2906
|
+
'M' + (x0 - 3 * sx) + ',' + baseY +
|
|
2907
|
+
' L' + x0 + ',' + apexY +
|
|
2908
|
+
' L' + (x0 + 3 * sx) + ',' + baseY + ' Z');
|
|
2909
|
+
svg.appendChild(arrow);
|
|
2910
|
+
}
|
|
1711
2911
|
} else {
|
|
1712
2912
|
const y = bandOf(brick)[0];
|
|
1713
2913
|
const line = d.createElementNS(SVGNS, 'line');
|
|
@@ -1755,6 +2955,161 @@
|
|
|
1755
2955
|
}
|
|
1756
2956
|
}
|
|
1757
2957
|
|
|
2958
|
+
/**
|
|
2959
|
+
* Every `edge` entry, drawn after every lane: one `<path>` (`d` from
|
|
2960
|
+
* `geometry.edgeLayout`), a label `<text>` when the entry has one, and —
|
|
2961
|
+
* for `selectedEdge` only — two endpoint handles from
|
|
2962
|
+
* `geometry.edgeHandleRect`, exactly the rectangles `onCanvasDown` below
|
|
2963
|
+
* hit-tests against, so the drawn handle and the clickable one cannot
|
|
2964
|
+
* drift apart.
|
|
2965
|
+
*
|
|
2966
|
+
* A second, invisible, wide-stroke `<path>` (`class="ed-wave-edge-hit"`)
|
|
2967
|
+
* is layered over each visible one purely so it can be clicked: the
|
|
2968
|
+
* visible path is 1px wide, and `document.elementFromPoint` (what
|
|
2969
|
+
* `onCanvasDown` reads to answer "did this press land on a line") would
|
|
2970
|
+
* otherwise miss any press that is not on that exact pixel.
|
|
2971
|
+
* `lib/md2doc.js`'s `.ed-wave-edge-hit` rule sets `pointer-events: stroke`
|
|
2972
|
+
* — what makes a `stroke: transparent` path hit-testable at all, without
|
|
2973
|
+
* which a transparent stroke paints nothing and "nothing painted" is what
|
|
2974
|
+
* `visiblePainted` (the SVG default) refuses to hit.
|
|
2975
|
+
*
|
|
2976
|
+
* Arrow markers are picked from the shape string, MEASURED against
|
|
2977
|
+
* `node_modules/wavedrom/lib/arc-shape.js`'s own `style` table (every
|
|
2978
|
+
* `case` there that sets a `style` string): a shape containing `<` sets
|
|
2979
|
+
* `marker-start:url(#arrowtail)`, one containing `>` sets
|
|
2980
|
+
* `marker-end:url(#arrowhead)`, and `+` is the one shape that sets BOTH
|
|
2981
|
+
* markers, to `url(#tee)` — the seven plain shapes (`-`, `~`, `-~`, `~-`,
|
|
2982
|
+
* `-|`, `|-`, `-|-`) set no `style` at all and draw no arrowhead.
|
|
2983
|
+
* (`applyEdgeMarkers` below is unaffected by the exact count either way —
|
|
2984
|
+
* it tests `indexOf('<')` / `indexOf('>')` rather than enumerating a
|
|
2985
|
+
* list — but this comment's own count should still match the pinned
|
|
2986
|
+
* engine.) `edgeArrowMarker` below reproduces the head/tail split with
|
|
2987
|
+
* ONE shared marker definition rather than two: `orient="auto-start-reverse"`
|
|
2988
|
+
* is what flips a `marker-start` 180° from a `marker-end` automatically,
|
|
2989
|
+
* so the single triangle already points the right way at both ends.
|
|
2990
|
+
*/
|
|
2991
|
+
function renderEdges(svg, doc, layout) {
|
|
2992
|
+
const edges = geometry.edgeLayout(doc, layout);
|
|
2993
|
+
overlay.setAttribute('data-wave-edge-count', String(edges.length));
|
|
2994
|
+
if (edges.length === 0) return edges;
|
|
2995
|
+
|
|
2996
|
+
const defs = d.createElementNS(SVGNS, 'defs');
|
|
2997
|
+
defs.appendChild(edgeArrowMarker());
|
|
2998
|
+
defs.appendChild(edgeTeeMarker());
|
|
2999
|
+
svg.appendChild(defs);
|
|
3000
|
+
|
|
3001
|
+
for (const e of edges) {
|
|
3002
|
+
const isSelected = e.index === selectedEdge;
|
|
3003
|
+
// v3.5.0 Task 8: the edge an in-flight endpoint drag is dragging
|
|
3004
|
+
// keeps drawing at its ACTUAL (still-committed) position — the
|
|
3005
|
+
// document is untouched until the drop (rule 3) — but is faded so
|
|
3006
|
+
// it reads as "not where this will end up" once the dashed preview
|
|
3007
|
+
// (`renderCanvas`, below `renderEdges`'s own call) is standing next
|
|
3008
|
+
// to it. `endpointDrag` can only ever name `selectedEdge` itself
|
|
3009
|
+
// (`render()`'s own clamp), so `isSelected` is already true here too
|
|
3010
|
+
// and this only ever adds a class, never substitutes for `.is-on`.
|
|
3011
|
+
const isDragSource = endpointDrag !== null && e.index === endpointDrag.index;
|
|
3012
|
+
const path = d.createElementNS(SVGNS, 'path');
|
|
3013
|
+
// Every visual property (stroke, fill, width) lives in
|
|
3014
|
+
// `lib/md2doc.js`'s `.ed-wave-edge-path` / `.is-on` CSS, the same
|
|
3015
|
+
// mechanism the rest of this drawing's colours already go through
|
|
3016
|
+
// (`.ed-wave-line`, `.ed-wave-selection`, `.ed-wave-cursor`, …) —
|
|
3017
|
+
// one place to repaint the editor, not two. Only genuinely geometric
|
|
3018
|
+
// attributes (`d`, the marker refs) stay here as attributes.
|
|
3019
|
+
path.setAttribute('class', 'ed-wave-edge-path' + (isSelected ? ' is-on' : '') +
|
|
3020
|
+
(isDragSource ? ' is-drag-source' : ''));
|
|
3021
|
+
path.setAttribute('data-edge-index', String(e.index));
|
|
3022
|
+
path.setAttribute('d', e.d);
|
|
3023
|
+
applyEdgeMarkers(path, e.edge.shape);
|
|
3024
|
+
svg.appendChild(path);
|
|
3025
|
+
|
|
3026
|
+
const hit = d.createElementNS(SVGNS, 'path');
|
|
3027
|
+
hit.setAttribute('class', 'ed-wave-edge-hit');
|
|
3028
|
+
hit.setAttribute('data-edge-index', String(e.index));
|
|
3029
|
+
hit.setAttribute('d', e.d);
|
|
3030
|
+
svg.appendChild(hit);
|
|
3031
|
+
|
|
3032
|
+
if (e.edge.label !== '') {
|
|
3033
|
+
const t = d.createElementNS(SVGNS, 'text');
|
|
3034
|
+
t.setAttribute('class', 'ed-wave-edge-label');
|
|
3035
|
+
t.setAttribute('x', String(e.labelPos.x));
|
|
3036
|
+
t.setAttribute('y', String(e.labelPos.y - 4));
|
|
3037
|
+
// `text-anchor` positions the glyphs against (x, y) — geometry, not
|
|
3038
|
+
// colour — so it stays an attribute, exactly as the existing
|
|
3039
|
+
// `.ed-wave-buslabel` text already does a few lines up.
|
|
3040
|
+
t.setAttribute('text-anchor', 'middle');
|
|
3041
|
+
t.textContent = e.edge.label;
|
|
3042
|
+
svg.appendChild(t);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
if (isSelected) {
|
|
3046
|
+
for (const end of ['from', 'to']) {
|
|
3047
|
+
const r = geometry.edgeHandleRect(e[end]);
|
|
3048
|
+
const h = d.createElementNS(SVGNS, 'rect');
|
|
3049
|
+
h.setAttribute('class', 'ed-wave-edge-handle');
|
|
3050
|
+
h.setAttribute('data-edge-handle', end);
|
|
3051
|
+
h.setAttribute('data-edge-index', String(e.index));
|
|
3052
|
+
h.setAttribute('x', String(r.x));
|
|
3053
|
+
h.setAttribute('y', String(r.y));
|
|
3054
|
+
h.setAttribute('width', String(r.width));
|
|
3055
|
+
h.setAttribute('height', String(r.height));
|
|
3056
|
+
svg.appendChild(h);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return edges;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/** `marker-start` / `marker-end` for one shape — see `renderEdges`'s own
|
|
3064
|
+
* comment for the measurement behind this split. */
|
|
3065
|
+
function applyEdgeMarkers(path, shape) {
|
|
3066
|
+
if (shape === '+') {
|
|
3067
|
+
path.setAttribute('marker-start', 'url(#ed-wave-tee)');
|
|
3068
|
+
path.setAttribute('marker-end', 'url(#ed-wave-tee)');
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
if (shape.indexOf('<') !== -1) path.setAttribute('marker-start', 'url(#ed-wave-arrowhead)');
|
|
3072
|
+
if (shape.indexOf('>') !== -1) path.setAttribute('marker-end', 'url(#ed-wave-arrowhead)');
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
/** One triangle, reused for both ends via `auto-start-reverse`. */
|
|
3076
|
+
function edgeArrowMarker() {
|
|
3077
|
+
const m = d.createElementNS(SVGNS, 'marker');
|
|
3078
|
+
m.setAttribute('id', 'ed-wave-arrowhead');
|
|
3079
|
+
m.setAttribute('viewBox', '0 0 8 8');
|
|
3080
|
+
m.setAttribute('markerWidth', '8');
|
|
3081
|
+
m.setAttribute('markerHeight', '8');
|
|
3082
|
+
m.setAttribute('refX', '7');
|
|
3083
|
+
m.setAttribute('refY', '4');
|
|
3084
|
+
m.setAttribute('orient', 'auto-start-reverse');
|
|
3085
|
+
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
3086
|
+
poly.setAttribute('points', '0,0 8,4 0,8');
|
|
3087
|
+
poly.setAttribute('class', 'ed-wave-edge-arrow');
|
|
3088
|
+
m.appendChild(poly);
|
|
3089
|
+
return m;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
/** The `+` shape's tee: a crossbar perpendicular to the path at either
|
|
3093
|
+
* end, symmetric under a 180° flip so `orient="auto"` needs no reverse. */
|
|
3094
|
+
function edgeTeeMarker() {
|
|
3095
|
+
const m = d.createElementNS(SVGNS, 'marker');
|
|
3096
|
+
m.setAttribute('id', 'ed-wave-tee');
|
|
3097
|
+
m.setAttribute('viewBox', '0 0 8 8');
|
|
3098
|
+
m.setAttribute('markerWidth', '8');
|
|
3099
|
+
m.setAttribute('markerHeight', '8');
|
|
3100
|
+
m.setAttribute('refX', '4');
|
|
3101
|
+
m.setAttribute('refY', '4');
|
|
3102
|
+
m.setAttribute('orient', 'auto');
|
|
3103
|
+
const line = d.createElementNS(SVGNS, 'line');
|
|
3104
|
+
line.setAttribute('x1', '4');
|
|
3105
|
+
line.setAttribute('y1', '0');
|
|
3106
|
+
line.setAttribute('x2', '4');
|
|
3107
|
+
line.setAttribute('y2', '8');
|
|
3108
|
+
line.setAttribute('class', 'ed-wave-edge-tee');
|
|
3109
|
+
m.appendChild(line);
|
|
3110
|
+
return m;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
1758
3113
|
// ── the keyboard on the drawing ───────────────────────────────────────
|
|
1759
3114
|
/**
|
|
1760
3115
|
* Every gesture the drawing has, without a pointer.
|
|
@@ -1807,14 +3162,69 @@
|
|
|
1807
3162
|
moveCursor(key, ev.shiftKey === true);
|
|
1808
3163
|
return true;
|
|
1809
3164
|
}
|
|
3165
|
+
// v3.5.0 Task 7: the keyboard's two-step version of the drag, ahead of
|
|
3166
|
+
// the plain Enter-paints case below so an armed Enter never falls
|
|
3167
|
+
// through to it. First Enter marks `cursor` as `pendingFrom` (arrows
|
|
3168
|
+
// above already moved it, exactly as a mouse press would have);
|
|
3169
|
+
// second Enter — from wherever the cursor walked to since — is the
|
|
3170
|
+
// SAME `finishEdgeDrag` the mouse's `onCanvasUp` calls, so the three
|
|
3171
|
+
// no-op drops and the one-armed-if-nothing-happened rule are decided
|
|
3172
|
+
// in that one place, not re-decided here.
|
|
3173
|
+
if (edgeMode === 'armed' && key === 'Enter' && cursor !== null) {
|
|
3174
|
+
ev.preventDefault();
|
|
3175
|
+
ev.stopPropagation();
|
|
3176
|
+
if (pendingFrom === null) {
|
|
3177
|
+
pendingFrom = { at: cursor.laneIndex, cell: cursor.cycle };
|
|
3178
|
+
say('起點已標記,移到終點再按一次 Enter');
|
|
3179
|
+
render();
|
|
3180
|
+
} else {
|
|
3181
|
+
finishEdgeDrag(cursor.laneIndex, cursor.cycle);
|
|
3182
|
+
}
|
|
3183
|
+
return true;
|
|
3184
|
+
}
|
|
3185
|
+
// v3.5.0 Task 11: Enter on a cell inside a bus box opens that box's
|
|
3186
|
+
// label field instead of painting over it with the brush — the
|
|
3187
|
+
// keyboard's own version of the pointer's click below in
|
|
3188
|
+
// `onCanvasDown`. `resolveBusTarget` is the SAME resolution
|
|
3189
|
+
// `onCanvasDown` uses (fix round 1: a cursor resting on a `.`/`|`
|
|
3190
|
+
// continuation inside a run used to fall straight through to
|
|
3191
|
+
// `paintAtCursor()` below with nothing catching it — the identical
|
|
3192
|
+
// silent-corruption hazard the pointer path had, closed the same way
|
|
3193
|
+
// here rather than only on the mouse), so the two input devices can
|
|
3194
|
+
// never disagree about which cells are labellable or where a press
|
|
3195
|
+
// on a continuation actually lands. Placed AFTER the armed-edge Enter
|
|
3196
|
+
// above (arming an edge outranks opening a label field — the same
|
|
3197
|
+
// press cannot mean both) and BEFORE the generic Enter-paints case
|
|
3198
|
+
// below, which it must pre-empt.
|
|
3199
|
+
if (key === 'Enter' && cursor !== null) {
|
|
3200
|
+
const dlLayout = geometry.layoutOf(store.doc, SIZES);
|
|
3201
|
+
const dlRow = dlLayout.lanes[cursor.laneIndex];
|
|
3202
|
+
const dlLane = dlRow === undefined ? null : dlRow.lane;
|
|
3203
|
+
if (dlLane !== null) {
|
|
3204
|
+
const dlTarget = resolveBusTarget(dlLane, cursor.cycle);
|
|
3205
|
+
if (dlTarget === 'blocked') {
|
|
3206
|
+
ev.preventDefault();
|
|
3207
|
+
ev.stopPropagation();
|
|
3208
|
+
say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3209
|
+
return true;
|
|
3210
|
+
}
|
|
3211
|
+
if (dlTarget !== null) {
|
|
3212
|
+
ev.preventDefault();
|
|
3213
|
+
ev.stopPropagation();
|
|
3214
|
+
cursor = { laneIndex: cursor.laneIndex, cycle: dlTarget.cycle };
|
|
3215
|
+
openDataEdit(cursor.laneIndex, dlTarget.cycle);
|
|
3216
|
+
return true;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
1810
3220
|
// A brush character both PICKS the brush and paints with it. Picking
|
|
1811
3221
|
// without painting would need a second keystroke to do the thing the
|
|
1812
3222
|
// user already said, and the toolbar's own brush buttons are still there
|
|
1813
|
-
// for「選起來待會再用」. `
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
if (
|
|
3223
|
+
// for「選起來待會再用」. `isBrushKey` is the admission rule — see its
|
|
3224
|
+
// own comment (this file's top) for why it is not keyed on
|
|
3225
|
+
// `ev.shiftKey` (v3.5.0 Task 10 fix round 1: that used to block
|
|
3226
|
+
// `P`/`N`/`|`, all typed with Shift, from the keyboard entirely).
|
|
3227
|
+
if (isBrushKey(key)) {
|
|
1818
3228
|
ev.preventDefault();
|
|
1819
3229
|
ev.stopPropagation();
|
|
1820
3230
|
brush = key;
|
|
@@ -1990,8 +3400,213 @@
|
|
|
1990
3400
|
function onCanvasDown(ev) {
|
|
1991
3401
|
if (ev.button !== 0) return;
|
|
1992
3402
|
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3403
|
+
// v3.5.0 Task 7: armed edge-creation claims the press outright, before
|
|
3404
|
+
// the edge-select / deselect / paint dispatch below ever runs — the
|
|
3405
|
+
// whole point of arming is that the NEXT press starts the drag, so it
|
|
3406
|
+
// must not be shadowed by "did this land on an existing edge" or "did
|
|
3407
|
+
// this land on a cell to paint". A press that misses every lane
|
|
3408
|
+
// (`at === null`, e.g. the canvas's own padding) leaves the button
|
|
3409
|
+
// armed rather than eating it for nothing: missing the diagram is not
|
|
3410
|
+
// the same as aiming at the wrong cell, but the recovery is the same —
|
|
3411
|
+
// try the press again.
|
|
3412
|
+
if (edgeMode === 'armed') {
|
|
3413
|
+
const at = cellFromEvent(ev, layout);
|
|
3414
|
+
if (at === null) return;
|
|
3415
|
+
ev.preventDefault();
|
|
3416
|
+
pendingFrom = { at: at.laneIndex, cell: at.cycle };
|
|
3417
|
+
setEdgeMode('dragging');
|
|
3418
|
+
// The keyboard follows the pointer here for the same reason it does
|
|
3419
|
+
// in the paint path below: `cursor` is the ONE notion of "where is
|
|
3420
|
+
// the other end right now" that the mouse move handler, the preview
|
|
3421
|
+
// line and (on the keyboard's own path) `handleDrawingKey` all read,
|
|
3422
|
+
// so drag and keystroke can never disagree about the live endpoint.
|
|
3423
|
+
cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
3424
|
+
canvas.focus();
|
|
3425
|
+
render();
|
|
3426
|
+
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3427
|
+
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
// v3.5.0 Task 8: a press on the SELECTED edge's own handle starts an
|
|
3431
|
+
// endpoint drag, ahead of the plain select/deselect dispatch a few
|
|
3432
|
+
// lines down — that dispatch's own `edgeHitAt` would otherwise see the
|
|
3433
|
+
// exact same press and read it as "re-select the edge that is already
|
|
3434
|
+
// selected", which is a real answer but the wrong one for a press ON
|
|
3435
|
+
// its handle. Recomputed fresh here (`geometry.edgeLayout`, not a
|
|
3436
|
+
// cached list) for the same reason `cellFromEvent`/`edgeHitAt`
|
|
3437
|
+
// already are — a press can never be judged against a layout
|
|
3438
|
+
// `renderCanvas` has moved on from.
|
|
3439
|
+
//
|
|
3440
|
+
// Scoped to `handle.index === selectedEdge` on purpose:
|
|
3441
|
+
// `geometry.edgeHandleAt` answers for EVERY edge's endpoints
|
|
3442
|
+
// regardless of selection (see its own comment), but `renderEdges`
|
|
3443
|
+
// only ever DRAWS handles on the selected one — a press near a
|
|
3444
|
+
// DIFFERENT edge's endpoint has no handle on screen to have landed on,
|
|
3445
|
+
// and falls through to the ordinary select-by-path/point dispatch
|
|
3446
|
+
// below exactly as it always has.
|
|
3447
|
+
if (selectedEdge !== null) {
|
|
3448
|
+
const edges = geometry.edgeLayout(store.doc, layout);
|
|
3449
|
+
const box = canvas.getBoundingClientRect();
|
|
3450
|
+
const handle = geometry.edgeHandleAt(edges, ev.clientX - box.left, ev.clientY - box.top);
|
|
3451
|
+
if (handle !== null && handle.index === selectedEdge) {
|
|
3452
|
+
ev.preventDefault();
|
|
3453
|
+
// Fix round 1, item 1: a self-loop (`from === to`) puts both ends
|
|
3454
|
+
// at the exact same pixel, so `geometry.edgeHandleAt`'s own
|
|
3455
|
+
// `['to', 'from']` iteration always resolves the press to `'to'` —
|
|
3456
|
+
// there is no position this file (or the user) can use to tell
|
|
3457
|
+
// the two apart, and inferring one would just be a guess dressed
|
|
3458
|
+
// up as an answer. What was missing was not a fix, it was the
|
|
3459
|
+
// honest half: before this, the press silently moved `'to'` with
|
|
3460
|
+
// nothing on screen saying that is what happened, or that `'from'`
|
|
3461
|
+
// was never reachable at all.
|
|
3462
|
+
const dragged = edges.filter(function (e) { return e.index === handle.index; })[0];
|
|
3463
|
+
if (dragged !== undefined && dragged.edge.from === dragged.edge.to) {
|
|
3464
|
+
say('這是一條 self-loop(起點跟終點在同一格),兩端的把手疊在同一個位置:' +
|
|
3465
|
+
'只有拖得到的這一端可以搬,另一端目前搬不到。');
|
|
3466
|
+
}
|
|
3467
|
+
endpointDrag = { index: handle.index, end: handle.end, at: null, cell: null };
|
|
3468
|
+
canvas.focus();
|
|
3469
|
+
// Fix round 1, item 2: without a repaint here the press looks
|
|
3470
|
+
// inert until the first `pointermove` — `.is-drag-source` (set
|
|
3471
|
+
// from this same `endpointDrag`, read by `renderEdges`) would not
|
|
3472
|
+
// fade the dragged edge until then, unlike the sibling
|
|
3473
|
+
// `edgeMode === 'armed'` branch above, which repaints immediately
|
|
3474
|
+
// on its own press. `renderCanvas`, not the full `render()`: this
|
|
3475
|
+
// state change touches only the canvas SVG (the faded path; the
|
|
3476
|
+
// dashed landing preview has nothing to draw yet since `at` is
|
|
3477
|
+
// still `null`) — `selectedEdge` is unchanged, so the edge
|
|
3478
|
+
// inspector row and the lane list are both already showing exactly
|
|
3479
|
+
// what they should, and paying for `renderLanes`/`renderEdgeBar`
|
|
3480
|
+
// again would be the same needless cost the existing
|
|
3481
|
+
// `hadSelectedEdge`-gated branch further down in this function
|
|
3482
|
+
// already avoids.
|
|
3483
|
+
renderCanvas(store.doc, layout);
|
|
3484
|
+
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3485
|
+
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3486
|
+
return; // 不塗、不選格
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
// v3.5.0 Task 11 fix round 2 (widened in round 3): a press that lands
|
|
3490
|
+
// on the label's own mark — the `<text>` when the run has a label, or
|
|
3491
|
+
// (round 3) the small `<circle class="ed-wave-buslabel-empty">`
|
|
3492
|
+
// `drawLane` draws in its place when the run has none — opens the
|
|
3493
|
+
// data-label field instead of arming a paint drag, rather than merely
|
|
3494
|
+
// somewhere inside the box the mark sits in. Round 1 gated this on
|
|
3495
|
+
// the CELL (`codec.dataSlotOf`/`resolveBusTarget` alone, asked off
|
|
3496
|
+
// the pressed cycle with no regard to what was actually under the
|
|
3497
|
+
// pointer) and that was reviewer-verified to be its own regression:
|
|
3498
|
+
// T6b paints `N` onto a bus cell with the mouse, and painting a
|
|
3499
|
+
// bus cell is exactly what twenty-two brushes exist for — a gesture
|
|
3500
|
+
// that swallows every press anywhere in the box leaves the user no
|
|
3501
|
+
// pointer route to repaint one at all. The string is what a press on
|
|
3502
|
+
// its mark means to change, so the mark is the target; a press
|
|
3503
|
+
// anywhere else in the SAME box — the polygon, its bevelled ends, a
|
|
3504
|
+
// held continuation's own cell — falls straight through to the
|
|
3505
|
+
// ordinary paint dispatch below exactly as it always has, ROUND 1's
|
|
3506
|
+
// fix included: that fix's `resolveBusTarget` still runs, just gated
|
|
3507
|
+
// on the mark hit rather than on cell membership, so a press on the
|
|
3508
|
+
// label of a merged run still resolves to the run's owning explicit
|
|
3509
|
+
// cycle regardless of which cycle's pixels the mark happens to
|
|
3510
|
+
// straddle (see `resolveBusTarget`'s own comment — its answer is the
|
|
3511
|
+
// same whichever cycle a press on that shared mark geometrically
|
|
3512
|
+
// lands on). Both marks carry the SAME `ed-wave-buslabel` class —
|
|
3513
|
+
// round 3's placeholder ADDS `ed-wave-buslabel-empty` on top of it
|
|
3514
|
+
// rather than replacing it — so this check needed no change at all
|
|
3515
|
+
// to cover the empty case; it was already asking for the class, not
|
|
3516
|
+
// for an element type. `.ed-wave-buslabel`'s `pointer-events:
|
|
3517
|
+
// visiblePainted` (`lib/md2doc.js`) plus paint order — each mark is
|
|
3518
|
+
// appended AFTER its box's `<polygon>` in `drawLane`, so it sits on
|
|
3519
|
+
// top and wins the hit test in its own footprint — is what makes the
|
|
3520
|
+
// two targets disjoint instead of the mark being invisibly swallowed
|
|
3521
|
+
// by the polygon underneath it.
|
|
3522
|
+
//
|
|
3523
|
+
// final review finding 4: this block moved AHEAD of `edgeHitAt` below
|
|
3524
|
+
// (it used to run after it), and its condition now asks
|
|
3525
|
+
// `busLabelHitAt` instead of testing `ev.target` directly.
|
|
3526
|
+
//
|
|
3527
|
+
// Gating `edgeHandleAt` on `selectedEdge` (the change in `edgeHitAt`
|
|
3528
|
+
// itself, above) stops a PHANTOM, undrawn handle from claiming a press
|
|
3529
|
+
// by pure coordinate math — but MEASURED against a real page, that
|
|
3530
|
+
// alone does not make this block reachable: `renderEdges` draws
|
|
3531
|
+
// `.ed-wave-edge-hit` (a 10px-wide, invisible-but-hit-testable stroke,
|
|
3532
|
+
// `pointer-events: stroke`) AFTER every lane's own content, so for any
|
|
3533
|
+
// edge whose endpoint lands on a bus cell, that invisible stroke
|
|
3534
|
+
// paints ON TOP of the cell's `.ed-wave-buslabel` mark. `ev.target` —
|
|
3535
|
+
// the browser's own topmost-element answer — is `path.ed-wave-edge-
|
|
3536
|
+
// hit` at that pixel regardless of source order in THIS function:
|
|
3537
|
+
// moving this block ahead of `edgeHitAt` does not change what
|
|
3538
|
+
// `ev.target` IS, only when it gets asked. Reordering plus the
|
|
3539
|
+
// original `ev.target` check was tried first and measured to still
|
|
3540
|
+
// fail — `elementFromPoint` at the labelled cell's own centre resolved
|
|
3541
|
+
// to the hit path, not the label, exactly as before.
|
|
3542
|
+
//
|
|
3543
|
+
// `busLabelHitAt` answers a different question: not "what is the
|
|
3544
|
+
// TOPMOST element here" but "is there a bus-label mark ANYWHERE in the
|
|
3545
|
+
// stack of elements under this press", via `elementsFromPoint`
|
|
3546
|
+
// (plural) — the standard way to see past one hit-testable layer
|
|
3547
|
+
// sitting on top of another at the same point. That is a real,
|
|
3548
|
+
// measured difference in outcome, not a matter of code order.
|
|
3549
|
+
if (busLabelHitAt(ev) !== null) {
|
|
3550
|
+
const busAt = cellFromEvent(ev, layout);
|
|
3551
|
+
if (busAt !== null) {
|
|
3552
|
+
const busLane = layout.lanes[busAt.laneIndex].lane;
|
|
3553
|
+
const busTarget = resolveBusTarget(busLane, busAt.cycle);
|
|
3554
|
+
// `resolveBusTarget` should never actually answer `null` here —
|
|
3555
|
+
// the label only ever exists over a cycle the codec itself calls
|
|
3556
|
+
// a bus level — but the check is kept rather than assumed, same
|
|
3557
|
+
// reasoning as the `'blocked'` branch just below it.
|
|
3558
|
+
if (busTarget === 'blocked') {
|
|
3559
|
+
ev.preventDefault();
|
|
3560
|
+
say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
if (busTarget !== null) {
|
|
3564
|
+
ev.preventDefault();
|
|
3565
|
+
const busHadSelectedEdge = selectedEdge !== null;
|
|
3566
|
+
selectedEdge = null;
|
|
3567
|
+
cursor = { laneIndex: busAt.laneIndex, cycle: busTarget.cycle };
|
|
3568
|
+
selection = { laneIndex: busAt.laneIndex, from: busTarget.cycle, to: busTarget.cycle };
|
|
3569
|
+
canvas.focus();
|
|
3570
|
+
if (busHadSelectedEdge) render();
|
|
3571
|
+
else renderCanvas(store.doc, layout);
|
|
3572
|
+
openDataEdit(busAt.laneIndex, busTarget.cycle);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
// An edge — its endpoint handle first (it sits ON TOP of the cell it
|
|
3578
|
+
// anchors to, and a selected edge's handle must win the press even
|
|
3579
|
+
// though the cell underneath is perfectly paintable), then its path —
|
|
3580
|
+
// is asked BEFORE the cell/brush dispatch below (but AFTER the bus
|
|
3581
|
+
// label block above — see its own comment for why that one now goes
|
|
3582
|
+
// first). A hit selects and returns without arming `drag`: this
|
|
3583
|
+
// dispatch draws and selects edges; a press on the SELECTED edge's own
|
|
3584
|
+
// handle never reaches here at all (see the drag-start branch above),
|
|
3585
|
+
// so this remains a plain select — it does not itself drag anything.
|
|
3586
|
+
const edgeHit = edgeHitAt(ev, layout);
|
|
3587
|
+
if (edgeHit !== null) {
|
|
3588
|
+
ev.preventDefault();
|
|
3589
|
+
selectedEdge = edgeHit;
|
|
3590
|
+
canvas.focus();
|
|
3591
|
+
render();
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
// A press that hit neither an edge's handle nor its path deselects
|
|
3595
|
+
// whatever WAS selected. The inspector row is the only visible sign a
|
|
3596
|
+
// selection exists, and a selection that outlives a click elsewhere
|
|
3597
|
+
// would let a later 刪除這條線 act on an edge the user has stopped
|
|
3598
|
+
// looking at. This is IN ADDITION to the paint gesture below, on the
|
|
3599
|
+
// very same press — not instead of it.
|
|
3600
|
+
const hadSelectedEdge = selectedEdge !== null;
|
|
3601
|
+
selectedEdge = null;
|
|
1993
3602
|
const at = cellFromEvent(ev, layout);
|
|
1994
|
-
if (at === null)
|
|
3603
|
+
if (at === null) {
|
|
3604
|
+
// Missed the diagram entirely (e.g. the canvas's own padding). Only
|
|
3605
|
+
// the deselect, if there was one to make, needs a repaint; nothing
|
|
3606
|
+
// else here changed.
|
|
3607
|
+
if (hadSelectedEdge) render();
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
1995
3610
|
ev.preventDefault();
|
|
1996
3611
|
drag = { laneIndex: at.laneIndex, from: at.cycle, to: at.cycle };
|
|
1997
3612
|
selection = { laneIndex: at.laneIndex, from: at.cycle, to: at.cycle };
|
|
@@ -2002,12 +3617,54 @@
|
|
|
2002
3617
|
// then pressing `1` would paint wherever the keyboard had been left.
|
|
2003
3618
|
cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
2004
3619
|
canvas.focus();
|
|
2005
|
-
|
|
3620
|
+
// The common case — no edge was selected — keeps the cheap
|
|
3621
|
+
// `renderCanvas`-only repaint this always did. A full `render()` is
|
|
3622
|
+
// only paid on the transition that actually needs it: hiding the
|
|
3623
|
+
// inspector row, which lives in the toolbar `renderCanvas` never
|
|
3624
|
+
// touches.
|
|
3625
|
+
if (hadSelectedEdge) render();
|
|
3626
|
+
else renderCanvas(store.doc, layout);
|
|
2006
3627
|
d.addEventListener('mousemove', onCanvasMove, true);
|
|
2007
3628
|
d.addEventListener('mouseup', onCanvasUp, true);
|
|
2008
3629
|
}
|
|
2009
3630
|
|
|
2010
3631
|
function onCanvasMove(ev) {
|
|
3632
|
+
if (endpointDrag !== null) {
|
|
3633
|
+
// Same abandoned-button recovery as the two drags below: the button
|
|
3634
|
+
// came up somewhere this page never heard about.
|
|
3635
|
+
if (ev.buttons === 0) { onCanvasUp(); return; }
|
|
3636
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3637
|
+
const at = cellFromEvent(ev, layout);
|
|
3638
|
+
// A move that strays off the diagram leaves `at`/`cell` exactly
|
|
3639
|
+
// where they last were, same recovery as `edgeMode === 'dragging'`
|
|
3640
|
+
// below and for the same reason: the drop target the user is
|
|
3641
|
+
// currently over is still the last cell the pointer was actually on
|
|
3642
|
+
// top of, not "nowhere".
|
|
3643
|
+
if (at !== null) {
|
|
3644
|
+
endpointDrag = { index: endpointDrag.index, end: endpointDrag.end,
|
|
3645
|
+
at: at.laneIndex, cell: at.cycle };
|
|
3646
|
+
}
|
|
3647
|
+
// The document is untouched during the drag (rule 3): this repaints
|
|
3648
|
+
// the preview only, never a `commit`.
|
|
3649
|
+
renderCanvas(store.doc, layout);
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
if (edgeMode === 'dragging') {
|
|
3653
|
+
// Same abandoned-button recovery as the paint drag below: the button
|
|
3654
|
+
// came up somewhere this page never heard about.
|
|
3655
|
+
if (ev.buttons === 0) { onCanvasUp(); return; }
|
|
3656
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3657
|
+
const at = cellFromEvent(ev, layout);
|
|
3658
|
+
// A move that strays off the diagram leaves `cursor` — and so the
|
|
3659
|
+
// preview line — exactly where it last was, rather than snapping the
|
|
3660
|
+
// line away: the drop target the user is currently over is still the
|
|
3661
|
+
// last cell the pointer was actually on top of.
|
|
3662
|
+
if (at !== null) cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
3663
|
+
// The document is untouched during the drag (rule 2): this repaints
|
|
3664
|
+
// the preview line only, never a `commit`.
|
|
3665
|
+
renderCanvas(store.doc, layout);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
2011
3668
|
if (drag === null) return;
|
|
2012
3669
|
// The button came up somewhere this page never heard about — outside the
|
|
2013
3670
|
// window, or over the browser's own chrome. Without this the drag stays
|
|
@@ -2032,6 +3689,21 @@
|
|
|
2032
3689
|
function onCanvasUp() {
|
|
2033
3690
|
d.removeEventListener('mousemove', onCanvasMove, true);
|
|
2034
3691
|
d.removeEventListener('mouseup', onCanvasUp, true);
|
|
3692
|
+
if (endpointDrag !== null) {
|
|
3693
|
+
finishEndpointDrag();
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
if (edgeMode === 'dragging') {
|
|
3697
|
+
// `cursor` is never null here: `onCanvasDown` set it to the press
|
|
3698
|
+
// cell before this drag was ever armed, and every move since then
|
|
3699
|
+
// only ever moved it to another real cell (see `onCanvasMove`
|
|
3700
|
+
// above). The null-safe form costs nothing and matches the
|
|
3701
|
+
// `finishEdgeDrag`/`codec.ensureNode` contract exactly rather than
|
|
3702
|
+
// asserting a fact this file does not have to assert.
|
|
3703
|
+
finishEdgeDrag(cursor === null ? null : cursor.laneIndex,
|
|
3704
|
+
cursor === null ? null : cursor.cycle);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
2035
3707
|
if (drag === null) return;
|
|
2036
3708
|
const g = drag;
|
|
2037
3709
|
drag = null;
|
|
@@ -2040,6 +3712,131 @@
|
|
|
2040
3712
|
});
|
|
2041
3713
|
}
|
|
2042
3714
|
|
|
3715
|
+
/**
|
|
3716
|
+
* The end of an endpoint drag on the SELECTED edge — one `commit`, so one
|
|
3717
|
+
* undo step, matching every other gesture in this file (constraint 5).
|
|
3718
|
+
*
|
|
3719
|
+
* Unlike `finishEdgeDrag` below, this has no keyboard-driven twin and
|
|
3720
|
+
* takes no parameters: there is no second input device sharing `cursor`
|
|
3721
|
+
* to aim it, so everything the drop needs already lives in `endpointDrag`
|
|
3722
|
+
* itself, read and cleared in the same first two lines the same way
|
|
3723
|
+
* `finishEdgeDrag` clears `pendingFrom`'s owning mode up front.
|
|
3724
|
+
*
|
|
3725
|
+
* `drag.at === null` means the pointer never landed on a real cell for
|
|
3726
|
+
* the whole gesture (pressed the handle, released off the diagram or
|
|
3727
|
+
* without ever moving) — nothing to drop onto, so this only repaints to
|
|
3728
|
+
* take the (already absent) preview line off screen and does not call
|
|
3729
|
+
* `commit` at all; a `commit` with a non-integer `at` would just be
|
|
3730
|
+
* `codec.moveEdgeEnd`'s own bounds check refusing it one function later
|
|
3731
|
+
* for no benefit.
|
|
3732
|
+
*
|
|
3733
|
+
* Every other landing — including the three no-ops `moveEdgeEnd` itself
|
|
3734
|
+
* refuses (dropping back on the end's own cell, dropping on the cell
|
|
3735
|
+
* holding the edge's OTHER end, and an out-of-range target) — reaches
|
|
3736
|
+
* `commit` exactly once and is judged by `commit`'s own return value,
|
|
3737
|
+
* the same "one test of did-anything-change" rule `finishEdgeDrag`
|
|
3738
|
+
* documents at length below: nothing here re-derives any of those three
|
|
3739
|
+
* checks, that would be a second belief about the same refusal living
|
|
3740
|
+
* next to `moveEdgeEnd`'s own.
|
|
3741
|
+
*/
|
|
3742
|
+
function finishEndpointDrag() {
|
|
3743
|
+
const drag = endpointDrag;
|
|
3744
|
+
endpointDrag = null;
|
|
3745
|
+
if (drag === null) return;
|
|
3746
|
+
if (drag.at === null) { render(); return; }
|
|
3747
|
+
commit('edge-move', function (doc) {
|
|
3748
|
+
return codec.moveEdgeEnd(doc, drag.index, drag.end, drag.at, drag.cell);
|
|
3749
|
+
});
|
|
3750
|
+
render();
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
/**
|
|
3754
|
+
* The end of an edge-creation gesture, reached from both input devices —
|
|
3755
|
+
* the mouse's `onCanvasUp` above and the keyboard's second Enter in
|
|
3756
|
+
* `handleDrawingKey` below. Constraint 4: one function, not two
|
|
3757
|
+
* histories of the same decision.
|
|
3758
|
+
*
|
|
3759
|
+
* `at`/`cell` name the drop cell, or `null`/`null` when the gesture did
|
|
3760
|
+
* not land on any lane at all. Nothing below special-cases the no-op
|
|
3761
|
+
* drops rule 3 lists — dropping back on the start cell, dropping on a
|
|
3762
|
+
* row with no waveform (a spacer — a group's own title is never a row
|
|
3763
|
+
* to drop on in the first place: `codec.isLane` rejects the title
|
|
3764
|
+
* string, so `flattenLanes` never allocates one, and there is no
|
|
3765
|
+
* coordinate here to mis-hit), dropping on the cell that already holds
|
|
3766
|
+
* this edge's OTHER end, and — fix round 1 — repeating the exact same
|
|
3767
|
+
* drag a second time:
|
|
3768
|
+
*
|
|
3769
|
+
* - a spacer's row IS a real, hittable cell (`geometry.cellAt` answers
|
|
3770
|
+
* it like any other), but `codec.ensureNode`'s own lane-local bound
|
|
3771
|
+
* refuses it: an empty `wave` reads as zero cells, so no cycle is
|
|
3772
|
+
* ever in range. The "missed the diagram entirely" case
|
|
3773
|
+
* (`at === null`) falls through the SAME check for a different
|
|
3774
|
+
* reason — neither `at` nor `cell` is an integer `ensureNode`'s
|
|
3775
|
+
* bounds test accepts;
|
|
3776
|
+
* - the start cell and the drop cell landing on the same anchor —
|
|
3777
|
+
* whether because they truly are the same cell, or because the drop
|
|
3778
|
+
* cell is wherever this edge's other end already lives — is exactly
|
|
3779
|
+
* `a.letter === b.letter` below;
|
|
3780
|
+
* - a repeat of this exact gesture between the same two cells would,
|
|
3781
|
+
* without the check below, silently push a byte-identical second
|
|
3782
|
+
* `doc.edge` entry — two arrows drawn exactly on top of each other,
|
|
3783
|
+
* indistinguishable on screen, plus a second undo step for nothing
|
|
3784
|
+
* visible (MEASURED against the real store: drag (0,1)->(0,3) twice,
|
|
3785
|
+
* `doc.edge` becomes `["a~>b","a~>b"]`). Refused here rather than
|
|
3786
|
+
* before `commit`, same as the other three: an entry with the same
|
|
3787
|
+
* four fields (`from`, `to`, `shape`, `label`) already in `doc.edge`
|
|
3788
|
+
* makes this call's own output redundant. This is narrower than
|
|
3789
|
+
* "refuse any second edge between these two anchors" — two
|
|
3790
|
+
* DIFFERENT annotations between the same pair (`a~>b setup` next to
|
|
3791
|
+
* `a-b hold`) are legitimate and stay possible; only a verbatim
|
|
3792
|
+
* repeat of THIS gesture's own fixed output (`shape: '~>'`,
|
|
3793
|
+
* `label: ''`) is refused.
|
|
3794
|
+
*
|
|
3795
|
+
* So there is one test of "did anything change", not four: `commit`'s
|
|
3796
|
+
* own return value. A refused/no-op commit pushes no undo step (`commit`
|
|
3797
|
+
* already guarantees that) and — rule 1 — leaves the gesture `'armed'`
|
|
3798
|
+
* rather than disarming it, because the user pressed the button and got
|
|
3799
|
+
* nothing for it; a real edge disarms back to `'idle'`. Either way this
|
|
3800
|
+
* repaints once more on its own, because `commit` only repaints when it
|
|
3801
|
+
* changed something, and the pending-line preview has to come off the
|
|
3802
|
+
* screen in the no-op case too.
|
|
3803
|
+
*
|
|
3804
|
+
* Fix round 3: `pendingFrom === null` on entry is no longer read as
|
|
3805
|
+
* "nothing to do" and left at that. `carryMarks` can now drop
|
|
3806
|
+
* `pendingFrom` to `null` on its own (round 2, both when a structural
|
|
3807
|
+
* change makes it untraceable and when no `laneTrace` is injected at
|
|
3808
|
+
* all) WITHOUT touching `edgeMode` — before round 2 the only code path
|
|
3809
|
+
* that ever nulled `pendingFrom` was `setEdgeMode('idle')` itself, so
|
|
3810
|
+
* the two could never come apart. Reached from `onCanvasUp` while
|
|
3811
|
+
* `edgeMode === 'dragging'` (the mouse button was down when the
|
|
3812
|
+
* document changed out from under it), leaving `edgeMode` untouched
|
|
3813
|
+
* here would strand it at `'dragging'` forever: `onCanvasMove` and
|
|
3814
|
+
* `onCanvasUp` both check that mode FIRST, so every later paint drag's
|
|
3815
|
+
* `mousemove`/`mouseup` would be swallowed by this function instead of
|
|
3816
|
+
* painting, silently and with no error. So this resyncs the mode to
|
|
3817
|
+
* the mark it depends on before doing anything else.
|
|
3818
|
+
*/
|
|
3819
|
+
function finishEdgeDrag(at, cell) {
|
|
3820
|
+
if (pendingFrom === null) { setEdgeMode('idle'); render(); return; }
|
|
3821
|
+
const from = pendingFrom;
|
|
3822
|
+
const changed = commit('edge-add', function (doc) {
|
|
3823
|
+
const a = codec.ensureNode(doc, from.at, from.cell);
|
|
3824
|
+
if (a.letter === null) return doc;
|
|
3825
|
+
const b = codec.ensureNode(a.doc, at, cell);
|
|
3826
|
+
if (b.letter === null) return doc;
|
|
3827
|
+
if (a.letter === b.letter) return doc; // same anchor both ends
|
|
3828
|
+
const dup = (b.doc.edge || []).some(function (entry) {
|
|
3829
|
+
const e = codec.parseEdge(entry);
|
|
3830
|
+
return e !== null && e.from === a.letter && e.to === b.letter &&
|
|
3831
|
+
e.shape === '~>' && e.label === '';
|
|
3832
|
+
});
|
|
3833
|
+
if (dup) return doc; // byte-identical repeat of this gesture
|
|
3834
|
+
return codec.addEdge(b.doc, { from: a.letter, to: b.letter, shape: '~>', label: '' });
|
|
3835
|
+
});
|
|
3836
|
+
setEdgeMode(changed ? 'idle' : 'armed');
|
|
3837
|
+
render();
|
|
3838
|
+
}
|
|
3839
|
+
|
|
2043
3840
|
/** Pointer coordinates in the drawing's own space, then the codec's cell. */
|
|
2044
3841
|
function cellFromEvent(ev, layout) {
|
|
2045
3842
|
if (canvas === null) return null;
|
|
@@ -2047,6 +3844,137 @@
|
|
|
2047
3844
|
return geometry.cellAt(layout, ev.clientX - box.left, ev.clientY - box.top);
|
|
2048
3845
|
}
|
|
2049
3846
|
|
|
3847
|
+
/**
|
|
3848
|
+
* The `doc.edge` index the press landed on, or `null`.
|
|
3849
|
+
*
|
|
3850
|
+
* Two questions, in order, both against `geometry.edgeLayout(store.doc,
|
|
3851
|
+
* layout)` computed fresh here — the same recompute-don't-cache rule
|
|
3852
|
+
* `cellFromEvent`/`geometry.cellAt` already follow, so a press can never
|
|
3853
|
+
* be judged against a layout `renderCanvas` has moved on from:
|
|
3854
|
+
*
|
|
3855
|
+
* 1. `geometry.edgeHandleAt` — a handle is a small rect centred on an
|
|
3856
|
+
* endpoint, asked in the SAME layout-local space `cellFromEvent`
|
|
3857
|
+
* uses (the canvas's own `getBoundingClientRect()`; `SIZES.nameColWidth`
|
|
3858
|
+
* is 0 in this file, so that space starts at cycle 0 with no offset
|
|
3859
|
+
* to subtract). final review finding 4: trusted when it names the
|
|
3860
|
+
* edge that is actually SELECTED — `renderEdges` only DRAWS a
|
|
3861
|
+
* handle on the selected edge, so its answer for any OTHER edge is a
|
|
3862
|
+
* rectangle with nothing on screen, and letting that phantom
|
|
3863
|
+
* rectangle claim the press is what used to swallow a bus label's
|
|
3864
|
+
* press whenever the label's cell doubled as an unselected edge's
|
|
3865
|
+
* endpoint (see `onCanvasDown`'s `busLabelHitAt` check, which now
|
|
3866
|
+
* runs before this function is even called, for the other half of
|
|
3867
|
+
* that fix). A press within a handle-sized target of the SELECTED
|
|
3868
|
+
* edge's own endpoint still selects it rather than painting the
|
|
3869
|
+
* cell under it. final review re-review, item 1: ALSO trusted when
|
|
3870
|
+
* the named edge is a self-loop (`from === to`), selected or not —
|
|
3871
|
+
* a self-loop's `pathFor` output is a zero-length path (every
|
|
3872
|
+
* command's endpoint equals its start), which Chrome gives no hit
|
|
3873
|
+
* area at all, so #2 below can never find one; the handle is the
|
|
3874
|
+
* only thing a self-loop can ever be selected by, and a self-loop
|
|
3875
|
+
* the user hand-writes into the markdown must stay selectable and
|
|
3876
|
+
* deletable even before it has ever been clicked once.
|
|
3877
|
+
* 2. The `<path class="ed-wave-edge-hit">` twin `renderEdges` layers
|
|
3878
|
+
* over each visible edge, read back through `document.elementFromPoint`
|
|
3879
|
+
* at the press's SCREEN coordinates (no conversion needed — that is
|
|
3880
|
+
* what `elementFromPoint` already takes).
|
|
3881
|
+
*/
|
|
3882
|
+
function edgeHitAt(ev, layout) {
|
|
3883
|
+
if (canvas === null) return null;
|
|
3884
|
+
const edges = geometry.edgeLayout(store.doc, layout);
|
|
3885
|
+
if (edges.length === 0) return null;
|
|
3886
|
+
const box = canvas.getBoundingClientRect();
|
|
3887
|
+
const handle = geometry.edgeHandleAt(edges, ev.clientX - box.left, ev.clientY - box.top);
|
|
3888
|
+
// final review finding 4: only trust the handle answer when it names
|
|
3889
|
+
// the edge that is actually SELECTED — the same scoping the
|
|
3890
|
+
// endpoint-drag branch in `onCanvasDown` already applies, for the same
|
|
3891
|
+
// reason (see its comment): `renderEdges` only ever DRAWS a handle on
|
|
3892
|
+
// the selected edge, so `geometry.edgeHandleAt`'s answer for any OTHER
|
|
3893
|
+
// edge index is a rectangle with nothing on screen at all. Before this,
|
|
3894
|
+
// that undrawn rectangle still won the press here — a bus label whose
|
|
3895
|
+
// cell happened to double as an unselected edge's endpoint could never
|
|
3896
|
+
// open its label editor, because this branch claimed the press first.
|
|
3897
|
+
// An edge stays selectable regardless: the fall-through below still
|
|
3898
|
+
// hit-tests every edge's own 10px `.ed-wave-edge-hit` path.
|
|
3899
|
+
//
|
|
3900
|
+
// final review re-review, item 1: EXCEPT a self-loop (`from === to`),
|
|
3901
|
+
// which this "an edge stays selectable regardless" promise does not
|
|
3902
|
+
// actually cover — MEASURED regression, not a hypothetical: `pathFor`
|
|
3903
|
+
// degenerates to a zero-length path for every shape family when
|
|
3904
|
+
// `from === to` (every `L`/`C` command's endpoint equals its start),
|
|
3905
|
+
// and Chrome gives a zero-length, butt-cap path NO hit area at all —
|
|
3906
|
+
// `elementsFromPoint` returns nothing there, so the `.ed-wave-edge-hit`
|
|
3907
|
+
// fallback below can never find it either. The phantom handle this
|
|
3908
|
+
// finding gates off was the ONLY thing that could ever select a
|
|
3909
|
+
// self-loop that has never been selected before (`selectedEdge` is set
|
|
3910
|
+
// from exactly this function's return value, nowhere else) — a
|
|
3911
|
+
// self-loop the user hand-writes into the markdown (the UI itself
|
|
3912
|
+
// never creates one; `finishEdgeDrag` refuses a same-letter drag) would
|
|
3913
|
+
// otherwise be permanently unselectable and undeletable by mouse. So a
|
|
3914
|
+
// handle match is trusted when it is drawn (`handle.index ===
|
|
3915
|
+
// selectedEdge`, as before) OR when the edge it names is a self-loop —
|
|
3916
|
+
// a self-loop's handle is the one hit target it has, selected or not.
|
|
3917
|
+
if (handle !== null) {
|
|
3918
|
+
if (handle.index === selectedEdge) return handle.index;
|
|
3919
|
+
const named = edges.filter(function (e) { return e.index === handle.index; })[0];
|
|
3920
|
+
if (named !== undefined && named.edge.from === named.edge.to) return handle.index;
|
|
3921
|
+
}
|
|
3922
|
+
if (typeof d.elementFromPoint !== 'function') return null;
|
|
3923
|
+
const el = d.elementFromPoint(ev.clientX, ev.clientY);
|
|
3924
|
+
const hitPath = el !== null && el !== undefined && typeof el.closest === 'function'
|
|
3925
|
+
? el.closest('.ed-wave-edge-hit') : null;
|
|
3926
|
+
if (hitPath === null || !canvas.contains(hitPath)) return null;
|
|
3927
|
+
const idx = hitPath.getAttribute('data-edge-index');
|
|
3928
|
+
return idx === null ? null : Number(idx);
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
/**
|
|
3932
|
+
* The `.ed-wave-buslabel` mark under a press, or `null` — final review
|
|
3933
|
+
* finding 4.
|
|
3934
|
+
*
|
|
3935
|
+
* NOT `ev.target`: `renderEdges` draws `.ed-wave-edge-hit` (invisible,
|
|
3936
|
+
* `pointer-events: stroke`, 10px wide) AFTER every lane's own content, so
|
|
3937
|
+
* whenever an edge's endpoint lands on a bus cell, that stroke paints ON
|
|
3938
|
+
* TOP of the cell's own label mark — `ev.target` at that pixel is the
|
|
3939
|
+
* edge-hit path, never the label underneath it, and no amount of
|
|
3940
|
+
* reordering the CALLS in `onCanvasDown` changes that, because `ev.target`
|
|
3941
|
+
* is the browser's own topmost-element answer, decided at dispatch time
|
|
3942
|
+
* by paint order, not by which `if` this file happens to ask first.
|
|
3943
|
+
* MEASURED against a real page: moving the bus-label check ahead of
|
|
3944
|
+
* `edgeHitAt` while still reading `ev.target` left the label unreachable,
|
|
3945
|
+
* identically to before.
|
|
3946
|
+
*
|
|
3947
|
+
* `document.elementsFromPoint` (plural — distinct from the singular
|
|
3948
|
+
* `elementFromPoint` `edgeHitAt` above reads) answers a different
|
|
3949
|
+
* question: the WHOLE stack of elements under the point, in z-order, not
|
|
3950
|
+
* only the topmost one. Walking it for a `.ed-wave-buslabel` finds the
|
|
3951
|
+
* mark even when something else is painted over it, which is exactly
|
|
3952
|
+
* this finding's fix. The walk stops at `canvas` itself so a label-shaped
|
|
3953
|
+
* class elsewhere in the document (outside the canvas this press is
|
|
3954
|
+
* even inside) can never match.
|
|
3955
|
+
*
|
|
3956
|
+
* Falls back to the old `ev.target`-only check when `elementsFromPoint`
|
|
3957
|
+
* is not available at all (older engines) — that keeps every OTHER bus
|
|
3958
|
+
* label (the overwhelming majority, which do not sit under an edge's
|
|
3959
|
+
* endpoint) working exactly as it always has; only the overlap case this
|
|
3960
|
+
* finding is about stays unfixed on such an engine.
|
|
3961
|
+
*/
|
|
3962
|
+
function busLabelHitAt(ev) {
|
|
3963
|
+
function isLabel(el) {
|
|
3964
|
+
return el !== null && el !== undefined && typeof el.classList === 'object' &&
|
|
3965
|
+
el.classList !== null && el.classList.contains('ed-wave-buslabel');
|
|
3966
|
+
}
|
|
3967
|
+
if (typeof d.elementsFromPoint === 'function') {
|
|
3968
|
+
const stack = d.elementsFromPoint(ev.clientX, ev.clientY);
|
|
3969
|
+
for (const el of stack) {
|
|
3970
|
+
if (isLabel(el)) return el;
|
|
3971
|
+
if (el === canvas) break;
|
|
3972
|
+
}
|
|
3973
|
+
return null;
|
|
3974
|
+
}
|
|
3975
|
+
return isLabel(ev.target) ? ev.target : null;
|
|
3976
|
+
}
|
|
3977
|
+
|
|
2050
3978
|
// ── the preview ───────────────────────────────────────────────────────
|
|
2051
3979
|
/**
|
|
2052
3980
|
* The engine's own rendering of the very same document.
|
|
@@ -2119,6 +4047,7 @@
|
|
|
2119
4047
|
groupSpansOf: groupSpansOf,
|
|
2120
4048
|
mountedInsideContent: mountedInsideContent,
|
|
2121
4049
|
BRUSHES: BRUSHES,
|
|
4050
|
+
isBrushKey: isBrushKey,
|
|
2122
4051
|
SIZES: SIZES,
|
|
2123
4052
|
};
|
|
2124
4053
|
});
|