@helping-ai-workflow/md2doc 3.4.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/editor/client.js +44 -5
- package/lib/editor/server.js +21 -2
- package/lib/editor/wave-codec.js +775 -1
- package/lib/editor/wave-draw.js +1470 -0
- package/lib/editor/wave-geometry.js +647 -14
- package/lib/editor/wave-panels.js +1102 -0
- package/lib/editor/wave-store.js +64 -2
- package/lib/editor/wave-ui.js +2906 -972
- package/lib/md2doc.js +641 -33
- package/package.json +2 -2
package/lib/editor/wave-ui.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* inlined as a plain <script>, not bundled, so a bare require() here would be
|
|
10
10
|
* undefined in the browser — every dependency arrives as a parameter instead
|
|
11
11
|
* (`opts.codec`, `opts.geometry`, `opts.createStore`, `opts.wavedrom`,
|
|
12
|
-
* `opts.document`). That is also why this file
|
|
13
|
-
* layer that knows which `document` and which
|
|
14
|
-
* client.js, and it is the only one that should be
|
|
12
|
+
* `opts.document`, `opts.draw`, `opts.panels`). That is also why this file
|
|
13
|
+
* names no global of its own: the layer that knows which `document` and which
|
|
14
|
+
* engine build are in play is client.js, and it is the only one that should be
|
|
15
|
+
* deciding.
|
|
15
16
|
*
|
|
16
17
|
* FIVE things this file is not allowed to get wrong, each of them a ruling from
|
|
17
18
|
* Tasks 1-5 rather than a preference:
|
|
@@ -57,12 +58,71 @@
|
|
|
57
58
|
|
|
58
59
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The brush palette, exactly the set the spec names, in that order.
|
|
63
|
+
*
|
|
64
|
+
* v3.5.0 Task 10 widened this from 11 to 22: `P`/`N` (explicit-edge
|
|
65
|
+
* clocks — see the arrow measurement in `drawLane`'s isClock branch),
|
|
66
|
+
* `2`-`9` (bus levels beyond the plain `=`, already understood by
|
|
67
|
+
* `brickOf` and `codec.levelsOf` but never offered as a brush), and `|`
|
|
68
|
+
* (the gap marker) placed last because it is not a level at all —
|
|
69
|
+
* `codec.levelsOf('=.|')` measured it as "carry the previous level", so
|
|
70
|
+
* painting it does not change what a cycle shows, only that it shows a
|
|
71
|
+
* discontinuity.
|
|
72
|
+
*/
|
|
73
|
+
const BRUSHES = ['0', '1', 'x', 'z', 'p', 'n', 'P', 'N', 'h', 'l', 'u', 'd',
|
|
74
|
+
'=', '2', '3', '4', '5', '6', '7', '8', '9', '|'];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a `KeyboardEvent.key` value both PICKS and PAINTS a brush —
|
|
78
|
+
* `handleDrawingKey` (below `enterDrawing`) tests this exact call, not a
|
|
79
|
+
* copy of it, so the admission rule and what the keyboard can actually
|
|
80
|
+
* reach cannot drift apart the way the module's own prologue warns about
|
|
81
|
+
* (rule 3: two beliefs about the same thing is the failure this batch
|
|
82
|
+
* keeps producing). Named and exported on its own so a test can pin
|
|
83
|
+
* "every brush is keyboard-reachable" against the real rule instead of a
|
|
84
|
+
* re-implementation of it — see `wave-codec.test.js`'s
|
|
85
|
+
* `isBrushKey`-roster assertion.
|
|
86
|
+
*
|
|
87
|
+
* `key.length === 1` is what actually excludes navigation: every
|
|
88
|
+
* `BRUSHES` entry is one character, while `ArrowLeft`/`ArrowRight`/
|
|
89
|
+
* `ArrowUp`/`ArrowDown`/`Home`/`End` are multi-character key NAMES, so
|
|
90
|
+
* `BRUSHES.indexOf(key)` is already `-1` for all six regardless of this
|
|
91
|
+
* length check — it is kept anyway so that invariant is spelled out at
|
|
92
|
+
* the point of use rather than left to rely on `BRUSHES` never growing a
|
|
93
|
+
* multi-character entry by mistake. Deliberately NOT keyed on
|
|
94
|
+
* `ev.shiftKey`: v3.5.0 Task 10 added `P`/`N` (and `|` sits next to `\`
|
|
95
|
+
* on a US layout) to `BRUSHES`, and all three are typed WITH Shift on an
|
|
96
|
+
* ordinary keyboard, so a shiftKey exclusion here would make them
|
|
97
|
+
* mouse-only — see Task 10's fix-round report for the full reasoning on
|
|
98
|
+
* why dropping it does not reopen the Shift+arrow risk the old comment
|
|
99
|
+
* named (that risk is excluded above, by `nav`, on `key` identity, not on
|
|
100
|
+
* `ev.shiftKey`).
|
|
101
|
+
*/
|
|
102
|
+
function isBrushKey(key) {
|
|
103
|
+
return typeof key === 'string' && key.length === 1 && BRUSHES.indexOf(key) !== -1;
|
|
104
|
+
}
|
|
62
105
|
|
|
63
106
|
/** Drawing sizes. Handed to `wave-geometry` so the hit test and the paint
|
|
64
|
-
* cannot drift; nothing here re-derives a row position.
|
|
65
|
-
|
|
107
|
+
* cannot drift; nothing here re-derives a row position.
|
|
108
|
+
*
|
|
109
|
+
* v3.6.0 Task 6: nameColWidth was 0 — the canvas always supported a name
|
|
110
|
+
* column (`wave-geometry.DEFAULTS` carried 40), the UI was just passing 0.
|
|
111
|
+
* 120px: the canvas needs to say what a lane is called, not lean on the
|
|
112
|
+
* rail beside it. Task 8 re-measures this against the longest lane name in
|
|
113
|
+
* the repo's fixtures and may change it — this value is not re-derived
|
|
114
|
+
* here.
|
|
115
|
+
*
|
|
116
|
+
* v3.6.0 Task 8 — MEASURED: the longest `name:` fixture across
|
|
117
|
+
* test/editor-journey.test.js, test/wave-codec.test.js and
|
|
118
|
+
* test/wave-geometry.test.js is 12 chars (`he said "hi"`). Rendered as an
|
|
119
|
+
* SVG `<text>` at 11px (`wave-draw.js`'s `.ed-wave-lane-label`) it measures
|
|
120
|
+
* 54.9px in the app's default sans-serif stack and 79.5px in the worst
|
|
121
|
+
* case tried (monospace) — both comfortably under the 110px a top-level
|
|
122
|
+
* (depth 0) name gets (`nameColWidth 120 - NAME_GAP 10`), so 120 stays.
|
|
123
|
+
* See task-8-report.md for the full measurement (puppeteer,
|
|
124
|
+
* `getComputedTextLength`). */
|
|
125
|
+
const SIZES = { laneHeight: 34, cycleWidth: 48, nameColWidth: 120 };
|
|
66
126
|
|
|
67
127
|
/**
|
|
68
128
|
* The index the preview is rendered under, and why it is not 0.
|
|
@@ -89,52 +149,6 @@
|
|
|
89
149
|
const PREVIEW_INDEX = 9000;
|
|
90
150
|
const PREVIEW_ID_PREFIX = 'md2doc-wave-preview-';
|
|
91
151
|
|
|
92
|
-
/**
|
|
93
|
-
* What wavedrom 3.5.0 actually draws for one level, by its own brick id.
|
|
94
|
-
*
|
|
95
|
-
* Every row was MEASURED in this session against the pinned engine, by
|
|
96
|
-
* rendering `wave: 'z' + c + c` (the `z` anchors the char so it is never at
|
|
97
|
-
* cycle 0) and reading the second half-brick of each cycle out of the
|
|
98
|
-
* rendered SVG's `<use xlink:href="#…">` list:
|
|
99
|
-
*
|
|
100
|
-
* 0 000 1 111 x xxx z zzz u uuu d ddd
|
|
101
|
-
* h 111 l 000 H 111 L 000
|
|
102
|
-
* p nclk P nclk n pclk N pclk
|
|
103
|
-
* = vvv-2 2 vvv-2 3 vvv-3 … 9 vvv-9
|
|
104
|
-
* anything else xxx (measured with 'Q' and 'ä')
|
|
105
|
-
*
|
|
106
|
-
* So this is not a re-implementation of the engine's level model — `levelsOf`
|
|
107
|
-
* is that, and it lives in the codec. This is the last step: which PICTURE
|
|
108
|
-
* the engine paints for a level it has already decided. Keeping it in
|
|
109
|
-
* production code rather than in the test is deliberate: the drawing has to
|
|
110
|
-
* make exactly this decision anyway (an `h` lane is drawn high, an `=` lane
|
|
111
|
-
* is drawn as a bus), and a copy of it living only in a test would be a
|
|
112
|
-
* second belief about the engine with nothing pinning it to the first.
|
|
113
|
-
*/
|
|
114
|
-
function brickOf(level) {
|
|
115
|
-
if (typeof level !== 'string' || level.length === 0) return 'xxx';
|
|
116
|
-
if (level === '0' || level === 'l' || level === 'L') return '000';
|
|
117
|
-
if (level === '1' || level === 'h' || level === 'H') return '111';
|
|
118
|
-
if (level === 'z') return 'zzz';
|
|
119
|
-
if (level === 'u') return 'uuu';
|
|
120
|
-
if (level === 'd') return 'ddd';
|
|
121
|
-
if (level === 'p' || level === 'P') return 'nclk';
|
|
122
|
-
if (level === 'n' || level === 'N') return 'pclk';
|
|
123
|
-
if (level === '=') return 'vvv-2';
|
|
124
|
-
if (level >= '2' && level <= '9') return 'vvv-' + level;
|
|
125
|
-
return 'xxx';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** A brick that draws a whole clock period and therefore never merges with
|
|
129
|
-
* its neighbour: two `p` cycles are two clock periods, not one long one. */
|
|
130
|
-
function isClock(brick) {
|
|
131
|
-
return brick === 'nclk' || brick === 'pclk';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function isBus(brick) {
|
|
135
|
-
return brick.slice(0, 4) === 'vvv-';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
152
|
/**
|
|
139
153
|
* Whether `el` is inside the editor's serialisation surface.
|
|
140
154
|
*
|
|
@@ -152,28 +166,6 @@
|
|
|
152
166
|
return false;
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
/**
|
|
156
|
-
* A plain, unfrozen copy of a document.
|
|
157
|
-
*
|
|
158
|
-
* The store's documents are deep-frozen — that is what stops a stray write
|
|
159
|
-
* from corrupting an undo entry — and wavedrom's renderer WRITES INTO the
|
|
160
|
-
* source object it is handed. Under this file's strict prologue that is a
|
|
161
|
-
* throw, so the preview gets its own copy every time. Bounded by the codec's
|
|
162
|
-
* own nesting ceiling so a pathological document cannot run the stack out
|
|
163
|
-
* here either.
|
|
164
|
-
*/
|
|
165
|
-
function plainCopy(value, depth) {
|
|
166
|
-
if (depth > 64 || value === null || typeof value !== 'object') return value;
|
|
167
|
-
if (Array.isArray(value)) {
|
|
168
|
-
const out = [];
|
|
169
|
-
for (let i = 0; i < value.length; i++) out.push(plainCopy(value[i], depth + 1));
|
|
170
|
-
return out;
|
|
171
|
-
}
|
|
172
|
-
const out = {};
|
|
173
|
-
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], depth + 1);
|
|
174
|
-
return out;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
169
|
/** Line and column of a byte offset, 1-based — what a parse failure has to
|
|
178
170
|
* say beyond its own message for the offset to mean anything to a person. */
|
|
179
171
|
function whereIs(text, offset) {
|
|
@@ -182,59 +174,14 @@
|
|
|
182
174
|
return { line: lines.length, column: lines[lines.length - 1].length + 1 };
|
|
183
175
|
}
|
|
184
176
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
* would put the lane list one row out of step with the waveform beside it for
|
|
194
|
-
* every lane below the group, which is exactly the class of silent
|
|
195
|
-
* disagreement this batch keeps producing.
|
|
196
|
-
*
|
|
197
|
-
* The span is derived from the lanes' own paths — `layout.lanes[i].path` is
|
|
198
|
-
* the codec's — so a group with no lanes under it has no span and is not
|
|
199
|
-
* shown here; it is not addressable through the bridge either.
|
|
200
|
-
*/
|
|
201
|
-
function groupSpansOf(doc, layout) {
|
|
202
|
-
const byKey = new Map();
|
|
203
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
204
|
-
const path = layout.lanes[i].path;
|
|
205
|
-
// Every proper ancestor between `signal` and the lane itself is a group.
|
|
206
|
-
for (let cut = 2; cut < path.length; cut++) {
|
|
207
|
-
const prefix = path.slice(0, cut);
|
|
208
|
-
const key = JSON.stringify(prefix);
|
|
209
|
-
const seen = byKey.get(key);
|
|
210
|
-
if (seen === undefined) {
|
|
211
|
-
byKey.set(key, { path: prefix, depth: cut - 1, from: i, to: i });
|
|
212
|
-
} else {
|
|
213
|
-
seen.to = i;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const out = [];
|
|
218
|
-
for (const span of byKey.values()) {
|
|
219
|
-
const container = valueAt(doc, span.path);
|
|
220
|
-
const title = Array.isArray(container) && typeof container[0] === 'string'
|
|
221
|
-
? container[0] : null;
|
|
222
|
-
if (title === null) continue; // an untitled group has nothing to show
|
|
223
|
-
out.push({ path: span.path, title: title, depth: span.depth,
|
|
224
|
-
from: span.from, to: span.to });
|
|
225
|
-
}
|
|
226
|
-
out.sort(function (a, b) { return a.from - b.from || a.depth - b.depth; });
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function valueAt(doc, path) {
|
|
231
|
-
let node = doc;
|
|
232
|
-
for (const seg of path) {
|
|
233
|
-
if (node === null || typeof node !== 'object') return undefined;
|
|
234
|
-
node = node[seg];
|
|
235
|
-
}
|
|
236
|
-
return node;
|
|
237
|
-
}
|
|
177
|
+
// `plainCopy`, `groupSpansOf`, `directFirstLaneOf` and `valueAt` used to
|
|
178
|
+
// live here. v3.6.0 Task 2 moved `plainCopy` and `directFirstLaneOf` to
|
|
179
|
+
// `wave-panels.js` with the rendering functions that were their only
|
|
180
|
+
// callers (and Task 14 then deleted `directFirstLaneOf` outright, with the
|
|
181
|
+
// per-row 解散群組 button that was its one caller); `groupSpansOf` and
|
|
182
|
+
// `valueAt` moved to `wave-geometry.js` per ruling R9, since both
|
|
183
|
+
// `wave-panels.js` and `wave-draw.js` need them and that module is the one
|
|
184
|
+
// place both already depend on.
|
|
238
185
|
|
|
239
186
|
/** The labels a lane's `data` supplies, one per explicit value cycle. */
|
|
240
187
|
function labelsOf(lane) {
|
|
@@ -285,9 +232,63 @@
|
|
|
285
232
|
throw new Error('wave-ui: the overlay may not be mounted inside .content ' +
|
|
286
233
|
'(its children are serialised back into the user markdown)');
|
|
287
234
|
}
|
|
235
|
+
// `opts.draw` (wave-draw.js's `createDrawer`) arrives as a parameter like
|
|
236
|
+
// every other dependency here, per this file's own header comment — a
|
|
237
|
+
// bare `require('./wave-draw.js')` would be undefined in the browser,
|
|
238
|
+
// where wave-ui.js is injected as a plain <script> with no shim of its
|
|
239
|
+
// own (see lib/editor/server.js). Missing it fails LOUD rather than
|
|
240
|
+
// quietly drawing nothing: a diagram with no drawing layer behind it must
|
|
241
|
+
// never look like a diagram that simply has nothing to draw.
|
|
242
|
+
if (!opts.draw) {
|
|
243
|
+
throw new Error('wave-ui: opts.draw (wave-draw.js\'s createDrawer) is required');
|
|
244
|
+
}
|
|
245
|
+
// v3.6.0 Task 2: `opts.panels` (wave-panels.js's `createPanels`) is the
|
|
246
|
+
// toolbar/lane-rail/right-hand-panels counterpart to `opts.draw` just
|
|
247
|
+
// above, arriving as a parameter for the identical reason — no shim for
|
|
248
|
+
// a bare `require('./wave-panels.js')` in the browser injection path.
|
|
249
|
+
if (!opts.panels) {
|
|
250
|
+
throw new Error('wave-ui: opts.panels (wave-panels.js\'s createPanels) is required');
|
|
251
|
+
}
|
|
288
252
|
|
|
289
253
|
const store = opts.createStore(opts.source);
|
|
290
254
|
|
|
255
|
+
/**
|
|
256
|
+
* v3.6.0 Task 13: the 原始碼 panel's whole reason to exist — the exact
|
|
257
|
+
* bytes 保留並關閉 would write right now, not a re-serialisation made
|
|
258
|
+
* for display. The SAME `store.toPatch()` path `afterStoreMoved` already
|
|
259
|
+
* runs after every gesture (`lastPatch`, below), recomputed fresh here
|
|
260
|
+
* rather than reusing that cached value: this can be asked before the
|
|
261
|
+
* very first gesture, while `lastPatch` is still `null`.
|
|
262
|
+
*
|
|
263
|
+
* A refusal is shown as a sentence, not thrown — the same "say what it
|
|
264
|
+
* cannot do" rule `renderUnmodelled` and the unreadable-source panel
|
|
265
|
+
* both already follow, rather than crashing a panel over a state the
|
|
266
|
+
* user can act on (undo one step, or make a smaller change).
|
|
267
|
+
*/
|
|
268
|
+
function sourceText() {
|
|
269
|
+
const patch = store.toPatch();
|
|
270
|
+
return patch.ok === true ? patch.text
|
|
271
|
+
: '(這個改動目前沒辦法只改幾個位元組寫回去:' + patch.reason + ')';
|
|
272
|
+
}
|
|
273
|
+
// Test-visible, same convention `client.js` already uses for its own
|
|
274
|
+
// `window.__edTest*` probes: a plain global a Puppeteer test can call
|
|
275
|
+
// directly, assigned through `d.defaultView` rather than a bare
|
|
276
|
+
// `window` reference — this file names no global of its own (see its
|
|
277
|
+
// header) and is reachable from node with a fake `opts.document`, where
|
|
278
|
+
// a bare `window` would throw.
|
|
279
|
+
const probeWin = d.defaultView;
|
|
280
|
+
if (probeWin) probeWin.__edWaveSourceProbe = sourceText;
|
|
281
|
+
|
|
282
|
+
const drawer = opts.draw.createDrawer({
|
|
283
|
+
d: d,
|
|
284
|
+
geometry: geometry,
|
|
285
|
+
codec: codec,
|
|
286
|
+
SIZES: SIZES,
|
|
287
|
+
SVGNS: SVGNS,
|
|
288
|
+
BRUSHES: BRUSHES,
|
|
289
|
+
labelsOf: labelsOf,
|
|
290
|
+
});
|
|
291
|
+
|
|
291
292
|
const overlay = d.createElement('div');
|
|
292
293
|
overlay.className = 'ed-wave-overlay';
|
|
293
294
|
overlay.setAttribute('role', 'dialog');
|
|
@@ -302,15 +303,32 @@
|
|
|
302
303
|
// closing.
|
|
303
304
|
let brush = '1';
|
|
304
305
|
let clip = null;
|
|
305
|
-
|
|
306
|
+
// v3.6.0 Task 14: `{laneIndex, laneTo, from, to}` — THE selection, and
|
|
307
|
+
// the only one this dialog has. `laneIndex`..`laneTo` (inclusive,
|
|
308
|
+
// normalised so `laneTo >= laneIndex`) is the lane range the 訊號/群組
|
|
309
|
+
// sections act on; `from`..`to` is the cycle range on `laneIndex` that
|
|
310
|
+
// the 週期 section and the brushes act on. An ordinary selection has
|
|
311
|
+
// `laneTo === laneIndex`; only Shift+click on the rail widens it.
|
|
312
|
+
//
|
|
313
|
+
// Every producer writes all four keys. That is deliberate rather than
|
|
314
|
+
// "absent means single lane": this repo has already paid for one value
|
|
315
|
+
// whose two ends disagreed about what `undefined` meant (wavedrom's own
|
|
316
|
+
// `captext` vs `ticktock`), and a reader here does
|
|
317
|
+
// `i >= laneIndex && i <= laneTo` with no second rule to remember.
|
|
318
|
+
let selection = null;
|
|
306
319
|
let drag = null; // live pointer drag, never pushed until mouseup
|
|
307
320
|
let gestures = 0;
|
|
308
321
|
let lastPatch = null;
|
|
309
322
|
// Where the keyboard should end up after the NEXT repaint, when that is not
|
|
310
|
-
// simply "wherever it was". A
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// to move the same lane twice, not two different ones.
|
|
323
|
+
// simply "wherever it was". A gesture that MOVES a lane is the case that
|
|
324
|
+
// needs it: whatever the keyboard was on belongs to a row, and after the
|
|
325
|
+
// move the lane the user is pushing around sits at a different row — so
|
|
326
|
+
// Alt+↓ twice has to move the same lane twice, not two different ones.
|
|
327
|
+
// (v3.6.0 Task 16 re-pointed this sentence: it used to name the rail's
|
|
328
|
+
// per-row ▲, which Task 14 removed; `moveSelectedLane` now carries the
|
|
329
|
+
// same fact and nominates the moved lane's own name field. The narrative
|
|
330
|
+
// further down about what ▲/▼ USED to do is a record of a past version
|
|
331
|
+
// and is deliberately left as it was.)
|
|
314
332
|
let focusOverride = null;
|
|
315
333
|
// Where the KEYBOARD is on the drawing: one cell, and the keyboard's whole
|
|
316
334
|
// equivalent of the pointer. Null until the keyboard has actually entered
|
|
@@ -321,6 +339,102 @@
|
|
|
321
339
|
// never end up pointing at different cells: painting with `1` after a click
|
|
322
340
|
// paints the cell that was clicked.
|
|
323
341
|
let cursor = null;
|
|
342
|
+
// The `edge` array entry the inspector row edits — the codec's ORIGINAL
|
|
343
|
+
// index into `doc.edge`, exactly what `geometry.edgeLayout` reports as
|
|
344
|
+
// `index` (NOT the filtered array position; a dangling entry ahead of it
|
|
345
|
+
// is skipped there and would otherwise shift everything after it out of
|
|
346
|
+
// step). Null means no edge is selected, and that is the whole of the
|
|
347
|
+
// inspector row's visibility rule — see `renderEdgeBar`. `render()`
|
|
348
|
+
// clamps it to null the moment the edge it names stops being drawable
|
|
349
|
+
// (deleted, or its entry no longer parses), so a stale index can never
|
|
350
|
+
// reach `renderEdgeBar` or a shape/label commit.
|
|
351
|
+
let selectedEdge = null;
|
|
352
|
+
// v3.6.0 Task 13 fix round 3: the PREVIOUS render's `selectedEdge`, so
|
|
353
|
+
// `render()` below can tell "just became selected" (null -> a number)
|
|
354
|
+
// apart from "still selected, repainting for some other reason" (a
|
|
355
|
+
// number -> the same or a different number) — see the comment at that
|
|
356
|
+
// check for why the distinction matters.
|
|
357
|
+
let lastSelectedEdge = null;
|
|
358
|
+
// v3.5.0 Task 7: the edge-CREATION gesture's own state, shared verbatim
|
|
359
|
+
// by the pointer and the keyboard — one state machine, two input sources.
|
|
360
|
+
// `edgeMode` is explicit rather than inferred from "is a key held" or "is
|
|
361
|
+
// the mouse button down", for the same reason the brush canvas keeps
|
|
362
|
+
// `drag` as its own variable instead of asking `ev.buttons`: a painting
|
|
363
|
+
// drag and an edge-creation drag must never be read as each other, and a
|
|
364
|
+
// named mode is what lets `onCanvasDown` choose between them up front.
|
|
365
|
+
// 'idle' — the 關聯線 button is off; every press paints or selects.
|
|
366
|
+
// 'armed' — pressed once; the NEXT thing (a press, or a cell plus
|
|
367
|
+
// Enter) marks the start of a line.
|
|
368
|
+
// 'dragging' — the mouse is down after that press; every move only
|
|
369
|
+
// repaints a preview, the document is untouched.
|
|
370
|
+
// `pendingFrom` is the marked start cell, `{at, cell}` in the same two
|
|
371
|
+
// numbers `codec.ensureNode`/`geometry.cellRect` already take — the
|
|
372
|
+
// codec's flattened lane index and the cycle inside it. It is cleared
|
|
373
|
+
// only when `setEdgeMode` moves all the way back to 'idle': going back to
|
|
374
|
+
// 'armed' after a drop that produced nothing (rule 1 below) deliberately
|
|
375
|
+
// LEAVES it standing, so a keyboard user who marked a start and then
|
|
376
|
+
// pressed Enter over an empty row does not have to re-mark it — the next
|
|
377
|
+
// Enter still means "finish from where I already stood".
|
|
378
|
+
let edgeMode = 'idle';
|
|
379
|
+
let pendingFrom = null;
|
|
380
|
+
// v3.6.0 Task 11: which transition anchor the pointer is nearest while
|
|
381
|
+
// armed — `{laneIndex, cell}`, `geometry.boundaryAt`'s own field names,
|
|
382
|
+
// never converted to `cellFromEvent`'s `cycle` (see that function's own
|
|
383
|
+
// comment). Cleared whenever `edgeMode` leaves 'armed' for any reason
|
|
384
|
+
// (idle or dragging) so a stale hot dot never survives past the state
|
|
385
|
+
// that produced it — set alongside `edgeMode` in `setEdgeMode` itself,
|
|
386
|
+
// the one place that already keeps every other edge-gesture mark
|
|
387
|
+
// (`pendingFrom`) in sync with it.
|
|
388
|
+
let hoverBoundary = null;
|
|
389
|
+
/** The one place `edgeMode` changes. Keeps the arm button's own visual
|
|
390
|
+
* state, the overlay's `data-wave-edgemode` (what a test or a future
|
|
391
|
+
* task reads instead of reaching into this closure) and the "is a start
|
|
392
|
+
* cell marked" invariant all moving together. */
|
|
393
|
+
function setEdgeMode(next) {
|
|
394
|
+
edgeMode = next;
|
|
395
|
+
edgeBtn.setAttribute('aria-pressed', next === 'idle' ? 'false' : 'true');
|
|
396
|
+
edgeBtn.classList.toggle('is-on', next !== 'idle');
|
|
397
|
+
overlay.setAttribute('data-wave-edgemode', next);
|
|
398
|
+
if (next === 'idle') pendingFrom = null;
|
|
399
|
+
// v3.6.0 Task 11: the hover dot only ever means anything while
|
|
400
|
+
// 'armed' (dragging repaints its own hot dot via `onCanvasMove`'s
|
|
401
|
+
// `at`, computed fresh every move) — leaving 'armed' for either
|
|
402
|
+
// neighbour state without clearing this would let a stale hot dot
|
|
403
|
+
// from the last hover survive into a repaint nothing is tracking it
|
|
404
|
+
// for any more.
|
|
405
|
+
if (next !== 'armed') hoverBoundary = null;
|
|
406
|
+
}
|
|
407
|
+
// v3.5.0 Task 8: dragging either END of the SELECTED edge to another
|
|
408
|
+
// cell. `{index, end, at, cell} | null` — `index` is the same original
|
|
409
|
+
// `doc.edge` index `selectedEdge` names (a drag can only start from that
|
|
410
|
+
// edge's own drawn handle, see `onCanvasDown`), `end` is `'from'` or
|
|
411
|
+
// `'to'`, and `at`/`cell` are the live drop target, `null` until the
|
|
412
|
+
// first `pointermove` lands on a real cell.
|
|
413
|
+
//
|
|
414
|
+
// Unlike `edgeMode`/`pendingFrom` — two separate variables that Task 7
|
|
415
|
+
// spent three fix rounds re-synchronising after one could go stale while
|
|
416
|
+
// the other stood — this is ONE variable that is both "is a drag
|
|
417
|
+
// happening" and "what does it know so far". There is no second flag to
|
|
418
|
+
// fall out of step with it: `onCanvasMove`/`onCanvasUp` test this value
|
|
419
|
+
// itself, so nulling it is the whole of cancelling the gesture, in one
|
|
420
|
+
// place, atomically. It still has to be threaded through the same two
|
|
421
|
+
// places Task 7's marks are, though, because the hazard those rounds
|
|
422
|
+
// found is not particular to `pendingFrom` — it is "a mark naming a lane
|
|
423
|
+
// outlives the document that lane numbering was valid in":
|
|
424
|
+
// - `carryMarks` below carries `at` by lane IDENTITY, exactly like
|
|
425
|
+
// `pendingFrom.at`, for the same reason: a structural undo/redo can
|
|
426
|
+
// land while this drag is in flight (the keyboard stays live while a
|
|
427
|
+
// mouse button is held — Ctrl+Z does not care what the mouse is
|
|
428
|
+
// doing), and a raw row number surviving that would let the eventual
|
|
429
|
+
// drop's `moveEdgeEnd` bounds-check successfully against a lane the
|
|
430
|
+
// user was never actually pointing at.
|
|
431
|
+
// - `render()`'s own `selectedEdge` clamp is widened to drop this too:
|
|
432
|
+
// `index` can only ever be reached from `selectedEdge`'s own drawn
|
|
433
|
+
// handle, so the two may never disagree — a drag whose edge stops
|
|
434
|
+
// being drawable (deleted, or its selection cleared by that same
|
|
435
|
+
// mid-drag undo) has to die with it rather than commit a move onto
|
|
436
|
+
// whatever entry now happens to sit at that array position.
|
|
437
|
+
let endpointDrag = null;
|
|
324
438
|
// The document the marks' lane numbers are numbers INTO.
|
|
325
439
|
//
|
|
326
440
|
// `cursor` and `selection` name a ROW, and a row number means a different
|
|
@@ -344,6 +458,14 @@
|
|
|
344
458
|
// a rename field ran `close('escape')` and threw the WHOLE session away,
|
|
345
459
|
// which is the most expensive answer available to「我改名改到一半不想改了」.
|
|
346
460
|
let groupRename = null;
|
|
461
|
+
// v3.5.0 Task 11: the bus data-label field while it is open — this
|
|
462
|
+
// dialog's OTHER sub-panel, same shape as `groupRename` above (a
|
|
463
|
+
// `{cancel, retire}` pair) and layered the same way in `onKeyDown`'s
|
|
464
|
+
// Escape handling. Unlike the rail's rename field, this one's node lives
|
|
465
|
+
// beside the canvas SVG that `renderCanvas` rebuilds on every cursor
|
|
466
|
+
// move, not only on a structural edit — so `retire()` is called from
|
|
467
|
+
// THERE, not only from `render()` (see `renderCanvas`'s own first line).
|
|
468
|
+
let dataEdit = null;
|
|
347
469
|
|
|
348
470
|
// ── the modal focus model ─────────────────────────────────────────────
|
|
349
471
|
//
|
|
@@ -461,6 +583,7 @@
|
|
|
461
583
|
if (closed) return;
|
|
462
584
|
closed = true;
|
|
463
585
|
drag = null;
|
|
586
|
+
endpointDrag = null;
|
|
464
587
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
465
588
|
d.removeEventListener('keydown', onKeyDown, true);
|
|
466
589
|
d.removeEventListener('mousemove', onCanvasMove, true);
|
|
@@ -530,6 +653,27 @@
|
|
|
530
653
|
// user had made, and the one Ctrl+Z that takes a discard back is not
|
|
531
654
|
// something the keystroke announces.
|
|
532
655
|
if (groupRename !== null) { groupRename.cancel(); return; }
|
|
656
|
+
// v3.5.0 Task 11: the bus data-label field, this dialog's OTHER
|
|
657
|
+
// text sub-panel — same layering reason as `groupRename` just
|
|
658
|
+
// above: without this, Escape typed while labelling a bus value
|
|
659
|
+
// discarded the whole editing session instead of just the field.
|
|
660
|
+
if (dataEdit !== null) { dataEdit.cancel(); return; }
|
|
661
|
+
// v3.5.0 Task 7: the armed/dragging edge gesture is this dialog's
|
|
662
|
+
// OTHER sub-panel-shaped thing — not a text field, but still a
|
|
663
|
+
// half-done action standing in front of the dialog's own Escape.
|
|
664
|
+
// Cancelling it never touches the document (rule 2: nothing was
|
|
665
|
+
// written yet), so there is no undo step to leave behind either.
|
|
666
|
+
if (edgeMode !== 'idle') { setEdgeMode('idle'); render(); return; }
|
|
667
|
+
// v3.5.0 Task 8: a third sub-panel-shaped thing, same rule — the
|
|
668
|
+
// document is untouched during an endpoint drag (rule 3), so
|
|
669
|
+
// cancelling it is nulling the one variable that both is and knows
|
|
670
|
+
// the drag, then a repaint. The mousemove/mouseup listeners are left
|
|
671
|
+
// attached exactly as `edgeMode`'s own cancel above leaves them:
|
|
672
|
+
// `onCanvasUp` unconditionally removes both at its very first line
|
|
673
|
+
// whenever the button eventually comes up, so a stray move in
|
|
674
|
+
// between finds `endpointDrag === null`, falls through every other
|
|
675
|
+
// branch and repaints nothing — no listener leak, no second rule.
|
|
676
|
+
if (endpointDrag !== null) { endpointDrag = null; render(); return; }
|
|
533
677
|
close('escape');
|
|
534
678
|
return;
|
|
535
679
|
}
|
|
@@ -548,6 +692,114 @@
|
|
|
548
692
|
next.focus();
|
|
549
693
|
return;
|
|
550
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* v3.6.0 Task 16: the dialog's four Alt+arrow chords.
|
|
697
|
+
*
|
|
698
|
+
* Alt+↑ / Alt+↓ move the SELECTED LANE up or down the list.
|
|
699
|
+
* Alt+← / Alt+→ move the CELL CURSOR to the next transition, while
|
|
700
|
+
* the 關聯線 gesture is armed.
|
|
701
|
+
*
|
|
702
|
+
* Alt is this dialog's "the arrow key, but coarser" modifier, and the
|
|
703
|
+
* two halves are honestly not the same KIND of operation — vertically
|
|
704
|
+
* it acts on the document (a lane changes place, `commit` runs),
|
|
705
|
+
* horizontally only on the cursor (nothing is written). What they share
|
|
706
|
+
* is that each is the larger-grained reading of the arrow beneath it,
|
|
707
|
+
* and that neither is reachable any other way from the keyboard.
|
|
708
|
+
*
|
|
709
|
+
* Alt+←/→ arrived here in fix round 3, moved off plain ←/→. See the
|
|
710
|
+
* long comment in `handleDrawingKey` for what that binding cost (T9b:
|
|
711
|
+
* a jump can only land on a transition, so it left several cells with
|
|
712
|
+
* no keyboard route at all, and the keyboard could no longer reproduce
|
|
713
|
+
* the mouse's edge byte for byte). This branch is the only home
|
|
714
|
+
* available to it: the call to `handleDrawingKey` below is gated on
|
|
715
|
+
* `!ev.altKey`, so no Alt-modified key can reach that function, and
|
|
716
|
+
* re-opening that gate is how `Ctrl+Z` routing would get disturbed.
|
|
717
|
+
*
|
|
718
|
+
* ALL FOUR ARE CLAIMED DIALOG-WIDE, including the two that then refuse.
|
|
719
|
+
* `Alt+←`/`Alt+→` is Back/Forward in a desktop browser, and an
|
|
720
|
+
* unclaimed one inside a modal editing session is a keystroke that can
|
|
721
|
+
* navigate the whole session away. Claiming it — `preventDefault()` in
|
|
722
|
+
* the capture phase, on every target inside this overlay — is the
|
|
723
|
+
* direction that reduces that risk whichever way a given browser
|
|
724
|
+
* behaves. MEASURED, and the measurement is INCONCLUSIVE rather than
|
|
725
|
+
* reassuring: this repo's headless Chrome does not fire back-navigation
|
|
726
|
+
* on a CDP-synthesised `Alt+ArrowLeft` at all — verified on a bare page
|
|
727
|
+
* with no key handler of any kind, history length 3, document unchanged
|
|
728
|
+
* — so the harness cannot answer the question either way, and a real
|
|
729
|
+
* desktop browser is where it would have to be confirmed. The cost of
|
|
730
|
+
* claiming it is one: Option+←/→ word navigation inside a lane's name
|
|
731
|
+
* field, a single-line input, stops working.
|
|
732
|
+
*
|
|
733
|
+
* Task 14 took the rail's per-row ▲/▼ off — one pair per lane, none of
|
|
734
|
+
* which fit in the slimmed row — and left the handle's drag-and-drop
|
|
735
|
+
* as the only way to reorder a lane at all. A gesture that exists only
|
|
736
|
+
* under a pointer is the gap v3.5.0 Task 10 spent a whole task closing
|
|
737
|
+
* on this same surface, so it does not get to reopen here. The
|
|
738
|
+
* reference fork never had a second door either: its shortcut parser
|
|
739
|
+
* returns null the moment it sees `altKey`, so `moveLane` is
|
|
740
|
+
* drag-only there. Ours has two doors and both go through the SAME
|
|
741
|
+
* `codec.moveLane`, with display positions — the rail's own drop
|
|
742
|
+
* handler in `wave-panels.js` is the other one — rather than growing a
|
|
743
|
+
* second opinion about lane order.
|
|
744
|
+
*
|
|
745
|
+
* The LANE half is claimed here rather than inside `handleDrawingKey`
|
|
746
|
+
* for two reasons that are both load-bearing. `handleDrawingKey`
|
|
747
|
+
* accepts exactly two targets (the canvas and the dialog) and returns
|
|
748
|
+
* false on anything else, while the place a person reordering lanes
|
|
749
|
+
* actually sits is the lane's own NAME FIELD — which is where
|
|
750
|
+
* `moveSelectedLane`'s `focusOverride` puts them and keeps them, so
|
|
751
|
+
* Alt+↓ twice moves the same lane twice instead of two different ones.
|
|
752
|
+
* And the call to it just below is gated on `!ev.altKey`, so an
|
|
753
|
+
* Alt-modified key cannot reach it at all.
|
|
754
|
+
*
|
|
755
|
+
* Above the `!(ctrl||meta) || alt` gate further down, which returns on
|
|
756
|
+
* any Alt key: everything Alt-modified this dialog claims has to be
|
|
757
|
+
* claimed before that line.
|
|
758
|
+
*
|
|
759
|
+
* TWO THINGS THE CONDITION DELIBERATELY DOES NOT SAY, both v3.6.0 Task
|
|
760
|
+
* 16 fix round 1, both documented rather than guarded:
|
|
761
|
+
*
|
|
762
|
+
* `ev.shiftKey` is not tested, so **Alt+Shift+↑/↓ moves the lane too**.
|
|
763
|
+
* That is the same shape `isBrushKey` settled on (see its own comment
|
|
764
|
+
* at the top of this file): keying an admission rule on Shift is what
|
|
765
|
+
* blocked `P`/`N`/`|` from the keyboard entirely in v3.5.0 Task 10.
|
|
766
|
+
* There is no second meaning for Shift here — a lane move has no range
|
|
767
|
+
* to extend — so the extra modifier is harmless rather than ambiguous,
|
|
768
|
+
* and refusing it would only make the key stop working for anyone
|
|
769
|
+
* whose hand is still on Shift from a Shift+click on the rail.
|
|
770
|
+
*
|
|
771
|
+
* No sub-panel gate either. A press taken while the rail's rename
|
|
772
|
+
* field or the bus data-label field is open moves the lane and lets
|
|
773
|
+
* the repaint retire that field, dropping whatever was half-typed in
|
|
774
|
+
* it — exactly what `Ctrl+Z` already does there (see `render()`'s own
|
|
775
|
+
* first line, and `renderCanvas`'s). Both text sub-panels behave the
|
|
776
|
+
* same way and neither wedges the keyboard: `moveSelectedLane` has
|
|
777
|
+
* already written `focusOverride`, so `dataEdit.retire`'s own
|
|
778
|
+
* `focusOverride === null` claim never fires and `restoreFocus` lands
|
|
779
|
+
* on a real element. Standing this gesture behind the sub-panels the
|
|
780
|
+
* way Escape does is a design call this task did not take.
|
|
781
|
+
*/
|
|
782
|
+
if (store.ok === true && ev.altKey === true && !ev.ctrlKey && !ev.metaKey &&
|
|
783
|
+
(ev.key === 'ArrowUp' || ev.key === 'ArrowDown' ||
|
|
784
|
+
ev.key === 'ArrowLeft' || ev.key === 'ArrowRight')) {
|
|
785
|
+
ev.preventDefault();
|
|
786
|
+
ev.stopPropagation();
|
|
787
|
+
if (ev.key === 'ArrowUp' || ev.key === 'ArrowDown') {
|
|
788
|
+
moveSelectedLane(ev.key === 'ArrowUp' ? -1 : 1);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
// Armed only, and it REFUSES OUT LOUD otherwise rather than becoming
|
|
792
|
+
// a silent no-op. `jumpTransition` writes `hoverBoundary`, which
|
|
793
|
+
// `setEdgeMode` documents as meaningful only while 'armed' — a jump
|
|
794
|
+
// taken while idle would leave a hot dot behind that nothing is
|
|
795
|
+
// tracking, and it would light up the moment the user did arm.
|
|
796
|
+
if (edgeMode !== 'armed') {
|
|
797
|
+
panels.say('先按「關聯線」武裝,Alt+←/→ 才有轉態點可以跳');
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
jumpTransition(ev.key === 'ArrowRight' ? 1 : -1);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
551
803
|
// The drawing's own keys, claimed here rather than on the canvas element
|
|
552
804
|
// so they are decided in the same place — and in the same phase — as
|
|
553
805
|
// Escape, Tab and undo/redo. Deliberately BELOW those three (a cell
|
|
@@ -577,14 +829,14 @@
|
|
|
577
829
|
ev.preventDefault();
|
|
578
830
|
ev.stopPropagation();
|
|
579
831
|
if (store.undo()) afterStoreMoved('undo');
|
|
580
|
-
else say('沒有可以復原的動作');
|
|
832
|
+
else panels.say('沒有可以復原的動作');
|
|
581
833
|
return;
|
|
582
834
|
}
|
|
583
835
|
if (key === 'y' || (key === 'z' && ev.shiftKey)) {
|
|
584
836
|
ev.preventDefault();
|
|
585
837
|
ev.stopPropagation();
|
|
586
838
|
if (store.redo()) afterStoreMoved('redo');
|
|
587
|
-
else say('沒有可以重做的動作');
|
|
839
|
+
else panels.say('沒有可以重做的動作');
|
|
588
840
|
}
|
|
589
841
|
}
|
|
590
842
|
d.addEventListener('keydown', onKeyDown, true);
|
|
@@ -668,13 +920,101 @@
|
|
|
668
920
|
marksDoc = store.doc;
|
|
669
921
|
|
|
670
922
|
// ── toolbar ───────────────────────────────────────────────────────────
|
|
923
|
+
//
|
|
924
|
+
// v3.6.0 Task 12: eight captioned `.ed-wave-tool-group` sections
|
|
925
|
+
// (週期/電位/訊號/群組/關聯線/歷史/匯出/檔案) instead of the two flat bars
|
|
926
|
+
// this used to be. See `wave-panels.js`'s `toolGroup()` for why each
|
|
927
|
+
// caption sits to the LEFT of its buttons rather than above them.
|
|
671
928
|
const toolbar = d.createElement('div');
|
|
672
|
-
toolbar.className = 'ed-wave-toolbar';
|
|
929
|
+
toolbar.className = 'ed-wave-toolbar ed-wave-tools';
|
|
673
930
|
panel.appendChild(toolbar);
|
|
674
931
|
|
|
932
|
+
// v3.6.0 Task 2/12: the toolbar/right-hand-panels/lane-rail factory.
|
|
933
|
+
// Called here, right after `panel` exists, which is BEFORE several
|
|
934
|
+
// elements its OTHER returned functions need (`metaBar`'s own fields,
|
|
935
|
+
// the edge inspector, `laneCol`, the preview host, `status`). `deps` is
|
|
936
|
+
// the same object referenced below at each of those elements' own
|
|
937
|
+
// creation — see `deps.metaBar =` etc. — and `wave-panels.js`'s own
|
|
938
|
+
// header explains why that is not a reorder. `actions` is the callback
|
|
939
|
+
// bag for the `wave-ui.js`-owned state a moved function still needs to
|
|
940
|
+
// read or write; see that same header for what each entry is for and
|
|
941
|
+
// why `close` is not among them (nothing that moved calls it).
|
|
942
|
+
const deps = {
|
|
943
|
+
d: d,
|
|
944
|
+
geometry: geometry,
|
|
945
|
+
codec: codec,
|
|
946
|
+
SIZES: SIZES,
|
|
947
|
+
actions: {
|
|
948
|
+
commit: commit,
|
|
949
|
+
render: render,
|
|
950
|
+
selectionSet: function (value) { selection = value; },
|
|
951
|
+
focusOverrideGet: function () { return focusOverride; },
|
|
952
|
+
focusOverrideSet: function (key) { focusOverride = key; },
|
|
953
|
+
groupRenameGet: function () { return groupRename; },
|
|
954
|
+
groupRenameSet: function (value) { groupRename = value; },
|
|
955
|
+
sourceText: sourceText,
|
|
956
|
+
},
|
|
957
|
+
overlay: overlay,
|
|
958
|
+
panel: panel,
|
|
959
|
+
PREVIEW_INDEX: PREVIEW_INDEX,
|
|
960
|
+
PREVIEW_ID_PREFIX: PREVIEW_ID_PREFIX,
|
|
961
|
+
wavedrom: opts.wavedrom,
|
|
962
|
+
};
|
|
963
|
+
const panels = opts.panels.createPanels(deps);
|
|
964
|
+
|
|
965
|
+
// v3.6.0 Task 15: the shared `NOT_YET_WIRED` title that used to stand
|
|
966
|
+
// here went out with its last three holders — 匯出's SVG / PNG /
|
|
967
|
+
// 複製 WaveJSON. Every button this toolbar builds now does something,
|
|
968
|
+
// and nothing is created disabled to make a section's shape look real.
|
|
969
|
+
|
|
970
|
+
// ── 週期 ──────────────────────────────────────────────────────────────
|
|
971
|
+
const cycleGroup = panels.toolGroup('週期');
|
|
972
|
+
toolbar.appendChild(cycleGroup);
|
|
973
|
+
panels.toolButton('ed-wave-cycle-insert', '插入 cycle',
|
|
974
|
+
'在選取的位置插入一個 cycle(每一條 lane 同時加寬,不會有人掉隊)', function () {
|
|
975
|
+
const at = selection === null ? 0 : Math.min(selection.from, selection.to);
|
|
976
|
+
commit('insert-cycle', function (doc) { return codec.insertCycles(doc, at, 1); });
|
|
977
|
+
}, cycleGroup);
|
|
978
|
+
panels.toolButton('ed-wave-cycle-delete', '刪除 cycle',
|
|
979
|
+
'刪掉選取的 cycle(每一條 lane 同時變窄)', function () {
|
|
980
|
+
if (selection === null) { panels.say('先選一段 cycle 再刪'); return; }
|
|
981
|
+
const from = Math.min(selection.from, selection.to);
|
|
982
|
+
const count = Math.abs(selection.to - selection.from) + 1;
|
|
983
|
+
commit('delete-cycle', function (doc) { return codec.deleteCycles(doc, from, count); });
|
|
984
|
+
}, cycleGroup);
|
|
985
|
+
panels.toolButton('ed-wave-cycle-copy', '複製',
|
|
986
|
+
'把選取的 cycle 從每一條 lane 上拷一份', function () {
|
|
987
|
+
if (selection === null) { panels.say('先選一段 cycle 再複製'); return; }
|
|
988
|
+
const from = Math.min(selection.from, selection.to);
|
|
989
|
+
const count = Math.abs(selection.to - selection.from) + 1;
|
|
990
|
+
clip = codec.copyCycles(store.doc, from, count);
|
|
991
|
+
panels.say('複製了 ' + count + ' 個 cycle');
|
|
992
|
+
render();
|
|
993
|
+
}, cycleGroup);
|
|
994
|
+
panels.toolButton('ed-wave-cycle-paste', '貼上(插入)',
|
|
995
|
+
'把剪貼簿插進選取的位置', function () { pasteAt('insert'); }, cycleGroup);
|
|
996
|
+
panels.toolButton('ed-wave-cycle-paste-over', '貼上(覆蓋)',
|
|
997
|
+
'用剪貼簿蓋掉選取位置起算的那幾個 cycle', function () { pasteAt('overwrite'); }, cycleGroup);
|
|
998
|
+
|
|
999
|
+
function pasteAt(mode) {
|
|
1000
|
+
if (clip === null) { panels.say('剪貼簿是空的'); return; }
|
|
1001
|
+
const at = selection === null ? 0 : Math.min(selection.from, selection.to);
|
|
1002
|
+
commit('paste-' + mode, function (doc) {
|
|
1003
|
+
return codec.pasteCycles(doc, at, clip, mode);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ── 電位 ──────────────────────────────────────────────────────────────
|
|
1008
|
+
//
|
|
1009
|
+
// v3.6.0 Task 12: this section WRAPS the existing brush bar — the 22
|
|
1010
|
+
// brush buttons are still built inline by this same loop, with
|
|
1011
|
+
// `drawer.levelIcon` glyphs (Task 10) — `panels.toolButton` is not used
|
|
1012
|
+
// for them, so only the container they land in is new here.
|
|
1013
|
+
const brushGroup = panels.toolGroup('電位');
|
|
1014
|
+
toolbar.appendChild(brushGroup);
|
|
675
1015
|
const brushBar = d.createElement('div');
|
|
676
1016
|
brushBar.className = 'ed-wave-brushes';
|
|
677
|
-
|
|
1017
|
+
brushGroup.appendChild(brushBar);
|
|
678
1018
|
const brushButtons = [];
|
|
679
1019
|
for (const ch of BRUSHES) {
|
|
680
1020
|
const b = d.createElement('button');
|
|
@@ -682,7 +1022,21 @@
|
|
|
682
1022
|
b.className = 'ed-wave-brush';
|
|
683
1023
|
b.setAttribute('data-brush', ch);
|
|
684
1024
|
b.setAttribute('data-focus-key', 'brush-' + ch);
|
|
685
|
-
|
|
1025
|
+
// v3.6.0 Task 10: the glyph is built by `drawer.levelIcon` — the SAME
|
|
1026
|
+
// brick-drawing code `renderCanvas` paints the diagram with, not a
|
|
1027
|
+
// second hand-drawn set (see wave-draw.js's own `levelIcon` comment).
|
|
1028
|
+
// `panels.toolButton` is NOT used for these buttons: brush buttons are
|
|
1029
|
+
// built here, inline, not through that helper, so wiring the icon
|
|
1030
|
+
// where `toolButton` lives would put glyphs on the cycle-operation
|
|
1031
|
+
// buttons instead and leave every brush bare. The character stays on
|
|
1032
|
+
// the button too, as a caption next to the icon rather than instead
|
|
1033
|
+
// of it — a user who already knows the wavedrom letter should not
|
|
1034
|
+
// have to relearn a picture-only toolbar.
|
|
1035
|
+
b.appendChild(drawer.levelIcon(ch));
|
|
1036
|
+
const cap = d.createElement('span');
|
|
1037
|
+
cap.className = 'ed-wave-brush-char';
|
|
1038
|
+
cap.textContent = ch;
|
|
1039
|
+
b.appendChild(cap);
|
|
686
1040
|
b.addEventListener('click', function () {
|
|
687
1041
|
brush = ch;
|
|
688
1042
|
paintBrushState();
|
|
@@ -690,6 +1044,76 @@
|
|
|
690
1044
|
brushBar.appendChild(b);
|
|
691
1045
|
brushButtons.push(b);
|
|
692
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* The toolbar sections that act on `selection`, repainted from it.
|
|
1049
|
+
*
|
|
1050
|
+
* v3.6.0 Task 14. Called from `render()`, from `repaintDrawing()`, and
|
|
1051
|
+
* from every INLINE `drawer.renderCanvas` site in this file — which is
|
|
1052
|
+
* not a set of duplicate calls. `render()` is the slow path; every other
|
|
1053
|
+
* one of them both writes `selection` and reads it back out of the
|
|
1054
|
+
* drawer's own clamp, and none of them rebuilds the toolbar. Wired only
|
|
1055
|
+
* into `render()`, these buttons kept whatever state the LAST full
|
|
1056
|
+
* repaint left them in.
|
|
1057
|
+
*
|
|
1058
|
+
* Two rescues used to hide most of that, and fix round 1 is the gesture
|
|
1059
|
+
* where both are shut at once:
|
|
1060
|
+
*
|
|
1061
|
+
* - `onCanvasUp`'s `commit('paint', …)` re-renders — but `commit`
|
|
1062
|
+
* returns false without calling `afterStoreMoved` when the gesture
|
|
1063
|
+
* changed nothing, and pressing a cell that already holds the
|
|
1064
|
+
* brush's own value changes nothing.
|
|
1065
|
+
* - `onCanvasDown`'s `canvas.focus()` fires the drawer-wired
|
|
1066
|
+
* `enterDrawing` → `repaintDrawing` — but only when the canvas did
|
|
1067
|
+
* not already have the keyboard, i.e. never on the second press in
|
|
1068
|
+
* a row.
|
|
1069
|
+
*
|
|
1070
|
+
* MEASURED, both symptoms: (a) open the dialog (no selection, 刪除
|
|
1071
|
+
* disabled), walk to a lane with the arrow keys, and 刪除 was still
|
|
1072
|
+
* disabled although the lane was selected and highlighted on both the
|
|
1073
|
+
* rail and the canvas; (b) press req's cycle 2, then ack's cycle 4 —
|
|
1074
|
+
* `data-insert-at`/`data-lands-in` stayed on 2/`bus` while the button,
|
|
1075
|
+
* which reads the LIVE `selection`, would have inserted at 4 outside
|
|
1076
|
+
* every group. (b) is the worse of the two: it is this task's own
|
|
1077
|
+
* contract — the button says which container it is about to use BEFORE
|
|
1078
|
+
* it is pressed — broken by the button itself.
|
|
1079
|
+
*
|
|
1080
|
+
* So it is called at every writer of the state it paints, not at the
|
|
1081
|
+
* one whose symptom was reported. The rail's own `.is-selected` is a
|
|
1082
|
+
* separate, pre-existing staleness on those same inline paths (they do
|
|
1083
|
+
* not call `panels.renderLanes` either) and is deliberately NOT fixed
|
|
1084
|
+
* here.
|
|
1085
|
+
*
|
|
1086
|
+
* 新增 and 空白列 stay enabled with nothing selected — that case has a
|
|
1087
|
+
* real answer of its own (position 0; see `signalInsertAt`) — and they
|
|
1088
|
+
* publish WHERE the insert lands before they are pressed, which is the
|
|
1089
|
+
* property the rail's per-row + used to carry in `data-lands-in` and
|
|
1090
|
+
* its tooltip. The other three need a lane named, and 解散群組 needs
|
|
1091
|
+
* that lane to be inside a group; a button that looks pressable and
|
|
1092
|
+
* then answers「先選一條 lane」is the affordance this batch is
|
|
1093
|
+
* removing, not reproducing.
|
|
1094
|
+
*/
|
|
1095
|
+
function paintSelectionState(doc, layout) {
|
|
1096
|
+
const insertAt = signalInsertAt(layout);
|
|
1097
|
+
const insertLanding = panels.landingOf(doc, insertAt);
|
|
1098
|
+
const insertWhere = insertLanding.title === null
|
|
1099
|
+
? '群組外' : '群組「' + insertLanding.title + '」裡';
|
|
1100
|
+
for (const b of [signalAddBtn, signalBlankBtn]) {
|
|
1101
|
+
b.setAttribute('data-insert-at', String(insertAt));
|
|
1102
|
+
b.setAttribute('data-lands-in',
|
|
1103
|
+
insertLanding.title === null ? '' : insertLanding.title);
|
|
1104
|
+
}
|
|
1105
|
+
signalAddBtn.title = '在選取的 lane 後面加一條(沒有選取就加在最前面)—— 會落在' + insertWhere;
|
|
1106
|
+
signalBlankBtn.title = '在選取的 lane 後面插入一個空白列(WaveJSON 的 {},' +
|
|
1107
|
+
'用來空出間距;刪除跟刪一般的 lane 一樣)—— 會落在' + insertWhere;
|
|
1108
|
+
signalDeleteBtn.disabled = selection === null;
|
|
1109
|
+
signalCopyBtn.disabled = selection === null;
|
|
1110
|
+
groupBtn.disabled = selection === null || selection.laneTo === selection.laneIndex;
|
|
1111
|
+
groupDissolveBtn.disabled = selection === null ||
|
|
1112
|
+
!laneIsInGroup(layout, selection.laneIndex);
|
|
1113
|
+
overlay.setAttribute('data-wave-lane-range', selection === null ? ''
|
|
1114
|
+
: selection.laneIndex + ',' + selection.laneTo);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
693
1117
|
function paintBrushState() {
|
|
694
1118
|
for (const b of brushButtons) {
|
|
695
1119
|
b.classList.toggle('is-on', b.getAttribute('data-brush') === brush);
|
|
@@ -697,114 +1121,796 @@
|
|
|
697
1121
|
overlay.setAttribute('data-wave-brush', brush);
|
|
698
1122
|
}
|
|
699
1123
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1124
|
+
// ── 訊號 ──────────────────────────────────────────────────────────────
|
|
1125
|
+
//
|
|
1126
|
+
// v3.6.0 Task 14: these four came off the lane rail, where every row
|
|
1127
|
+
// carried its own copy and none of them fit (see `wave-panels.js`'s
|
|
1128
|
+
// `laneRow`). One copy each, here, acting on `selection` — the same mark
|
|
1129
|
+
// the 週期 section and the brushes read, not a second one kept only for
|
|
1130
|
+
// them.
|
|
1131
|
+
//
|
|
1132
|
+
// WHERE an insert lands is `laneInsertPath`'s asymmetry and cannot be
|
|
1133
|
+
// argued away: an insert at a group's first lane joins that group, an
|
|
1134
|
+
// insert just past its last lane does not. So the button says which
|
|
1135
|
+
// container it is about to use BEFORE it is pressed — `data-lands-in`
|
|
1136
|
+
// and the tooltip, both refreshed by `paintSelectionState` below on
|
|
1137
|
+
// every repaint, full or fast — and the status line says it again
|
|
1138
|
+
// after. That is the property the rail's per-row + used to carry,
|
|
1139
|
+
// moved rather than dropped.
|
|
1140
|
+
const signalGroup = panels.toolGroup('訊號');
|
|
1141
|
+
toolbar.appendChild(signalGroup);
|
|
703
1142
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1143
|
+
/**
|
|
1144
|
+
* Where 新增 / 空白列 put their new lane: right after the selection, and
|
|
1145
|
+
* at the very top when there is nothing selected.
|
|
1146
|
+
*
|
|
1147
|
+
* "After the selection" is `laneTo + 1`, not `laneIndex + 1`: with a
|
|
1148
|
+
* whole range picked, adding below the range is the only reading that
|
|
1149
|
+
* does not silently cut it in half.
|
|
1150
|
+
*
|
|
1151
|
+
* The no-selection answer is 0 rather than the end, and that is the one
|
|
1152
|
+
* choice here that is not obvious. The rail used to carry a + at every
|
|
1153
|
+
* position — one per row plus a tail — so every insert position from 0
|
|
1154
|
+
* to n was reachable in one press. One button that only ever inserts
|
|
1155
|
+
* AFTER something cannot offer position 0 at all: there is no lane to
|
|
1156
|
+
* be after. Pairing "after the selection" with "0 when there is no
|
|
1157
|
+
* selection" restores the full range — 0 with nothing picked, k+1 with
|
|
1158
|
+
* lane k picked, the end with the last lane picked — and it costs one
|
|
1159
|
+
* surprising default instead of one unreachable position. The button
|
|
1160
|
+
* says which it is about to do before it is pressed (`data-lands-in`
|
|
1161
|
+
* and the tooltip), so the surprise is visible rather than discovered.
|
|
1162
|
+
*/
|
|
1163
|
+
function signalInsertAt(layout) {
|
|
1164
|
+
const l = layout === undefined || layout === null
|
|
1165
|
+
? geometry.layoutOf(store.doc, SIZES) : layout;
|
|
1166
|
+
if (selection === null) return 0;
|
|
1167
|
+
return Math.min(selection.laneTo + 1, l.lanes.length);
|
|
714
1168
|
}
|
|
715
1169
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
render();
|
|
736
|
-
});
|
|
737
|
-
toolButton('ed-wave-cycle-paste', '貼上(插入)',
|
|
738
|
-
'把剪貼簿插進選取的位置', function () { pasteAt('insert'); });
|
|
739
|
-
toolButton('ed-wave-cycle-paste-over', '貼上(覆蓋)',
|
|
740
|
-
'用剪貼簿蓋掉選取位置起算的那幾個 cycle', function () { pasteAt('overwrite'); });
|
|
1170
|
+
function addLaneFromToolbar(name, lane) {
|
|
1171
|
+
const at = signalInsertAt();
|
|
1172
|
+
const landing = panels.landingOf(store.doc, at);
|
|
1173
|
+
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
1174
|
+
// Straight into the new lane's own name field — it is what the user
|
|
1175
|
+
// types next, and on a spacer (no name to type) there is nothing
|
|
1176
|
+
// else on that row to land on either. The lane arrives at display
|
|
1177
|
+
// position `at`, so that is the key.
|
|
1178
|
+
focusOverride = 'lane-name-' + at;
|
|
1179
|
+
const added = commit(name, function (doc2) { return codec.addLane(doc2, at, lane); });
|
|
1180
|
+
// fix round 1 (F6): the whole prefix per branch, not one concatenation
|
|
1181
|
+
// with a shared 落在. The two bases this replaced differ by exactly one
|
|
1182
|
+
// space — `say('新增的 lane 落在' + …)` has it because the phrase ends
|
|
1183
|
+
// in a Latin word (so does 複製's message further down), and
|
|
1184
|
+
// `say('空白列落在' + …)` does not because it ends in a CJK one. A
|
|
1185
|
+
// single shared suffix has to get one of them wrong.
|
|
1186
|
+
const lead = lane.wave === undefined ? '空白列落在' : '新增的 lane 落在';
|
|
1187
|
+
if (added) panels.say(lead + where);
|
|
1188
|
+
}
|
|
741
1189
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1190
|
+
const signalAddBtn = panels.toolButton('ed-wave-signal-add', '新增',
|
|
1191
|
+
'在選取的 lane 後面加一條(沒有選取就加在最前面)', function () {
|
|
1192
|
+
addLaneFromToolbar('add-lane', { name: '', wave: 'x' });
|
|
1193
|
+
}, signalGroup);
|
|
1194
|
+
// v3.5.0 Task 13: WaveDrom's own blank row, `{}`. There is deliberately
|
|
1195
|
+
// no dedicated "remove spacer" control: a spacer is a lane like any
|
|
1196
|
+
// other, so 刪除 below removes it exactly as it removes anything else.
|
|
1197
|
+
const signalBlankBtn = panels.toolButton('ed-wave-signal-blank', '空白列',
|
|
1198
|
+
'在選取的 lane 後面插入一個空白列(WaveJSON 的 {},用來空出間距;刪除跟刪一般的 lane 一樣)',
|
|
1199
|
+
function () { addLaneFromToolbar('add-spacer', {}); }, signalGroup);
|
|
1200
|
+
const signalDeleteBtn = panels.toolButton('ed-wave-signal-delete', '刪除',
|
|
1201
|
+
'刪掉選取的那幾條 lane(如果群組因此空掉,群組也會一起走)', function () {
|
|
1202
|
+
if (selection === null) { panels.say('先選一條 lane 再刪'); return; }
|
|
1203
|
+
const lo = selection.laneIndex;
|
|
1204
|
+
const hi = selection.laneTo;
|
|
1205
|
+
const n = hi - lo + 1;
|
|
1206
|
+
// The rows about to stop existing are exactly the ones the marks
|
|
1207
|
+
// name, so the selection goes FIRST — a selection naming cycles on
|
|
1208
|
+
// a lane that is about to disappear is stale the instant `commit`
|
|
1209
|
+
// runs, and that staleness is what `carryMarks` exists to prevent
|
|
1210
|
+
// everywhere else.
|
|
1211
|
+
selection = null;
|
|
1212
|
+
// The rows about to vanish are also where the keyboard is allowed to
|
|
1213
|
+
// land, and the button the user pressed is about to disable itself
|
|
1214
|
+
// (no selection left), so naming a destination here is not a nicety:
|
|
1215
|
+
// `restoreFocus`'s fallback would otherwise be a disabled button.
|
|
1216
|
+
// Display position `lo` is whatever lane inherits the place the
|
|
1217
|
+
// deleted range occupied — on a document emptied down to nothing the
|
|
1218
|
+
// key simply is not there and `restoreFocus` falls back to the
|
|
1219
|
+
// dialog, which is the right answer for「there is no row left」.
|
|
1220
|
+
focusOverride = 'lane-name-' + lo;
|
|
1221
|
+
// Bottom-up: every removal renumbers the rows BELOW it, so taking
|
|
1222
|
+
// the highest index first leaves the ones still to go untouched.
|
|
1223
|
+
//
|
|
1224
|
+
// fix round 1: an earlier draft of this comment claimed a top-down
|
|
1225
|
+
// loop would delete a DIFFERENT set of lanes. It would not, and the
|
|
1226
|
+
// claim is withdrawn rather than reworded — checked against the
|
|
1227
|
+
// codec over five ranges, group-spanning ones included: deleting
|
|
1228
|
+
// `lo` n times and deleting hi..lo give the identical document,
|
|
1229
|
+
// because whatever shifts into `lo` is the next member of a
|
|
1230
|
+
// CONTIGUOUS range. What bottom-up actually buys is that each index
|
|
1231
|
+
// still means the lane it meant when the range was measured, so the
|
|
1232
|
+
// loop stays correct if a range that is not contiguous ever reaches
|
|
1233
|
+
// it; top-down would be silently relying on the contiguity.
|
|
1234
|
+
const removed = commit('remove-lane', function (doc2) {
|
|
1235
|
+
let out = doc2;
|
|
1236
|
+
for (let k = hi; k >= lo; k -= 1) out = codec.removeLane(out, k);
|
|
1237
|
+
return out;
|
|
1238
|
+
});
|
|
1239
|
+
if (removed) panels.say('已刪掉 ' + n + ' 條 lane —— Ctrl+Z 可以拿回來');
|
|
1240
|
+
}, signalGroup);
|
|
1241
|
+
const signalCopyBtn = panels.toolButton('ed-wave-signal-copy', '複製',
|
|
1242
|
+
'複製選取的 lane,接在它後面(data 等欄位一起複製;node 錨點不會跟著複製,' +
|
|
1243
|
+
'避免兩條 lane 搶同一個字母)', function () {
|
|
1244
|
+
if (selection === null) { panels.say('先選一條 lane 再複製'); return; }
|
|
1245
|
+
// One lane, the range's first: `duplicateLane` copies one lane and
|
|
1246
|
+
// lands it at `at + 1`, and a loop over a range would interleave
|
|
1247
|
+
// each copy with the original it renumbers. The range's own far end
|
|
1248
|
+
// is left alone rather than guessed at.
|
|
1249
|
+
const at = selection.laneIndex;
|
|
1250
|
+
const landing = panels.landingOf(store.doc, at + 1);
|
|
1251
|
+
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
1252
|
+
focusOverride = 'lane-name-' + (at + 1);
|
|
1253
|
+
const didDup = commit('duplicate-lane', function (doc2) {
|
|
1254
|
+
return codec.duplicateLane(doc2, at);
|
|
1255
|
+
});
|
|
1256
|
+
if (didDup) panels.say('複製的 lane 落在' + where);
|
|
1257
|
+
}, signalGroup);
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* v3.6.0 Task 16: move the selected lane one row up (`step === -1`) or
|
|
1261
|
+
* down (`step === 1`) — the keyboard's half of the rail handle's drag,
|
|
1262
|
+
* reached by Alt+↑ / Alt+↓ in `onKeyDown`.
|
|
1263
|
+
*
|
|
1264
|
+
* ONE lane, the range's first, exactly as 複製 just above: `moveLane`
|
|
1265
|
+
* moves one lane, and a loop over a range would renumber each member out
|
|
1266
|
+
* from under the next. The range's far end is collapsed rather than
|
|
1267
|
+
* guessed at, so what moved and what is selected afterwards are the same
|
|
1268
|
+
* one lane.
|
|
1269
|
+
*
|
|
1270
|
+
* Both ends of the list REFUSE OUT LOUD instead of silently doing
|
|
1271
|
+
* nothing. A key that does nothing and says nothing is, to the person
|
|
1272
|
+
* pressing it, indistinguishable from a key this dialog never bound —
|
|
1273
|
+
* and the status line is where every other refusal in here already goes.
|
|
1274
|
+
*
|
|
1275
|
+
* WHERE THE MARKS END UP. They are collapsed onto the moved lane BEFORE
|
|
1276
|
+
* the commit, at its OLD row, and `carryMarks` — this file's single
|
|
1277
|
+
* mechanism for "a mark follows its lane", by lane identity through
|
|
1278
|
+
* `wave-store.laneTrace` — is what carries them to the new row.
|
|
1279
|
+
* Re-pointing them by hand afterwards would cost a second `render()`
|
|
1280
|
+
* (the one inside `commit` has already run and already consumed
|
|
1281
|
+
* `focusOverride`) and would stand a second belief about where the lane
|
|
1282
|
+
* went beside the trace's, which is the shape of defect this batch has
|
|
1283
|
+
* paid for repeatedly. MEASURED in a real browser on the journey
|
|
1284
|
+
* fixture, both from the trace alone: Alt+↓ on the lane selected at row
|
|
1285
|
+
* 0 with no cell cursor yet took `data-wave-lane-range` 0,0 → 1,1 and
|
|
1286
|
+
* left `data-wave-cursor` empty (no cursor is invented for a gesture
|
|
1287
|
+
* that never entered the drawing); Alt+↑ with the cell cursor sitting on
|
|
1288
|
+
* the moved lane at row 2 took `data-wave-cursor` 2,0 → 1,0 and
|
|
1289
|
+
* `data-wave-lane-range` 2,2 → 1,1.
|
|
1290
|
+
*
|
|
1291
|
+
* The invariant 訊號's 刪除 quietly depends on — a non-null `selection`
|
|
1292
|
+
* always names a lane that exists, so its `commit` always returns true —
|
|
1293
|
+
* survives every exit here. The two refusals leave `selection` exactly
|
|
1294
|
+
* as they found it. The commit path writes `{laneIndex: at, laneTo: at}`
|
|
1295
|
+
* with `at` an index `renderCanvas`'s clamp has already vouched for, and
|
|
1296
|
+
* a move changes no lane's EXISTENCE, so whether `carryMarks` then
|
|
1297
|
+
* traces it to `to` (both ends of a collapsed range trace together) or
|
|
1298
|
+
* drops it to null for want of an injected `laneTrace`, what is left is
|
|
1299
|
+
* either a real row or nothing at all — never a row number with no lane
|
|
1300
|
+
* under it.
|
|
1301
|
+
*/
|
|
1302
|
+
function moveSelectedLane(step) {
|
|
1303
|
+
if (selection === null) { panels.say('先選一條 lane 再搬'); return; }
|
|
1304
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
1305
|
+
const at = selection.laneIndex;
|
|
1306
|
+
const to = at + step;
|
|
1307
|
+
if (to < 0) { panels.say('這已經是第一條 lane 了,沒有更上面的位置'); return; }
|
|
1308
|
+
if (to >= layout.lanes.length) {
|
|
1309
|
+
panels.say('這已經是最後一條 lane 了,沒有更下面的位置');
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
selection = { laneIndex: at, laneTo: at, from: selection.from, to: selection.to };
|
|
1313
|
+
// The lane arrives at row `to`, so the keyboard goes to the field it
|
|
1314
|
+
// arrives in — the same rule the rail's own `drop` handler follows,
|
|
1315
|
+
// and the whole reason `focusOverride` exists (see its declaration).
|
|
1316
|
+
focusOverride = 'lane-name-' + to;
|
|
1317
|
+
const didMove = commit('move-lane', function (doc2) {
|
|
1318
|
+
return codec.moveLane(doc2, at, to);
|
|
747
1319
|
});
|
|
1320
|
+
// v3.6.0 Task 16 fix round 1: SAY WHICH CONTAINER IT LANDED IN.
|
|
1321
|
+
//
|
|
1322
|
+
// `moveLane` may cross a group boundary — `laneInsertPath`'s asymmetry,
|
|
1323
|
+
// which the whole 訊號 section already announces before and after every
|
|
1324
|
+
// insert. MEASURED against the real codec on the journey fixture:
|
|
1325
|
+
// Alt+↓ on `clk` at row 0 moves it INTO the group `bus`
|
|
1326
|
+
// (`["clk",["bus",…]]` → `[["bus",["req","clk","dat"]],…]`), and the
|
|
1327
|
+
// flattened name order is the only thing that changes on screen. Before
|
|
1328
|
+
// this, the single word the user got back was `afterStoreMoved`'s
|
|
1329
|
+
// generic「已寫回:move-lane」, so the one case in this function that
|
|
1330
|
+
// re-parents a lane was also its quietest — while BOTH refusals spoke.
|
|
1331
|
+
//
|
|
1332
|
+
// Asked of the document AFTER the move, unlike 複製 above, which asks
|
|
1333
|
+
// before because its lane does not exist yet. `laneInsertPath(doc, to)`
|
|
1334
|
+
// means "in front of whichever lane sits at `to`, in THAT lane's own
|
|
1335
|
+
// container", and after the move the lane at `to` is the moved one — so
|
|
1336
|
+
// its answer is the moved lane's own container, including the
|
|
1337
|
+
// group's-first-lane edge. Verified against the codec for all four
|
|
1338
|
+
// moves the journey scenario makes: bus / 群組外 / bus / bus.
|
|
1339
|
+
//
|
|
1340
|
+
// ONE shared lead — `'搬動的 lane 落在'` — with only the container name
|
|
1341
|
+
// branching. That is what the line below does, and the whole of what it
|
|
1342
|
+
// needs to do.
|
|
1343
|
+
//
|
|
1344
|
+
// fix round 2 (N2) WITHDRAWS the claim that stood here ("the whole
|
|
1345
|
+
// prefix per branch rather than one shared 落在, the same rule
|
|
1346
|
+
// 新增/空白列 above record"), which said the opposite of the code
|
|
1347
|
+
// directly beneath it. That rule exists because 新增 and 空白列 are TWO
|
|
1348
|
+
// gestures sharing ONE handler whose leads differ by a space —
|
|
1349
|
+
// `'新增的 lane 落在'` ends in a Latin word, `'空白列落在'` in a CJK one
|
|
1350
|
+
// — and this function has no such axis: one gesture, one lead. The
|
|
1351
|
+
// space after `lane` here is that typographic rule on its own, and the
|
|
1352
|
+
// 群組外 / 群組「…」裡 tail is the identical two-way split 複製 uses.
|
|
1353
|
+
if (didMove) {
|
|
1354
|
+
const landing = panels.landingOf(store.doc, to);
|
|
1355
|
+
panels.say('搬動的 lane 落在' +
|
|
1356
|
+
(landing.title === null ? '群組外' : '群組「' + landing.title + '」裡'));
|
|
1357
|
+
}
|
|
748
1358
|
}
|
|
749
1359
|
|
|
750
|
-
|
|
751
|
-
|
|
1360
|
+
// ── 群組 ──────────────────────────────────────────────────────────────
|
|
1361
|
+
const groupGroup = panels.toolGroup('群組');
|
|
1362
|
+
toolbar.appendChild(groupGroup);
|
|
1363
|
+
// Wrap the selected lane range into one new group. The codec is the one
|
|
1364
|
+
// that decides contiguity — this button does not pre-filter what is
|
|
1365
|
+
// selectable, it only decides whether there is enough of a selection to
|
|
1366
|
+
// bother trying, so the refusal message the user actually needed
|
|
1367
|
+
// ("these lanes are not next to each other") comes from `commit`'s own
|
|
1368
|
+
// status line rather than being silently swallowed here.
|
|
1369
|
+
//
|
|
1370
|
+
// v3.6.0 Task 14: the range is `selection`'s, not a second mark's — so
|
|
1371
|
+
//「選起來的那幾條」names the same highlight the canvas and the Lane
|
|
1372
|
+
// panel are showing, rather than a second one only this button could
|
|
1373
|
+
// see. Shift+click on the rail is the one gesture that WIDENS it;
|
|
1374
|
+
// Shift+↑/↓ on the canvas still collapses onto the lane it lands on
|
|
1375
|
+
// (see `moveCursor`), which is why the tooltip names the rail and not
|
|
1376
|
+
// the keyboard.
|
|
1377
|
+
//
|
|
1378
|
+
// The group is created with an EMPTY label and the rename field is
|
|
1379
|
+
// opened immediately — the same affordance 新增 gives a brand new lane
|
|
1380
|
+
// (empty `name`, keyboard straight into the field) — rather than a
|
|
1381
|
+
// placeholder string the user has to notice and clear.
|
|
1382
|
+
const groupBtn = panels.toolButton('ed-wave-group-make', '建立群組',
|
|
1383
|
+
'把選起來的那幾條連續 lane 包成一個新群組(在左邊的 lane 列表上 Shift+點選一段)',
|
|
1384
|
+
function () {
|
|
1385
|
+
if (selection === null || selection.laneTo === selection.laneIndex) {
|
|
1386
|
+
panels.say('先選至少兩條連續的 lane,才有東西可以群組');
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const ats = [];
|
|
1390
|
+
for (let k = selection.laneIndex; k <= selection.laneTo; k += 1) ats.push(k);
|
|
1391
|
+
const minAt = ats[0];
|
|
1392
|
+
const committed = commit('group-lanes', function (doc2) {
|
|
1393
|
+
return codec.groupLanes(doc2, ats, '');
|
|
1394
|
+
});
|
|
1395
|
+
if (!committed) {
|
|
1396
|
+
panels.say('這幾條 lane 不連續(或選取已經失效),WaveJSON 的群組只能是連續的一段');
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
// The commit above already repainted with the (now stale) highlight
|
|
1400
|
+
// showing; one more repaint clears it before handing the keyboard to
|
|
1401
|
+
// the rename field, the same two-step `renameGroup` itself already
|
|
1402
|
+
// is — a rebuild first, then the sub-panel laid over what it built.
|
|
1403
|
+
selection = null;
|
|
1404
|
+
render();
|
|
1405
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
1406
|
+
const spans = geometry.groupSpansOf(store.doc, layout);
|
|
1407
|
+
for (const span of spans) {
|
|
1408
|
+
if (span.from === minAt) { panels.renameGroup(span); break; }
|
|
1409
|
+
}
|
|
1410
|
+
}, groupGroup);
|
|
1411
|
+
// v3.6.0 Task 14: 解散 the group the selected lane is a DIRECT member
|
|
1412
|
+
// of. The lanes it held stay exactly where they were; only the wrapping
|
|
1413
|
+
// array and its title go. `ungroupLanes`'s own docstring explains why
|
|
1414
|
+
// "direct" is the only thing a display position can name — and why a
|
|
1415
|
+
// group built entirely out of sub-groups therefore has no lane that
|
|
1416
|
+
// names it, which is exactly the case `laneIsInGroup` below reports as
|
|
1417
|
+
// "nothing to dissolve here" by disabling this button.
|
|
1418
|
+
const groupDissolveBtn = panels.toolButton('ed-wave-group-dissolve', '解散群組',
|
|
1419
|
+
'解散選取的 lane 所屬的那個群組(裡面的 lane 都留著,只是不再是一組)', function () {
|
|
1420
|
+
if (selection === null) { panels.say('先選一條群組裡的 lane'); return; }
|
|
1421
|
+
const at = selection.laneIndex;
|
|
1422
|
+
// The lane at `at` survives ungrouping at the same display position
|
|
1423
|
+
// (`ungroupLanes`: the flattened order and count never change), so
|
|
1424
|
+
// its own name field is exactly where the keyboard should land.
|
|
1425
|
+
focusOverride = 'lane-name-' + at;
|
|
1426
|
+
const done = commit('ungroup', function (doc2) { return codec.ungroupLanes(doc2, at); });
|
|
1427
|
+
if (!done) panels.say('這條 lane 不在任何群組裡');
|
|
1428
|
+
}, groupGroup);
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Whether display position `at` is a direct member of some group —
|
|
1432
|
+
* `ungroupLanes`'s own precondition, asked of the layout rather than
|
|
1433
|
+
* re-derived. A lane sitting straight under `signal` has a path of
|
|
1434
|
+
* length 2 (`['signal', k]`); anything longer is inside at least one
|
|
1435
|
+
* group.
|
|
1436
|
+
*/
|
|
1437
|
+
function laneIsInGroup(layout, at) {
|
|
1438
|
+
if (at < 0 || at >= layout.lanes.length) return false;
|
|
1439
|
+
return layout.lanes[at].path.length > 2;
|
|
1440
|
+
}
|
|
1441
|
+
// ── 關聯線 ────────────────────────────────────────────────────────────
|
|
1442
|
+
const edgeGroup = panels.toolGroup('關聯線');
|
|
1443
|
+
toolbar.appendChild(edgeGroup);
|
|
1444
|
+
// v3.5.0 Task 7: arm the edge-creation gesture. One-shot in the sense
|
|
1445
|
+
// that a drag which PRODUCES an edge disarms it (`finishEdgeDrag` below);
|
|
1446
|
+
// pressing the button itself is a plain toggle, so a change of mind
|
|
1447
|
+
// before ever pressing on the canvas costs one more click, not Escape.
|
|
1448
|
+
const edgeBtn = panels.toolButton('ed-wave-edge-arm', '關聯線',
|
|
1449
|
+
'按一下武裝,然後從一格拖到另一格畫出關聯線(Escape 取消)', function () {
|
|
1450
|
+
setEdgeMode(edgeMode === 'idle' ? 'armed' : 'idle');
|
|
1451
|
+
// v3.6.0 Task 11: the transition dots (and their removal) are the
|
|
1452
|
+
// only on-canvas evidence arming happened — `setEdgeMode` above
|
|
1453
|
+
// only touches the button/overlay attributes, so without this the
|
|
1454
|
+
// dots would not appear until some OTHER repaint happened to run
|
|
1455
|
+
// first (the next cursor move, say), which is not "the instant you
|
|
1456
|
+
// press the button".
|
|
1457
|
+
render();
|
|
1458
|
+
}, edgeGroup);
|
|
1459
|
+
setEdgeMode('idle');
|
|
1460
|
+
|
|
1461
|
+
// ── 歷史 ──────────────────────────────────────────────────────────────
|
|
1462
|
+
const historyGroup = panels.toolGroup('歷史');
|
|
1463
|
+
toolbar.appendChild(historyGroup);
|
|
1464
|
+
const undoBtn = panels.toolButton('ed-wave-undo', '復原', '退回上一個動作', function () {
|
|
1465
|
+
if (!store.undo()) { panels.say('沒有可以復原的動作'); return; }
|
|
752
1466
|
afterStoreMoved('undo');
|
|
753
|
-
});
|
|
754
|
-
const redoBtn = toolButton('ed-wave-redo', '重做', '再做一次剛剛復原掉的動作', function () {
|
|
755
|
-
if (!store.redo()) { say('沒有可以重做的動作'); return; }
|
|
1467
|
+
}, historyGroup);
|
|
1468
|
+
const redoBtn = panels.toolButton('ed-wave-redo', '重做', '再做一次剛剛復原掉的動作', function () {
|
|
1469
|
+
if (!store.redo()) { panels.say('沒有可以重做的動作'); return; }
|
|
756
1470
|
afterStoreMoved('redo');
|
|
757
|
-
});
|
|
1471
|
+
}, historyGroup);
|
|
758
1472
|
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
// `config.hscale` USED to be a third field here and has been removed. The
|
|
762
|
-
// drawing does not model `hscale`, `period` or `phase` at all (zero
|
|
763
|
-
// occurrences in wave-codec, wave-geometry or this file), and measured
|
|
764
|
-
// against the pinned engine each of them changes what the engine paints
|
|
765
|
-
// while leaving the drawing exactly as it was:
|
|
1473
|
+
// ── 匯出 ──────────────────────────────────────────────────────────────
|
|
766
1474
|
//
|
|
767
|
-
//
|
|
768
|
-
// {wave:'0101', period:2} engine 16 drawing 4, unchanged
|
|
769
|
-
// {wave:'0101', phase:0.5} engine 7 drawing 4, unshifted
|
|
770
|
-
// {wave:'0101'} config.hscale:2 engine 16 drawing 4, unchanged
|
|
1475
|
+
// v3.6.0 Task 15: the three buttons Task 12 created as shape.
|
|
771
1476
|
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
// a half-model here would be a SECOND belief about cycle geometry in the
|
|
778
|
-
// same tree, which is the exact failure this batch keeps producing.
|
|
1477
|
+
// THE SOURCE IS THE PREVIEW'S SVG, never the canvas. The preview is the
|
|
1478
|
+
// ENGINE'S OWN OUTPUT, so what a user exports is the same picture the
|
|
1479
|
+
// rendered document shows; exporting the canvas would export this file's
|
|
1480
|
+
// second implementation of that picture — the one precondition 3 in this
|
|
1481
|
+
// module's header exists to stop from becoming a second belief on screen.
|
|
779
1482
|
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1483
|
+
// Every browser global the three need — `XMLSerializer`, `Blob`, `URL`,
|
|
1484
|
+
// `Image`, `setTimeout`, `navigator` — is reached through `probeWin`
|
|
1485
|
+
// (`d.defaultView`, declared beside `sourceText` above), never as a bare
|
|
1486
|
+
// identifier: this file is require-able in node against a fake
|
|
1487
|
+
// `opts.document` (see its header) and names no global of its own.
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* The preview's SVG, serialised so that it still draws OUTSIDE this page.
|
|
1491
|
+
*
|
|
1492
|
+
* The plain `serializeToString(svg)` that this obviously wants to be is a
|
|
1493
|
+
* trap, and a quiet one. `renderPreview` renders with `notFirstSignal`
|
|
1494
|
+
* true whenever a WaveDrom skin is already on the page — which is ALWAYS
|
|
1495
|
+
* in the flow that reaches this editor, because the entry affordance only
|
|
1496
|
+
* appears over a rendered diagram — so the preview SVG carries neither the
|
|
1497
|
+
* skin's `<defs>` nor its `<style>`. MEASURED on this repo's own wave
|
|
1498
|
+
* fixture: the preview SVG is 5057 bytes with 51 `<use>` elements, 0
|
|
1499
|
+
* `<defs>` and 0 `<style>`, every one of those 51 pointing at an id that
|
|
1500
|
+
* lives in a DIFFERENT SVG on the page. Serialised as-is it opens as half
|
|
1501
|
+
* a picture — the whole clock lane and every brick shape gone, all text in
|
|
1502
|
+
* the browser's default font — and it opens that way without erroring, so
|
|
1503
|
+
* a check that the file was written, or that the click did not throw, sees
|
|
1504
|
+
* nothing at all. Rasterised: 1409 non-white pixels of 54000 against 5693
|
|
1505
|
+
* for the self-contained file.
|
|
1506
|
+
*
|
|
1507
|
+
* So: clone the preview, and if the clone has no `<defs>` of its own,
|
|
1508
|
+
* prepend a CLONE of the page skin's `<style>` and `<defs>`.
|
|
1509
|
+
*/
|
|
1510
|
+
function exportSvgText() {
|
|
1511
|
+
const host = deps.previewHost;
|
|
1512
|
+
const svg = (host === null || host === undefined) ? null : host.querySelector('svg');
|
|
1513
|
+
if (svg === null || svg === undefined) return '';
|
|
1514
|
+
const clone = svg.cloneNode(true);
|
|
1515
|
+
if (clone.querySelector('defs') === null) {
|
|
1516
|
+
// `#socket` is the skin's own first entry, asked for INSIDE a
|
|
1517
|
+
// WaveDrom-rendered SVG's defs — the identical question, asked the
|
|
1518
|
+
// identical way, that `renderPreview` asks to decide whether to emit a
|
|
1519
|
+
// skin at all. Two different answers to "is the skin on this page"
|
|
1520
|
+
// would be exactly the disagreement that leaves this export broken.
|
|
1521
|
+
const socket = d.querySelector('svg.WaveDrom defs #socket');
|
|
1522
|
+
const skin = socket === null ? null : socket.closest('svg');
|
|
1523
|
+
if (skin !== null) {
|
|
1524
|
+
const defs = skin.querySelector('defs');
|
|
1525
|
+
const style = skin.querySelector('style');
|
|
1526
|
+
// CLONED, never moved. These nodes are what every diagram ON THE
|
|
1527
|
+
// PAGE draws from; an export that took them would blank the whole
|
|
1528
|
+
// document behind the dialog the first time anyone pressed 匯出.
|
|
1529
|
+
if (defs !== null) clone.insertBefore(defs.cloneNode(true), clone.firstChild);
|
|
1530
|
+
if (style !== null) clone.insertBefore(style.cloneNode(true), clone.firstChild);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return new probeWin.XMLSerializer().serializeToString(clone);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function downloadBlob(blob, filename) {
|
|
1537
|
+
const url = probeWin.URL.createObjectURL(blob);
|
|
1538
|
+
const a = d.createElement('a');
|
|
1539
|
+
a.href = url;
|
|
1540
|
+
a.download = filename;
|
|
1541
|
+
d.body.appendChild(a);
|
|
1542
|
+
a.click();
|
|
1543
|
+
a.remove();
|
|
1544
|
+
// revoke 延後一拍:立刻 revoke 會讓某些瀏覽器在下載開始前就失去來源。
|
|
1545
|
+
probeWin.setTimeout(function () { probeWin.URL.revokeObjectURL(url); }, 1000);
|
|
801
1546
|
}
|
|
802
1547
|
|
|
803
|
-
|
|
804
|
-
|
|
1548
|
+
function exportSvg() {
|
|
1549
|
+
const text = exportSvgText();
|
|
1550
|
+
if (text === '') { panels.say('預覽還沒畫出來,沒有東西可以匯出'); return; }
|
|
1551
|
+
downloadBlob(new probeWin.Blob([text], { type: 'image/svg+xml' }), exportName('svg'));
|
|
1552
|
+
panels.say('已匯出 ' + exportName('svg'));
|
|
1553
|
+
}
|
|
805
1554
|
|
|
806
|
-
|
|
807
|
-
|
|
1555
|
+
function exportPng() {
|
|
1556
|
+
const text = exportSvgText();
|
|
1557
|
+
if (text === '') { panels.say('預覽還沒畫出來,沒有東西可以匯出'); return; }
|
|
1558
|
+
const svg = deps.previewHost.querySelector('svg');
|
|
1559
|
+
const w = Number(svg.getAttribute('width'));
|
|
1560
|
+
const h = Number(svg.getAttribute('height'));
|
|
1561
|
+
// A zero-sized canvas is not an error to `toBlob`: it hands back a
|
|
1562
|
+
// perfectly valid, perfectly empty PNG. Refuse it in words rather than
|
|
1563
|
+
// let the user save a file that is a picture of nothing.
|
|
1564
|
+
if (!(w > 0) || !(h > 0)) {
|
|
1565
|
+
panels.say('預覽的尺寸讀不出來,PNG 匯不出去');
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const img = d.createElement('img');
|
|
1569
|
+
img.onload = function () {
|
|
1570
|
+
const cv = d.createElement('canvas');
|
|
1571
|
+
cv.width = w; cv.height = h;
|
|
1572
|
+
const ctx2 = cv.getContext('2d');
|
|
1573
|
+
// The SVG's own background rect is white, but only over its own box;
|
|
1574
|
+
// painting white first means a PNG dropped into a dark-themed document
|
|
1575
|
+
// does not show that document's background through the margins.
|
|
1576
|
+
ctx2.fillStyle = '#ffffff';
|
|
1577
|
+
ctx2.fillRect(0, 0, w, h);
|
|
1578
|
+
ctx2.drawImage(img, 0, 0);
|
|
1579
|
+
cv.toBlob(function (blob) {
|
|
1580
|
+
if (blob === null) { panels.say('PNG 轉檔失敗'); return; }
|
|
1581
|
+
downloadBlob(blob, exportName('png'));
|
|
1582
|
+
panels.say('已匯出 ' + exportName('png'));
|
|
1583
|
+
}, 'image/png');
|
|
1584
|
+
};
|
|
1585
|
+
img.onerror = function () { panels.say('PNG 轉檔失敗'); };
|
|
1586
|
+
// A data URI, not an object URL: an SVG loaded into an `<img>` renders
|
|
1587
|
+
// in the browser's secure static mode either way, and a data URI needs
|
|
1588
|
+
// no revoke to pair with it on a path that can end in `onerror`.
|
|
1589
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function exportJson() {
|
|
1593
|
+
// `sourceText()`, not a second serialisation: this is the same walk of
|
|
1594
|
+
// `store.toPatch()` that the 原始碼 panel shows and that 保留並關閉
|
|
1595
|
+
// would write, so what lands on the clipboard can never disagree with
|
|
1596
|
+
// either of them.
|
|
1597
|
+
const text = sourceText();
|
|
1598
|
+
const clip = probeWin.navigator && probeWin.navigator.clipboard;
|
|
1599
|
+
if (clip && typeof clip.writeText === 'function') {
|
|
1600
|
+
clip.writeText(text).then(function () {
|
|
1601
|
+
panels.say('WaveJSON 已複製到剪貼簿');
|
|
1602
|
+
}, function () {
|
|
1603
|
+
panels.say('複製失敗,可以從右欄的原始碼面板手動選取');
|
|
1604
|
+
});
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
panels.say('這個瀏覽器不給用剪貼簿,可以從右欄的原始碼面板手動選取');
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* 檔名:markdown 檔名 + block 序號 + 副檔名。
|
|
1612
|
+
*
|
|
1613
|
+
* `opts.fileLabel` IS the source, and the caller has to pass it. The
|
|
1614
|
+
* sentence that used to stand here — that `d.title` is "the same answer by
|
|
1615
|
+
* another road", because md2doc writes `path.basename(src, '.md')` into
|
|
1616
|
+
* `<title>` — was false the moment anyone edited anything. client.js owns
|
|
1617
|
+
* the page title while a session is open and decorates it:
|
|
1618
|
+
* `document.title = (documentIsDirty() ? '● ' : '') + baseTitle`. MEASURED
|
|
1619
|
+
* on this repo's wave fixture, one lane-copy before pressing 匯出:
|
|
1620
|
+
* `document.title` is `'● doc'` and the export lands as
|
|
1621
|
+
* `●-doc-wave-1.svg`. The marker survives the sanitiser (`●` is not in its
|
|
1622
|
+
* class, and stripping leading `-` runs does not reach it), and it only
|
|
1623
|
+
* appears for a user who actually edited — so an export tried straight
|
|
1624
|
+
* after opening gives the clean name and hides it.
|
|
1625
|
+
*
|
|
1626
|
+
* The fix is not to strip `'● '` here. That would put client.js's
|
|
1627
|
+
* decoration format into a second file, and the next change to the marker
|
|
1628
|
+
* would rot it silently. client.js passes `baseTitle` instead — the title
|
|
1629
|
+
* as it was BEFORE it began decorating, which is the one value in this
|
|
1630
|
+
* system that is exactly the markdown file's basename.
|
|
1631
|
+
*
|
|
1632
|
+
* `d.title` therefore stays only as a last resort for a host that passes
|
|
1633
|
+
* no `fileLabel` at all (today there is none). It is NOT equivalent: it is
|
|
1634
|
+
* whatever that host has written into the title by the time 匯出 is
|
|
1635
|
+
* pressed.
|
|
1636
|
+
*
|
|
1637
|
+
* The sanitiser is not cosmetic: a filename with a `/` in it would
|
|
1638
|
+
* otherwise reach `a.download` as a path separator.
|
|
1639
|
+
*/
|
|
1640
|
+
function exportName(ext) {
|
|
1641
|
+
const raw = String(opts.fileLabel || d.title || 'wave');
|
|
1642
|
+
const label = raw.replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '') || 'wave';
|
|
1643
|
+
return label + '-wave-' + Number(opts.blockIndex || 0) + '.' + ext;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const exportGroup = panels.toolGroup('匯出');
|
|
1647
|
+
toolbar.appendChild(exportGroup);
|
|
1648
|
+
panels.toolButton('ed-wave-export-svg', 'SVG',
|
|
1649
|
+
'把預覽那張圖存成 SVG 檔(含 WaveDrom 的 skin,離開這一頁也畫得出來)',
|
|
1650
|
+
exportSvg, exportGroup);
|
|
1651
|
+
panels.toolButton('ed-wave-export-png', 'PNG',
|
|
1652
|
+
'把預覽那張圖存成 PNG 檔(白底,尺寸跟預覽一樣)',
|
|
1653
|
+
exportPng, exportGroup);
|
|
1654
|
+
panels.toolButton('ed-wave-export-wavejson', '複製 WaveJSON',
|
|
1655
|
+
'把右欄原始碼面板上那份 WaveJSON 複製到剪貼簿',
|
|
1656
|
+
exportJson, exportGroup);
|
|
1657
|
+
|
|
1658
|
+
// ── 檔案 ──────────────────────────────────────────────────────────────
|
|
1659
|
+
//
|
|
1660
|
+
// v3.6.0 Task 12: both wired now, unlike every disabled section above —
|
|
1661
|
+
// neither waits on a later task. 保留並關閉 runs the SAME commit path the
|
|
1662
|
+
// header's 關閉 button already runs (`close('commit')`); 放棄 is new:
|
|
1663
|
+
// before this, discarding an edit was keyboard-only (Escape), so a
|
|
1664
|
+
// pointer user had no way to abandon one. `close`'s own contract makes
|
|
1665
|
+
// "anything that is not `'escape'` is a commit" the default, so wiring
|
|
1666
|
+
// 放棄 is one line.
|
|
1667
|
+
const fileGroup = panels.toolGroup('檔案');
|
|
1668
|
+
toolbar.appendChild(fileGroup);
|
|
1669
|
+
panels.toolButton('ed-wave-file-close', '保留並關閉',
|
|
1670
|
+
'保留這次編輯並關閉(跟標題列的「關閉」是同一個動作)', function () {
|
|
1671
|
+
close('commit');
|
|
1672
|
+
}, fileGroup);
|
|
1673
|
+
panels.toolButton('ed-wave-file-discard', '放棄',
|
|
1674
|
+
'放棄這次編輯(跟 Esc 是同一個動作;按一次 Ctrl+Z 可以拿回來)', function () {
|
|
1675
|
+
close('escape');
|
|
1676
|
+
}, fileGroup);
|
|
1677
|
+
|
|
1678
|
+
// head / foot / hscale — the document-level fields the 文件 panel
|
|
1679
|
+
// exposes, plus (below, in the Lane panel) a per-lane period/phase.
|
|
1680
|
+
//
|
|
1681
|
+
// v3.6.0 Task 8 fix round 2: the sentence that used to stand here —
|
|
1682
|
+
// "the drawing does not model `hscale`, `period` or `phase` at all
|
|
1683
|
+
// (zero occurrences in wave-codec, wave-geometry or this file)" — went
|
|
1684
|
+
// false at Task 4 and nobody caught it until a review two tasks later.
|
|
1685
|
+
// `wave-geometry.js`'s `layoutOf` reads a lane's own `period`/`phase`
|
|
1686
|
+
// into `originX`/`cycleWidth`, and `hscaleOf(cfg)` reads `config.hscale`
|
|
1687
|
+
// globally; `wave-draw.js` uses those per-lane values for every brick's
|
|
1688
|
+
// x-coordinate. That is real modelling, not zero occurrences.
|
|
1689
|
+
//
|
|
1690
|
+
// v3.6.0 Task 9 fix round 2 (ruling R30) then closed the half that was
|
|
1691
|
+
// still open when the paragraph above was written: the shared cycle grid
|
|
1692
|
+
// and the ruler are DIAGRAM-wide and now derive from the same
|
|
1693
|
+
// `hscale`-scaled diagram pitch the bricks are painted at, so they move
|
|
1694
|
+
// with `period` and `config.hscale` too. `test/wave-draw.test.js` pins
|
|
1695
|
+
// both fixtures value-by-value against the engine's own render —
|
|
1696
|
+
// `period: 4` draws the engine's 17 ticks (0..16), not `cycles + 1`;
|
|
1697
|
+
// `hscale: 2` draws them 96 apart (48 × 2), the case whose tick COUNT is
|
|
1698
|
+
// unchanged and whose pitch is the only tell.
|
|
1699
|
+
//
|
|
1700
|
+
// So as of Task 18 `panels.renderUnmodelled()` no longer fires for any of
|
|
1701
|
+
// the three: they are modelled, and saying otherwise is now the false
|
|
1702
|
+
// statement. See that function's own block comment.
|
|
1703
|
+
//
|
|
1704
|
+
// v3.6.0 Task 13: a CONTROL for these three is back — `config.hscale`
|
|
1705
|
+
// here, `period`/`phase` in the Lane panel below — because what made
|
|
1706
|
+
// shipping one "the worst of the three options" (see the Task 8 comment
|
|
1707
|
+
// this replaces) was a control that INVITES the two pictures to
|
|
1708
|
+
// disagree while showing a confident wrong one. Relocating the fields
|
|
1709
|
+
// into named, individually-labelled panel rows does not change that risk
|
|
1710
|
+
// by itself. What closed it is Tasks 4 and 9 actually MODELLING the
|
|
1711
|
+
// three — in `wave-geometry.js`'s `layoutOf`/`hscaleOf` and
|
|
1712
|
+
// `wave-draw.js`'s diagram pitch, the one place cycle geometry is
|
|
1713
|
+
// allowed to live, so this file never grew a second belief about it.
|
|
1714
|
+
// A control that sets a value the canvas then draws is not the failure
|
|
1715
|
+
// the Task 8 comment was guarding against.
|
|
1716
|
+
const headInput = panels.metaField(
|
|
1717
|
+
'ed-wave-head-text', 'head', '上方標題', 'head.text', panels.sections.document);
|
|
1718
|
+
const headTickInput = panels.metaField('ed-wave-head-tick', 'head.tick',
|
|
1719
|
+
'起始刻度,例如 0', 'head.tick', panels.sections.document);
|
|
1720
|
+
const headTockInput = panels.metaField('ed-wave-head-tock', 'head.tock',
|
|
1721
|
+
'半格刻度起始值,例如 0', 'head.tock', panels.sections.document);
|
|
1722
|
+
const headEveryInput = panels.metaField('ed-wave-head-every', 'head.every',
|
|
1723
|
+
'每幾格顯示一次刻度', 'head.every', panels.sections.document);
|
|
1724
|
+
const footInput = panels.metaField(
|
|
1725
|
+
'ed-wave-foot-text', 'foot', '下方標題', 'foot.text', panels.sections.document);
|
|
1726
|
+
// v3.5.0 Task 13, fix round 1: the ruler fields — the COMPLETE set, not
|
|
1727
|
+
// the three Task 13's own brief happened to name. Measured directly
|
|
1728
|
+
// against the pinned engine rather than recalled: `renderMarks` in
|
|
1729
|
+
// `node_modules/wavedrom/lib/render-marks.js` calls
|
|
1730
|
+
// `ticktock(lane, 'head', 'tick', ...)`, `ticktock(lane, 'head', 'tock',
|
|
1731
|
+
// ...)`, `ticktock(lane, 'foot', 'tick', ...)` and
|
|
1732
|
+
// `ticktock(lane, 'foot', 'tock', ...)` — all four, not just
|
|
1733
|
+
// `head.tick`/`foot.tock` — and inside `ticktock` itself,
|
|
1734
|
+
// `cxt[ref1].every` is read off whichever of `head`/`foot` `ref1` is, so
|
|
1735
|
+
// `every` applies under BOTH, not only `head`. That is eight keys —
|
|
1736
|
+
// `{head, foot} × {text, tick, tock, every}` — of which the first Task
|
|
1737
|
+
// 13 round shipped five (`head.text`/`foot.text` from v3.4.0, plus
|
|
1738
|
+
// `head.tick`/`head.every`/`foot.tock`). The three missing ones
|
|
1739
|
+
// (`head.tock`/`foot.tick`/`foot.every`) are added here so the set is
|
|
1740
|
+
// complete rather than an arbitrary subset a user cannot predict — "why
|
|
1741
|
+
// can I put a tick above the diagram but not below it" is exactly the
|
|
1742
|
+
// shape of bug five-of-eight invites.
|
|
1743
|
+
//
|
|
1744
|
+
// Labels are the literal WaveJSON PATH (`head.tick`, not bare `tick`),
|
|
1745
|
+
// not just the leaf key the first round used — with only one of
|
|
1746
|
+
// tick/tock/every ever existing under `head` (and the mirror one under
|
|
1747
|
+
// `foot`), a bare leaf label could not be confused for its sibling; with
|
|
1748
|
+
// all four now under both, `tick` alone would leave two fields on screen
|
|
1749
|
+
// both labelled `tick` with no way to tell which of them draws above the
|
|
1750
|
+
// diagram and which below. `head`/`foot` on the two `.text` fields stay
|
|
1751
|
+
// bare, since those never had a same-named sibling to begin with.
|
|
1752
|
+
// Grouped head-then-foot in DOM order (four fields, then four fields)
|
|
1753
|
+
// rather than the first round's ad hoc growth order, for the same
|
|
1754
|
+
// readability reason. v3.6.0 Task 13: head/tick/tock/every are now
|
|
1755
|
+
// created ABOVE, before `footInput`, so the document panel's DOM order
|
|
1756
|
+
// is the full `head.*` group followed by the full `foot.*` group —
|
|
1757
|
+
// `test/editor-journey.test.js`'s five-panel scenario pins this exact
|
|
1758
|
+
// order.
|
|
1759
|
+
const footTickInput = panels.metaField('ed-wave-foot-tick', 'foot.tick',
|
|
1760
|
+
'下方起始刻度,例如 0', 'foot.tick', panels.sections.document);
|
|
1761
|
+
const footTockInput = panels.metaField('ed-wave-foot-tock', 'foot.tock',
|
|
1762
|
+
'下方半格刻度起始值,例如 0', 'foot.tock', panels.sections.document);
|
|
1763
|
+
const footEveryInput = panels.metaField('ed-wave-foot-every', 'foot.every',
|
|
1764
|
+
'下方每幾格顯示一次刻度', 'foot.every', panels.sections.document);
|
|
1765
|
+
// v3.6.0 Task 13: the document panel's ninth field — see the block
|
|
1766
|
+
// comment above for why a control for this is back at all.
|
|
1767
|
+
const hscaleInput = panels.metaField('ed-wave-config-hscale', 'hscale',
|
|
1768
|
+
'整份圖的水平縮放倍率(config.hscale,1-100 的整數;空白=不設定)',
|
|
1769
|
+
'config.hscale', panels.sections.document);
|
|
1770
|
+
// Published for `panels.renderMeta`'s lazy read, same reason as
|
|
1771
|
+
// `deps.headInput` etc. below — see `wave-panels.js`'s own header for
|
|
1772
|
+
// why these stay a lazy `deps` read rather than a return value.
|
|
1773
|
+
deps.headInput = headInput;
|
|
1774
|
+
deps.footInput = footInput;
|
|
1775
|
+
deps.headTickInput = headTickInput;
|
|
1776
|
+
deps.headTockInput = headTockInput;
|
|
1777
|
+
deps.headEveryInput = headEveryInput;
|
|
1778
|
+
deps.footTickInput = footTickInput;
|
|
1779
|
+
deps.footTockInput = footTockInput;
|
|
1780
|
+
deps.footEveryInput = footEveryInput;
|
|
1781
|
+
deps.hscaleInput = hscaleInput;
|
|
1782
|
+
|
|
1783
|
+
headInput.addEventListener('change', function () { panels.commitBanner('head', 'text', headInput.value); });
|
|
1784
|
+
footInput.addEventListener('change', function () { panels.commitBanner('foot', 'text', footInput.value); });
|
|
1785
|
+
headTickInput.addEventListener('change', function () { panels.commitBanner('head', 'tick', headTickInput.value); });
|
|
1786
|
+
headTockInput.addEventListener('change', function () {
|
|
1787
|
+
panels.commitBanner('head', 'tock', headTockInput.value);
|
|
1788
|
+
});
|
|
1789
|
+
headEveryInput.addEventListener('change', function () { panels.commitBanner('head', 'every', headEveryInput.value); });
|
|
1790
|
+
footTickInput.addEventListener('change', function () {
|
|
1791
|
+
panels.commitBanner('foot', 'tick', footTickInput.value);
|
|
1792
|
+
});
|
|
1793
|
+
footTockInput.addEventListener('change', function () { panels.commitBanner('foot', 'tock', footTockInput.value); });
|
|
1794
|
+
footEveryInput.addEventListener('change', function () {
|
|
1795
|
+
panels.commitBanner('foot', 'every', footEveryInput.value);
|
|
1796
|
+
});
|
|
1797
|
+
hscaleInput.addEventListener('change', function () {
|
|
1798
|
+
panels.commitConfigField('hscale', hscaleInput.value);
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
// ── the Lane panel: period / phase for the currently selected lane ─────
|
|
1802
|
+
//
|
|
1803
|
+
// v3.6.0 Task 13: new. "Selected lane" here is the CYCLE selection's own
|
|
1804
|
+
// `laneIndex` (`selection`, below) — this dialog has never had a
|
|
1805
|
+
// separate "selected lane" concept, and inventing a second one only for
|
|
1806
|
+
// these two fields would be a second belief about what "selected" means
|
|
1807
|
+
// in the same file. Disabled whenever `selection` is null; see
|
|
1808
|
+
// `panels.renderLaneField`'s own comment for the render side.
|
|
1809
|
+
const lanePeriodInput = panels.metaField('ed-wave-lane-period', 'period',
|
|
1810
|
+
'先選一段 cycle 才能設定', 'period', panels.sections.lane);
|
|
1811
|
+
const lanePhaseInput = panels.metaField('ed-wave-lane-phase', 'phase',
|
|
1812
|
+
'先選一段 cycle 才能設定', 'phase', panels.sections.lane);
|
|
1813
|
+
deps.lanePeriodInput = lanePeriodInput;
|
|
1814
|
+
deps.lanePhaseInput = lanePhaseInput;
|
|
1815
|
+
lanePeriodInput.addEventListener('change', function () {
|
|
1816
|
+
if (selection === null) return;
|
|
1817
|
+
panels.commitLaneField(selection.laneIndex, 'period', lanePeriodInput.value);
|
|
1818
|
+
});
|
|
1819
|
+
lanePhaseInput.addEventListener('change', function () {
|
|
1820
|
+
if (selection === null) return;
|
|
1821
|
+
panels.commitLaneField(selection.laneIndex, 'phase', lanePhaseInput.value);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
// ── the edge inspector row ──────────────────────────────────────────
|
|
1825
|
+
//
|
|
1826
|
+
// Same rule as `renderUnmodelled` below: HIDDEN, with no box on screen at
|
|
1827
|
+
// all, unless `selectedEdge` names one — `renderEdgeBar` is what flips
|
|
1828
|
+
// `edgeBar.hidden` and it is only ever called from `render()`, so the two
|
|
1829
|
+
// can never disagree. Built once here, like `headInput`/`footInput`
|
|
1830
|
+
// above; `render()` repaints its VALUES on every pass through
|
|
1831
|
+
// `renderEdgeBar`, and the usual `data-focus-key` / `restoreFocus` pair
|
|
1832
|
+
// is what keeps a half-typed label from losing the caret on that repaint.
|
|
1833
|
+
const edgeBar = d.createElement('div');
|
|
1834
|
+
edgeBar.className = 'ed-wave-edgebar';
|
|
1835
|
+
edgeBar.hidden = true;
|
|
1836
|
+
// v3.6.0 Task 13: the 關聯線 panel's body, not the toolbar — see
|
|
1837
|
+
// `wave-panels.js`'s `sections` for why the container is available
|
|
1838
|
+
// synchronously here.
|
|
1839
|
+
panels.sections.edge.appendChild(edgeBar);
|
|
1840
|
+
|
|
1841
|
+
const shapeWrap = d.createElement('label');
|
|
1842
|
+
shapeWrap.className = 'ed-wave-metafield';
|
|
1843
|
+
const shapeWrapText = d.createElement('span');
|
|
1844
|
+
shapeWrapText.textContent = '形狀';
|
|
1845
|
+
shapeWrap.appendChild(shapeWrapText);
|
|
1846
|
+
const shapeSel = d.createElement('select');
|
|
1847
|
+
shapeSel.className = 'ed-wave-edge-shape';
|
|
1848
|
+
shapeSel.setAttribute('data-focus-key', 'ed-wave-edge-shape');
|
|
1849
|
+
for (const s of codec.EDGE_SHAPES) {
|
|
1850
|
+
const o = d.createElement('option');
|
|
1851
|
+
o.value = s;
|
|
1852
|
+
o.textContent = s;
|
|
1853
|
+
shapeSel.appendChild(o);
|
|
1854
|
+
}
|
|
1855
|
+
shapeSel.addEventListener('change', function () {
|
|
1856
|
+
if (selectedEdge === null) return;
|
|
1857
|
+
commit('edge-shape', function (doc) {
|
|
1858
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1859
|
+
if (cur === null) return doc;
|
|
1860
|
+
return codec.updateEdge(doc, selectedEdge,
|
|
1861
|
+
{ from: cur.from, to: cur.to, shape: shapeSel.value, label: cur.label });
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
shapeWrap.appendChild(shapeSel);
|
|
1865
|
+
edgeBar.appendChild(shapeWrap);
|
|
1866
|
+
|
|
1867
|
+
const edgeLabelWrap = d.createElement('label');
|
|
1868
|
+
edgeLabelWrap.className = 'ed-wave-metafield';
|
|
1869
|
+
const edgeLabelWrapText = d.createElement('span');
|
|
1870
|
+
edgeLabelWrapText.textContent = '標籤';
|
|
1871
|
+
edgeLabelWrap.appendChild(edgeLabelWrapText);
|
|
1872
|
+
const edgeLabelInput = d.createElement('input');
|
|
1873
|
+
edgeLabelInput.type = 'text';
|
|
1874
|
+
edgeLabelInput.className = 'ed-wave-edge-label';
|
|
1875
|
+
edgeLabelInput.setAttribute('data-focus-key', 'ed-wave-edge-label');
|
|
1876
|
+
edgeLabelInput.placeholder = '這條線的標籤';
|
|
1877
|
+
edgeLabelInput.addEventListener('change', function () {
|
|
1878
|
+
if (selectedEdge === null) return;
|
|
1879
|
+
commit('edge-label', function (doc) {
|
|
1880
|
+
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1881
|
+
if (cur === null) return doc;
|
|
1882
|
+
return codec.updateEdge(doc, selectedEdge,
|
|
1883
|
+
{ from: cur.from, to: cur.to, shape: cur.shape, label: edgeLabelInput.value });
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
edgeLabelWrap.appendChild(edgeLabelInput);
|
|
1887
|
+
edgeBar.appendChild(edgeLabelWrap);
|
|
1888
|
+
|
|
1889
|
+
const edgeDeleteBtn = d.createElement('button');
|
|
1890
|
+
edgeDeleteBtn.type = 'button';
|
|
1891
|
+
edgeDeleteBtn.className = 'ed-wave-edge-delete';
|
|
1892
|
+
edgeDeleteBtn.setAttribute('data-focus-key', 'ed-wave-edge-delete');
|
|
1893
|
+
edgeDeleteBtn.textContent = '刪除這條線';
|
|
1894
|
+
edgeDeleteBtn.title = '刪掉這條關聯線(連帶清掉沒有其他 edge 再用的 anchor 字母)';
|
|
1895
|
+
edgeDeleteBtn.addEventListener('click', function () {
|
|
1896
|
+
if (selectedEdge === null) return;
|
|
1897
|
+
const at = selectedEdge;
|
|
1898
|
+
// Cleared BEFORE `commit`, not after: `commit` repaints synchronously,
|
|
1899
|
+
// and `removeEdge` shifts every later entry's index down by one — so an
|
|
1900
|
+
// index cleared only after `commit` returns would let `render()`'s
|
|
1901
|
+
// clamp see a DIFFERENT edge that happens to have landed on the same
|
|
1902
|
+
// number, and silently keep the inspector open on it.
|
|
1903
|
+
selectedEdge = null;
|
|
1904
|
+
commit('edge-remove', function (doc) {
|
|
1905
|
+
return codec.pruneNodes(codec.removeEdge(doc, at));
|
|
1906
|
+
});
|
|
1907
|
+
});
|
|
1908
|
+
edgeBar.appendChild(edgeDeleteBtn);
|
|
1909
|
+
// Published for `panels.renderEdgeBar`'s lazy read, same reason as
|
|
1910
|
+
// `deps.metaBar` above.
|
|
1911
|
+
deps.edgeBar = edgeBar;
|
|
1912
|
+
deps.shapeSel = shapeSel;
|
|
1913
|
+
deps.edgeLabelInput = edgeLabelInput;
|
|
808
1914
|
|
|
809
1915
|
// The "this drawing is not showing you everything" notice. Empty, and with
|
|
810
1916
|
// no box on screen, for the documents that carry none of the three.
|
|
@@ -821,19 +1927,18 @@
|
|
|
821
1927
|
noSignal.className = 'ed-wave-unmodelled ed-wave-nolanes';
|
|
822
1928
|
noSignal.hidden = true;
|
|
823
1929
|
toolbar.appendChild(noSignal);
|
|
1930
|
+
// Published for `panels.renderUnmodelled`'s lazy read, same reason as
|
|
1931
|
+
// `deps.headInput` etc. above.
|
|
1932
|
+
deps.unmodelled = unmodelled;
|
|
1933
|
+
deps.noSignal = noSignal;
|
|
824
1934
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return next;
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// ── body: lane list | drawing | preview ───────────────────────────────
|
|
1935
|
+
// ── body: lane list | drawing | right-hand column ──────────────────────
|
|
1936
|
+
//
|
|
1937
|
+
// v3.6.0 Task 13: the body's third column USED to be `previewWrap`
|
|
1938
|
+
// directly; it is now `panels.side`, the five-section right-hand column
|
|
1939
|
+
// built inside `createPanels()` (see `wave-panels.js`'s own comment
|
|
1940
|
+
// above `side`/`sections`), and `previewWrap` moves one level deeper,
|
|
1941
|
+
// into `panels.sections.preview`.
|
|
837
1942
|
const body = d.createElement('div');
|
|
838
1943
|
body.className = 'ed-wave-body';
|
|
839
1944
|
panel.appendChild(body);
|
|
@@ -841,6 +1946,9 @@
|
|
|
841
1946
|
const laneCol = d.createElement('div');
|
|
842
1947
|
laneCol.className = 'ed-wave-lanes';
|
|
843
1948
|
body.appendChild(laneCol);
|
|
1949
|
+
// Published for `panels.renderLanes`/`panels.renameGroup`'s lazy read,
|
|
1950
|
+
// same reason as `deps.headInput` etc. above.
|
|
1951
|
+
deps.laneCol = laneCol;
|
|
844
1952
|
|
|
845
1953
|
const canvasWrap = d.createElement('div');
|
|
846
1954
|
canvasWrap.className = 'ed-wave-canvas-wrap';
|
|
@@ -860,7 +1968,15 @@
|
|
|
860
1968
|
// This preview belongs to neither.
|
|
861
1969
|
previewHost.id = PREVIEW_ID_PREFIX + PREVIEW_INDEX;
|
|
862
1970
|
previewWrap.appendChild(previewHost);
|
|
863
|
-
|
|
1971
|
+
panels.sections.preview.appendChild(previewWrap);
|
|
1972
|
+
// Published for `panels.renderPreview`'s lazy read, same reason as
|
|
1973
|
+
// `deps.headInput` etc. above.
|
|
1974
|
+
deps.previewWrap = previewWrap;
|
|
1975
|
+
deps.previewHost = previewHost;
|
|
1976
|
+
|
|
1977
|
+
// The right-hand column itself, appended once — the position the old
|
|
1978
|
+
// `previewWrap` occupied directly in `body`.
|
|
1979
|
+
body.appendChild(panels.side);
|
|
864
1980
|
|
|
865
1981
|
const status = d.createElement('div');
|
|
866
1982
|
status.className = 'ed-wave-status';
|
|
@@ -869,11 +1985,9 @@
|
|
|
869
1985
|
// the one place that has to be announced rather than merely painted.
|
|
870
1986
|
status.setAttribute('role', 'status');
|
|
871
1987
|
panel.appendChild(status);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
overlay.setAttribute('data-wave-status', text);
|
|
876
|
-
}
|
|
1988
|
+
// Published for `panels.say`'s lazy read, same reason as `deps.metaBar`
|
|
1989
|
+
// above.
|
|
1990
|
+
deps.status = status;
|
|
877
1991
|
|
|
878
1992
|
// ── the one road from a gesture to the document ───────────────────────
|
|
879
1993
|
/**
|
|
@@ -898,7 +2012,7 @@
|
|
|
898
2012
|
if (closed) return false;
|
|
899
2013
|
const changed = store.apply(name, fn);
|
|
900
2014
|
if (!changed) {
|
|
901
|
-
say('這個動作沒有改變任何東西');
|
|
2015
|
+
panels.say('這個動作沒有改變任何東西');
|
|
902
2016
|
return false;
|
|
903
2017
|
}
|
|
904
2018
|
gestures++;
|
|
@@ -932,10 +2046,51 @@
|
|
|
932
2046
|
if (marksDoc === nextDoc) return;
|
|
933
2047
|
const before = marksDoc;
|
|
934
2048
|
marksDoc = nextDoc;
|
|
935
|
-
|
|
2049
|
+
// Fix round 2: `pendingFrom` joins the guard's own "is there anything
|
|
2050
|
+
// to carry" question — widened rather than given a second guard of
|
|
2051
|
+
// its own, so there stays ONE call to `opts.laneTrace` and ONE
|
|
2052
|
+
// `rowOf` built from it, not a second belief about the lookup living
|
|
2053
|
+
// next to the first. Reachable with `cursor`/`selection` both null:
|
|
2054
|
+
// the keyboard's `armed` state is not locked to a document capture
|
|
2055
|
+
// the way a mouse drag is, so a user can mark a start cell with
|
|
2056
|
+
// Enter, delete every lane from the toolbar (which nulls `cursor`/
|
|
2057
|
+
// `selection` in `renderCanvas`'s own clamp but left `pendingFrom`
|
|
2058
|
+
// alone before this fix), add lanes back past the stale index, and
|
|
2059
|
+
// finish the gesture — silently anchoring an edge to a lane the
|
|
2060
|
+
// trace never actually confirmed, because the lookup below was
|
|
2061
|
+
// skipped entirely while `pendingFrom` was the only mark still
|
|
2062
|
+
// standing. `ensureNode` bounds-checks against whatever document is
|
|
2063
|
+
// in hand at THAT moment, not against the one `pendingFrom` was
|
|
2064
|
+
// marked in, so a stale-but-back-in-range index is silently valid to
|
|
2065
|
+
// it — this file's worst failure class, wrong data written with
|
|
2066
|
+
// nothing on screen that explains it.
|
|
2067
|
+
// v3.5.0 Task 8: `endpointDrag` widens this same guard rather than
|
|
2068
|
+
// getting one of its own, for the identical reason `pendingFrom` did
|
|
2069
|
+
// in fix round 2 — one call to `opts.laneTrace`, one `rowOf`, not a
|
|
2070
|
+
// second belief about the lookup. Reachable with the other three all
|
|
2071
|
+
// null: a keyboard undo/redo reached WHILE a mouse button is down
|
|
2072
|
+
// dragging an endpoint is exactly the same shape of gap `pendingFrom`
|
|
2073
|
+
// closed — the pointer holds no document capture of its own, only
|
|
2074
|
+
// this variable's `at` does.
|
|
2075
|
+
// v3.6.0 Task 14: `selection` carries the rail's lane range itself
|
|
2076
|
+
// now, so these three are the whole list again — there is no fourth
|
|
2077
|
+
// set of row numbers standing beside them to ask about.
|
|
2078
|
+
if (before === null ||
|
|
2079
|
+
(cursor === null && selection === null && pendingFrom === null &&
|
|
2080
|
+
endpointDrag === null)) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
936
2083
|
const trace = typeof opts.laneTrace === 'function'
|
|
937
2084
|
? opts.laneTrace(before, nextDoc) : null;
|
|
938
|
-
|
|
2085
|
+
// No `laneTrace` injected: "I no longer know where you were" applies
|
|
2086
|
+
// to `pendingFrom` exactly as it already does to `cursor`/`selection`
|
|
2087
|
+
// — dropped, not guessed at. `endpointDrag` joins them: a drag naming
|
|
2088
|
+
// a lane nothing here can still vouch for is the same hazard either
|
|
2089
|
+
// way.
|
|
2090
|
+
if (trace === null) {
|
|
2091
|
+
cursor = null; selection = null; pendingFrom = null; endpointDrag = null;
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
939
2094
|
// Structural or not: a content operation traces as the identity, so this
|
|
940
2095
|
// is a no-op for a paint without having to know that it is one.
|
|
941
2096
|
const rowOf = function (was) {
|
|
@@ -949,10 +2104,79 @@
|
|
|
949
2104
|
if (at !== null) cursor = { laneIndex: at, cycle: cursor.cycle };
|
|
950
2105
|
}
|
|
951
2106
|
if (selection !== null) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
2107
|
+
// v3.6.0 Task 14, tightened in fix round 1: BOTH ends of the lane
|
|
2108
|
+
// range are retraced, independently, and an end the trace cannot
|
|
2109
|
+
// vouch for is DROPPED rather than left on a stale number. That is
|
|
2110
|
+
// the member-by-member rule the retired lane multi-select used
|
|
2111
|
+
// ("an entry that cannot be traced is removed"), applied to a pair.
|
|
2112
|
+
//
|
|
2113
|
+
// Round 0 put this whole block inside `if (rowOf(laneIndex) !== null)`,
|
|
2114
|
+
// which reads like a guard and is not one: when the range's FIRST
|
|
2115
|
+
// lane was the one that vanished, nothing ran and BOTH numbers were
|
|
2116
|
+
// left standing. `renderCanvas`'s clamp does not save it — that
|
|
2117
|
+
// only pulls `laneTo` down to the last row and never collapses a
|
|
2118
|
+
// range — so 建立群組 and 訊號's 刪除 acted on a span the user never
|
|
2119
|
+
// selected. Reachable in three gestures: insert a lane, Shift-select
|
|
2120
|
+
// from it downwards, undo the insert.
|
|
2121
|
+
//
|
|
2122
|
+
// What survives, by case:
|
|
2123
|
+
// both ends traced → the traced range (re-normalised: a reorder
|
|
2124
|
+
// can swap which end is lower)
|
|
2125
|
+
// one end traced → collapsed onto that end — the one lane still
|
|
2126
|
+
// vouched for
|
|
2127
|
+
// neither traced → collapsed onto `laneIndex`'s OLD number. The
|
|
2128
|
+
// number is kept because `laneIndex` doubles as
|
|
2129
|
+
// a screen position `renderCanvas` re-clamps and
|
|
2130
|
+
// `cursor`'s own "land on whoever took the
|
|
2131
|
+
// place" instinct is right for that; the SPAN is
|
|
2132
|
+
// not kept, so a structural op can at worst
|
|
2133
|
+
// touch that one row, never a range.
|
|
2134
|
+
const near = rowOf(selection.laneIndex);
|
|
2135
|
+
const far = rowOf(selection.laneTo);
|
|
2136
|
+
const a = near !== null ? near : (far !== null ? far : selection.laneIndex);
|
|
2137
|
+
const b = far !== null ? far : a;
|
|
2138
|
+
selection = {
|
|
2139
|
+
laneIndex: Math.min(a, b), laneTo: Math.max(a, b),
|
|
2140
|
+
from: selection.from, to: selection.to,
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
// `pendingFrom` is a mark of the exact same shape as `cursor` (a row
|
|
2144
|
+
// number plus a cycle), carried the same way — by lane IDENTITY
|
|
2145
|
+
// through this same trace, not by raw row number (see the guard
|
|
2146
|
+
// above for why it must reach here even when `cursor`/`selection`
|
|
2147
|
+
// cannot). It deliberately does NOT copy cursor's "leave the number
|
|
2148
|
+
// alone when untraceable" half, though: `cursor` gets away with that
|
|
2149
|
+
// because `renderCanvas` re-clamps it into range on every repaint, and
|
|
2150
|
+
// because a cursor landing on "whatever now occupies that visual
|
|
2151
|
+
// position" after its own lane vanished is the right instinct for a
|
|
2152
|
+
// position marker. `pendingFrom` has no such clamp anywhere, and it is
|
|
2153
|
+
// not a position — it is a lane the user already committed to as one
|
|
2154
|
+
// fixed end of an edge. Fix round 2: leaving it at a stale raw index
|
|
2155
|
+
// is exactly the hazard the coordinator's review walked to the end —
|
|
2156
|
+
// delete the marked lane, add unrelated lanes back until the count
|
|
2157
|
+
// passes the stale number again, and `ensureNode` would bounds-check
|
|
2158
|
+
// successfully against a lane the user never chose. So an untraceable
|
|
2159
|
+
// `pendingFrom` is DROPPED here, the same as `trace === null` above
|
|
2160
|
+
// drops it wholesale — never left standing on a number nothing
|
|
2161
|
+
// vouches for.
|
|
2162
|
+
if (pendingFrom !== null) {
|
|
2163
|
+
const at = rowOf(pendingFrom.at);
|
|
2164
|
+
pendingFrom = at === null ? null : { at: at, cell: pendingFrom.cell };
|
|
2165
|
+
}
|
|
2166
|
+
// `endpointDrag.at` is a mark of the exact same shape as
|
|
2167
|
+
// `pendingFrom.at` — a lane the user is currently pointing a live
|
|
2168
|
+
// gesture at, not a position `renderCanvas` re-clamps on every
|
|
2169
|
+
// repaint — so it is carried the same way and dropped the same way
|
|
2170
|
+
// when the lane it named cannot be traced. Left alone while `at` is
|
|
2171
|
+
// still `null` (mousedown fired, no `pointermove` has landed on a
|
|
2172
|
+
// real cell yet): there is no row number there yet to go stale.
|
|
2173
|
+
// `index`/`end`/`cell` are not row numbers and need no retracing
|
|
2174
|
+
// here — `index` is guarded separately by `render()`'s widened
|
|
2175
|
+
// `selectedEdge` clamp (the drag dies with the selection it drags).
|
|
2176
|
+
if (endpointDrag !== null && endpointDrag.at !== null) {
|
|
2177
|
+
const at = rowOf(endpointDrag.at);
|
|
2178
|
+
endpointDrag = at === null ? null
|
|
2179
|
+
: { index: endpointDrag.index, end: endpointDrag.end, at: at, cell: endpointDrag.cell };
|
|
956
2180
|
}
|
|
957
2181
|
}
|
|
958
2182
|
|
|
@@ -965,9 +2189,9 @@
|
|
|
965
2189
|
lastPatch = store.toPatch();
|
|
966
2190
|
render();
|
|
967
2191
|
if (lastPatch.ok === true) {
|
|
968
|
-
say('已寫回:' + name);
|
|
2192
|
+
panels.say('已寫回:' + name);
|
|
969
2193
|
} else {
|
|
970
|
-
say('這個改動沒辦法只改幾個位元組寫回去:' + lastPatch.reason);
|
|
2194
|
+
panels.say('這個改動沒辦法只改幾個位元組寫回去:' + lastPatch.reason);
|
|
971
2195
|
}
|
|
972
2196
|
if (typeof opts.onGesture === 'function') {
|
|
973
2197
|
opts.onGesture({ name: name, patch: lastPatch, store: store });
|
|
@@ -977,6 +2201,192 @@
|
|
|
977
2201
|
// ── painting ──────────────────────────────────────────────────────────
|
|
978
2202
|
let canvas = null;
|
|
979
2203
|
|
|
2204
|
+
/**
|
|
2205
|
+
* v3.6.0 Task 13 fix round 4: keep the CURRENTLY selected edge's own
|
|
2206
|
+
* drawn handles clear of the expanded side rail — by narrowing the
|
|
2207
|
+
* RAIL, never by moving the canvas.
|
|
2208
|
+
*
|
|
2209
|
+
* Diagnosed against a real page before writing this (see task-13-
|
|
2210
|
+
* report.md, fix round 4), and the first two things tried both failed
|
|
2211
|
+
* against that diagnosis before this one was chosen:
|
|
2212
|
+
*
|
|
2213
|
+
* 1. Hypothesis (mine and the reviewer's, both wrong): the expand
|
|
2214
|
+
* reflows the canvas mid-gesture, so a point resolved before it
|
|
2215
|
+
* goes stale. MEASURED false — `.ed-wave-canvas`'s own
|
|
2216
|
+
* `getBoundingClientRect()` is IDENTICAL before, during and after
|
|
2217
|
+
* the whole gesture. Nothing moves.
|
|
2218
|
+
* 2. First fix tried: scroll the canvas horizontally to carry the
|
|
2219
|
+
* handle out from under the rail (`canvasWrap.scrollLeft`), the
|
|
2220
|
+
* same technique `pointInCanvas` uses in the test suite. MEASURED
|
|
2221
|
+
* to do nothing on the fixture that actually fails: a short
|
|
2222
|
+
* document's drawn content is NARROWER than the wrap's own
|
|
2223
|
+
* `clientWidth`, so `scrollWidth === clientWidth` — there is no
|
|
2224
|
+
* scrollABLE range at all, and setting `scrollLeft` past 0 is
|
|
2225
|
+
* silently clamped straight back to 0. The rail's expansion does
|
|
2226
|
+
* not add any scrollable width to the wrap (it is an absolutely
|
|
2227
|
+
* positioned overlay, outside the wrap's own box model), so a
|
|
2228
|
+
* short document can never be scrolled clear of it.
|
|
2229
|
+
*
|
|
2230
|
+
* The actual defect both of those diagnosed correctly but did not fix
|
|
2231
|
+
* is occlusion, not displacement: the expanded rail is `position:
|
|
2232
|
+
* absolute`, OPAQUE, `z-index: 3`, painted ON TOP of the canvas at a
|
|
2233
|
+
* narrow viewport (round 1's own deliberate design — the rail overlays
|
|
2234
|
+
* rather than squeezes, so the canvas keeps its width-budget win).
|
|
2235
|
+
* `elementFromPoint` at the handle's own coordinates resolved to a
|
|
2236
|
+
* `<span>` INSIDE the rail, not the handle — because the rail is what
|
|
2237
|
+
* is actually painted there now, regardless of scroll position.
|
|
2238
|
+
*
|
|
2239
|
+
* So this narrows the rail instead: its `width` (normally 340px, the
|
|
2240
|
+
* CSS default) is set only as far down as needed to keep the selected
|
|
2241
|
+
* edge's rightmost handle clear, floored at `MIN_WIDTH` so the panel
|
|
2242
|
+
* never shrinks to something unusable. `right: 0` stays anchored (the
|
|
2243
|
+
* CSS is untouched), so shrinking width moves the rail's LEFT edge
|
|
2244
|
+
* right, away from the handle, while the canvas itself never moves at
|
|
2245
|
+
* all — satisfying the same invariant the "reflow mid-gesture"
|
|
2246
|
+
* hypothesis was chasing (`.ed-wave-canvas`'s rect never changes
|
|
2247
|
+
* between a press and its release) for a different, correct reason.
|
|
2248
|
+
*
|
|
2249
|
+
* Recomputed from scratch on every call (the leading `style.width =
|
|
2250
|
+
* ''` is not an optimization to skip) rather than only on a change of
|
|
2251
|
+
* `selectedEdge` — an unrelated repaint can still move a lane's own
|
|
2252
|
+
* geometry (hscale, a resize) while the SAME edge stays selected, and
|
|
2253
|
+
* a stale width would either pointlessly over-narrow the rail or,
|
|
2254
|
+
* worse, under-narrow it and silently reopen the exact defect this
|
|
2255
|
+
* exists to close.
|
|
2256
|
+
*
|
|
2257
|
+
* Selecting an edge is very often immediately followed by dragging the
|
|
2258
|
+
* very handle selection just exposed — it is the ONLY way to adjust an
|
|
2259
|
+
* endpoint (`onCanvasDown`'s drag-start branch only ever fires for the
|
|
2260
|
+
* SELECTED edge's own handle), so select-then-drag is not an edge
|
|
2261
|
+
* case, it is the whole mechanism this protects.
|
|
2262
|
+
*
|
|
2263
|
+
* v3.6.0 final fix (M2): every handle is measured against the WRAP's own
|
|
2264
|
+
* visible rect before it is allowed to move anything. `.ed-wave-canvas-
|
|
2265
|
+
* wrap` is `overflow: auto`, and `getBoundingClientRect()` on a child it
|
|
2266
|
+
* has scrolled or clipped out of sight still returns a full viewport
|
|
2267
|
+
* rect for a box that is painted NOWHERE — this repo's own documented
|
|
2268
|
+
* trap, which the test helper `pointInCanvas` already applies and this
|
|
2269
|
+
* function did not. Two things follow, and both are load-bearing:
|
|
2270
|
+
*
|
|
2271
|
+
* - A handle with no intersection with the wrap's rect is SKIPPED
|
|
2272
|
+
* outright. It cannot be overlapped by the rail, because it is not
|
|
2273
|
+
* on screen to overlap.
|
|
2274
|
+
* - A handle that is only PARTLY visible contributes the right edge of
|
|
2275
|
+
* its visible part (`min(r.right, wrapRect.right)`), not its full
|
|
2276
|
+
* rect's right. Clamping alone would not do: a fully clipped handle
|
|
2277
|
+
* clamps to `wrapRect.right`, which is the largest value in the
|
|
2278
|
+
* whole calculation and would narrow the rail harder than the real
|
|
2279
|
+
* defect did. The skip is what makes the clamp safe.
|
|
2280
|
+
*
|
|
2281
|
+
* MEASURED on the fixture `test/wave-rail-clearance.test.js` now carries
|
|
2282
|
+
* (24 cycles, `edge: ['a~>b']` from cycle 1 to cycle 20, a real press on
|
|
2283
|
+
* the drawn `.ed-wave-edge-hit` stroke), at 1000x900 — the viewport this
|
|
2284
|
+
* whole version exists for, since `.ed-wave-side[data-expanded]` is
|
|
2285
|
+
* `position: absolute` below 1100px:
|
|
2286
|
+
*
|
|
2287
|
+
* to-handle right 1332.2 wrap right 970 visible false
|
|
2288
|
+
* before: side.style.width '160px' rendered 160px label input 23px
|
|
2289
|
+
* after: side.style.width '' rendered 340px label input 203px
|
|
2290
|
+
*
|
|
2291
|
+
* The rail was being collapsed to its floor by a handle drawn 362px
|
|
2292
|
+
* past the clip. The edge-shape `<select>` measures 50px in BOTH states
|
|
2293
|
+
* and is deliberately NOT quoted as a symptom — it is a fixed-width
|
|
2294
|
+
* control, so it survives the squeeze that guts the `<input>` beside it,
|
|
2295
|
+
* and quoting it would read as damage that was never there.
|
|
2296
|
+
*
|
|
2297
|
+
* The wrap's border box is used for that visibility test rather than its
|
|
2298
|
+
* padding box, matching `pointInCanvas`'s own visibility check (which
|
|
2299
|
+
* reserves the padding box for scroll ARITHMETIC). The wrap carries
|
|
2300
|
+
* padding and no border, and content is visible inside padding, so the
|
|
2301
|
+
* two boxes are the same rect here anyway.
|
|
2302
|
+
*
|
|
2303
|
+
* Where this genuinely does nothing, stated as the code behaves rather
|
|
2304
|
+
* than as round 4 hoped: an inline width IS still applied at a wide
|
|
2305
|
+
* viewport — MEASURED 160px at 1280x900 and 198px at 1600x900 on that
|
|
2306
|
+
* same 24-cycle document before this fix, i.e. the old "no inline width
|
|
2307
|
+
* is ever applied at a wide viewport" claim was simply false. It was
|
|
2308
|
+
* harmless there for a DIFFERENT reason than the one claimed:
|
|
2309
|
+
* `.ed-wave-side`'s `flex: 0 0 340px` beats an inline `width` for a flex
|
|
2310
|
+
* item's main size, so the rendered column stayed 340px regardless. The
|
|
2311
|
+
* same mechanism covers the collapsed 28px rail (`flex: 0 0 28px`), so a
|
|
2312
|
+
* leftover inline style from a PRIOR expansion cannot keep it
|
|
2313
|
+
* artificially wide. Only the `position: absolute` expanded state below
|
|
2314
|
+
* 1100px takes the inline width for real — which is exactly why that is
|
|
2315
|
+
* the state the defect showed up in.
|
|
2316
|
+
*
|
|
2317
|
+
* Residual, accepted limit — and it IS reached, including by this
|
|
2318
|
+
* function's own test fixture. When `bodyRight - rightmost - MARGIN`
|
|
2319
|
+
* falls below `MIN_WIDTH`, this clamps to `MIN_WIDTH` rather than
|
|
2320
|
+
* shrinking further, and the selected handle stays covered by the rail.
|
|
2321
|
+
*
|
|
2322
|
+
* MEASURED at HEAD on `MEDIUM_MD` — the 12-cycle fixture in
|
|
2323
|
+
* `test/wave-rail-clearance.test.js`'s own scenario 2 — at 1000x900:
|
|
2324
|
+
*
|
|
2325
|
+
* to-handle 888.2–900.2, painted and not clipped at all
|
|
2326
|
+
* bodyRight 970 -> fits = 970 - 900.2 - 4 = 65.8 (< MIN_WIDTH)
|
|
2327
|
+
* side.style.width '160px', so the rail's left edge lands at 810
|
|
2328
|
+
* => 90.2px of the handle is still under the rail, and
|
|
2329
|
+
* `elementFromPoint` at the handle's own centre returns a
|
|
2330
|
+
* `<span>` INSIDE `.ed-wave-side`, not the handle
|
|
2331
|
+
*
|
|
2332
|
+
* It needs no contrived fixture: 16 cycles with the `to` end at cycle
|
|
2333
|
+
* 12, at 970x900, leaves 166.4px of overlap the same way.
|
|
2334
|
+
*
|
|
2335
|
+
* The sentence that stood here — "a small residual overlap in a
|
|
2336
|
+
* genuinely too-narrow-to-satisfy-both-at-once case … which no fixture
|
|
2337
|
+
* here produces" — was therefore wrong three times: the overlap is most
|
|
2338
|
+
* of the handle rather than small, the case is ordinary rather than
|
|
2339
|
+
* genuinely-too-narrow, and the fixture that produces it shipped in the
|
|
2340
|
+
* SAME commit as the sentence denying it. It is recorded rather than
|
|
2341
|
+
* quietly replaced because it is this batch's signature defect (a true
|
|
2342
|
+
* piece of code with a false sentence beside it) landing inside the
|
|
2343
|
+
* delivery that exists to fix that defect — one clause after the
|
|
2344
|
+
* warning that "no fixture here reaches it" is a claim about fixtures,
|
|
2345
|
+
* never about what a user can reach.
|
|
2346
|
+
*
|
|
2347
|
+
* Why it is accepted rather than fixed here: below 1100px the rail is
|
|
2348
|
+
* `position: absolute` and `bodyRight === wrapRight` (MEASURED: both
|
|
2349
|
+
* 970 above), so a handle drawn near the right-hand end of a wide
|
|
2350
|
+
* diagram cannot be cleared by narrowing the rail AT ALL — no width
|
|
2351
|
+
* satisfies both, and `MIN_WIDTH` is the floor precisely so the panel
|
|
2352
|
+
* does not become unusable while trying. Clearing it needs a different
|
|
2353
|
+
* mechanism (scroll the wrap, or anchor the overlay to the other edge),
|
|
2354
|
+
* which is a design change and not a bound to re-tune.
|
|
2355
|
+
*
|
|
2356
|
+
* One more thing this does not do, in the opposite direction, and it
|
|
2357
|
+
* behaves identically before and after the fix above so it is not a
|
|
2358
|
+
* regression: the wrap has no scroll listener, so scrolling AFTER an
|
|
2359
|
+
* edge is selected does not re-run the verdict. MEASURED on the
|
|
2360
|
+
* 24-cycle fixture at 1000x900 — `scrollLeft = 356` brings the `to`
|
|
2361
|
+
* handle back to 964.2–976.2, i.e. painted and squarely under the rail,
|
|
2362
|
+
* while `side.style.width` stays `''` from the reading taken at
|
|
2363
|
+
* `scrollLeft 0`. That is the only measurable way the new
|
|
2364
|
+
* skip-what-the-wrap-clipped rule can be wrong, and it is a STALE
|
|
2365
|
+
* verdict rather than a wrong one.
|
|
2366
|
+
*/
|
|
2367
|
+
function clearSelectedEdgeOfSide() {
|
|
2368
|
+
panels.side.style.width = '';
|
|
2369
|
+
if (selectedEdge === null) return;
|
|
2370
|
+
const bodyRight = body.getBoundingClientRect().right;
|
|
2371
|
+
const wrapRect = canvasWrap.getBoundingClientRect();
|
|
2372
|
+
let rightmost = null;
|
|
2373
|
+
for (const end of ['from', 'to']) {
|
|
2374
|
+
const h = d.querySelector('.ed-wave-edge-handle[data-edge-index="' +
|
|
2375
|
+
selectedEdge + '"][data-edge-handle="' + end + '"]');
|
|
2376
|
+
if (h === null) continue;
|
|
2377
|
+
const r = h.getBoundingClientRect();
|
|
2378
|
+
if (r.right <= wrapRect.left || r.left >= wrapRect.right) continue;
|
|
2379
|
+
if (r.bottom <= wrapRect.top || r.top >= wrapRect.bottom) continue;
|
|
2380
|
+
const painted = Math.min(r.right, wrapRect.right);
|
|
2381
|
+
if (rightmost === null || painted > rightmost) rightmost = painted;
|
|
2382
|
+
}
|
|
2383
|
+
if (rightmost === null) return;
|
|
2384
|
+
const MARGIN = 4;
|
|
2385
|
+
const MIN_WIDTH = 160;
|
|
2386
|
+
const fits = bodyRight - rightmost - MARGIN;
|
|
2387
|
+
if (fits < 340) panels.side.style.width = Math.max(MIN_WIDTH, Math.round(fits)) + 'px';
|
|
2388
|
+
}
|
|
2389
|
+
|
|
980
2390
|
/**
|
|
981
2391
|
* Repaint everything, and give the keyboard cursor back where it was.
|
|
982
2392
|
*
|
|
@@ -1008,13 +2418,101 @@
|
|
|
1008
2418
|
const before = focusMark();
|
|
1009
2419
|
const doc = store.doc;
|
|
1010
2420
|
const layout = geometry.layoutOf(doc, SIZES);
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
2421
|
+
// Clamped BEFORE `renderCanvas`/`renderEdgeBar` read it, and against
|
|
2422
|
+
// `edgeLayout`'s own filtered list rather than `doc.edge.length` — an
|
|
2423
|
+
// edge whose anchor letter went missing is still an array element (so
|
|
2424
|
+
// a length check would miss it) but `edgeLayout` already will not draw
|
|
2425
|
+
// it, and an inspector row pointed at an edge nothing on screen shows
|
|
2426
|
+
// is exactly the stale-selection defect `carryMarks` above exists to
|
|
2427
|
+
// avoid for lanes.
|
|
2428
|
+
if (selectedEdge !== null && !edgeIsDrawable(doc, layout, selectedEdge)) {
|
|
2429
|
+
selectedEdge = null;
|
|
2430
|
+
}
|
|
2431
|
+
// v3.5.0 Task 8: `endpointDrag.index` can only ever be reached from
|
|
2432
|
+
// `selectedEdge`'s own drawn handle (see `onCanvasDown`), so the two
|
|
2433
|
+
// may never disagree — checked with `!==` rather than "is `index`
|
|
2434
|
+
// still drawable" on its own, because that would let a drag survive a
|
|
2435
|
+
// mid-drag undo that DESELECTS the edge without deleting it (the
|
|
2436
|
+
// clamp above nulls `selectedEdge` on its own timing). A drag that
|
|
2437
|
+
// outlives the selection it was dragging is exactly the class of
|
|
2438
|
+
// hazard Task 7 paid three rounds to close for `pendingFrom`/
|
|
2439
|
+
// `edgeMode`; this is the same rule applied to this task's own mark.
|
|
2440
|
+
if (endpointDrag !== null && endpointDrag.index !== selectedEdge) {
|
|
2441
|
+
endpointDrag = null;
|
|
2442
|
+
}
|
|
2443
|
+
panels.renderLanes(doc, layout, { selection: selection });
|
|
2444
|
+
// v3.6.0 Task 13: the Lane panel's period/phase — same `selection` mark
|
|
2445
|
+
// `renderLanes` above already reads for the CYCLE highlight, not a
|
|
2446
|
+
// second "selected lane" concept.
|
|
2447
|
+
panels.renderLaneField(doc, layout, { selection: selection });
|
|
2448
|
+
const wds = {
|
|
2449
|
+
cursor: cursor,
|
|
2450
|
+
selection: selection,
|
|
2451
|
+
pendingFrom: pendingFrom,
|
|
2452
|
+
endpointDrag: endpointDrag,
|
|
2453
|
+
selectedEdge: selectedEdge,
|
|
2454
|
+
overlay: overlay,
|
|
2455
|
+
canvasWrap: canvasWrap,
|
|
2456
|
+
dataEdit: dataEdit,
|
|
2457
|
+
enterDrawing: enterDrawing,
|
|
2458
|
+
onCanvasDown: onCanvasDown,
|
|
2459
|
+
onCanvasHover: onCanvasHover,
|
|
2460
|
+
edgeMode: edgeMode,
|
|
2461
|
+
hoverBoundary: hoverBoundary,
|
|
2462
|
+
};
|
|
2463
|
+
canvas = drawer.renderCanvas(canvas, doc, layout, wds);
|
|
2464
|
+
cursor = wds.cursor;
|
|
2465
|
+
selection = wds.selection;
|
|
2466
|
+
panels.renderEdgeBar(doc, { selectedEdge: selectedEdge });
|
|
2467
|
+
// v3.6.0 Task 13 fix round 3: selecting an edge is an action whose
|
|
2468
|
+
// ENTIRE feedback surface is the 關聯線 panel just painted above —
|
|
2469
|
+
// at a narrow viewport (<1100px) that panel sits inside the
|
|
2470
|
+
// collapsed rail, where every one of its controls renders at zero
|
|
2471
|
+
// size (`wave-panels.js`'s `section()` docstring and `lib/md2doc.js`'s
|
|
2472
|
+
// `.ed-wave-side` media query: `data-open` is the SECTION's own
|
|
2473
|
+
// toggle state and says nothing about whether the RAIL is currently
|
|
2474
|
+
// showing it). Left alone, a narrow-viewport user who selects an edge
|
|
2475
|
+
// gets no hint that the only surface to edit or delete it exists at
|
|
2476
|
+
// all. So this expands the rail — but only on the TRANSITION into
|
|
2477
|
+
// having a selection (`lastSelectedEdge === null`, checked against
|
|
2478
|
+
// the PREVIOUS render, not "selectedEdge !== null" on its own):
|
|
2479
|
+
// expanding on every repaint would fight a user who deliberately
|
|
2480
|
+
// collapses the rail back (the fix-round-1 button) while still
|
|
2481
|
+
// working on the same selected edge — the very next keystroke would
|
|
2482
|
+
// pop it back open. Firing once, at the moment of selection, is the
|
|
2483
|
+
// honest response without becoming a second one-way door.
|
|
2484
|
+
//
|
|
2485
|
+
// Lane/cycle selection does NOT get the same treatment, by deliberate
|
|
2486
|
+
// choice, not oversight: unlike an edge, selecting a lane already has
|
|
2487
|
+
// visible feedback OUTSIDE the side column — `renderLanes` highlights
|
|
2488
|
+
// the row (`.is-selected`) and the canvas itself draws the cycle
|
|
2489
|
+
// selection — so a narrow-viewport user who selects a lane is never
|
|
2490
|
+
// left looking at nothing the way an edge-select would leave them.
|
|
2491
|
+
if (selectedEdge !== null && lastSelectedEdge === null) {
|
|
2492
|
+
panels.expandSide();
|
|
2493
|
+
}
|
|
2494
|
+
lastSelectedEdge = selectedEdge;
|
|
2495
|
+
// v3.6.0 Task 13 fix round 4: re-checked on EVERY render while an
|
|
2496
|
+
// edge is selected, not only on the transition that just (maybe)
|
|
2497
|
+
// expanded the rail above — see `clearSelectedEdgeOfSide`'s own
|
|
2498
|
+
// comment for the mechanism. Unlike `expandSide()`, this has no
|
|
2499
|
+
// "only once" concern: it recomputes and resets from scratch each
|
|
2500
|
+
// call, so it stays correct across switching directly from one
|
|
2501
|
+
// selected edge to another, an unrelated repaint moving the SAME
|
|
2502
|
+
// edge's own handle (a resize, an hscale change), or the rail
|
|
2503
|
+
// collapsing back (where it correctly clears its own inline style).
|
|
2504
|
+
clearSelectedEdgeOfSide();
|
|
2505
|
+
panels.renderPreview(doc);
|
|
2506
|
+
panels.renderMeta(doc);
|
|
2507
|
+
panels.renderUnmodelled(doc, layout);
|
|
2508
|
+
// v3.6.0 Task 13: the 原始碼 panel. Repainted every pass, like every
|
|
2509
|
+
// other panel above — cheap (one `store.toPatch()`, already run once
|
|
2510
|
+
// per gesture by `afterStoreMoved`) and it means the panel can never
|
|
2511
|
+
// show a patch older than what is on screen.
|
|
2512
|
+
panels.renderSource();
|
|
1016
2513
|
undoBtn.disabled = !store.canUndo();
|
|
1017
2514
|
redoBtn.disabled = !store.canRedo();
|
|
2515
|
+
paintSelectionState(doc, layout);
|
|
1018
2516
|
overlay.setAttribute('data-wave-gestures', String(gestures));
|
|
1019
2517
|
overlay.setAttribute('data-wave-lanes', String(layout.lanes.length));
|
|
1020
2518
|
overlay.setAttribute('data-wave-cycles', String(layout.cycles));
|
|
@@ -1025,6 +2523,16 @@
|
|
|
1025
2523
|
restoreFocus(before);
|
|
1026
2524
|
}
|
|
1027
2525
|
|
|
2526
|
+
/** Whether `geometry.edgeLayout` would still draw the edge at `index` —
|
|
2527
|
+
* the same test the drawing itself applies, asked rather than repeated. */
|
|
2528
|
+
function edgeIsDrawable(doc, layout, index) {
|
|
2529
|
+
const edges = geometry.edgeLayout(doc, layout);
|
|
2530
|
+
for (const e of edges) {
|
|
2531
|
+
if (e.index === index) return true;
|
|
2532
|
+
}
|
|
2533
|
+
return false;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
1028
2536
|
/** What has the keyboard now, as something that survives a rebuild. */
|
|
1029
2537
|
function focusMark() {
|
|
1030
2538
|
const el = d.activeElement;
|
|
@@ -1046,6 +2554,16 @@
|
|
|
1046
2554
|
const want = focusOverride !== null ? focusOverride : (mark === null ? null : mark.key);
|
|
1047
2555
|
focusOverride = null;
|
|
1048
2556
|
if (want === null) return;
|
|
2557
|
+
// v3.6.0 Task 14: "did not come back" includes "came back DISABLED".
|
|
2558
|
+
// `.focus()` on a disabled control is inert — the fact T6h pinned for
|
|
2559
|
+
// the rail's ▲ at the top of the list — and the toolbar is now where
|
|
2560
|
+
// the gestures that disable their own button live: 訊號's 刪除 empties
|
|
2561
|
+
// the selection, so the very button the user pressed is disabled by
|
|
2562
|
+
// the repaint that press caused. Found-but-inert fell straight through
|
|
2563
|
+
// to `document.body`, which is the state this whole function exists to
|
|
2564
|
+
// prevent, and which turns the next Backspace into「delete the
|
|
2565
|
+
// selected blocks」.
|
|
2566
|
+
const inert = function (node) { return node === null || node.disabled === true; };
|
|
1049
2567
|
let el = overlay.querySelector('[data-focus-key="' + want + '"]');
|
|
1050
2568
|
// A named destination the repaint did not bring back. It happens: a
|
|
1051
2569
|
// group's key is its first lane's row number, so inserting a lane above
|
|
@@ -1054,10 +2572,10 @@
|
|
|
1054
2572
|
// where the keyboard actually was, and failing that to the dialog
|
|
1055
2573
|
// itself. Never to nothing: nothing is `document.body`, and this whole
|
|
1056
2574
|
// task is about not ending there.
|
|
1057
|
-
if (el
|
|
2575
|
+
if (inert(el) && mark !== null && mark.key !== want) {
|
|
1058
2576
|
el = overlay.querySelector('[data-focus-key="' + mark.key + '"]');
|
|
1059
2577
|
}
|
|
1060
|
-
if (el
|
|
2578
|
+
if (inert(el)) {
|
|
1061
2579
|
if (!overlay.contains(d.activeElement)) panel.focus();
|
|
1062
2580
|
return;
|
|
1063
2581
|
}
|
|
@@ -1070,689 +2588,185 @@
|
|
|
1070
2588
|
} catch (e) { /* not a text field */ }
|
|
1071
2589
|
}
|
|
1072
2590
|
|
|
1073
|
-
function renderMeta(doc) {
|
|
1074
|
-
const h = doc.head;
|
|
1075
|
-
const f = doc.foot;
|
|
1076
|
-
headInput.value = (h !== null && typeof h === 'object' && typeof h.text === 'string')
|
|
1077
|
-
? h.text : '';
|
|
1078
|
-
footInput.value = (f !== null && typeof f === 'object' && typeof f.text === 'string')
|
|
1079
|
-
? f.text : '';
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
/**
|
|
1083
|
-
* Say out loud which properties of this document the drawing is not
|
|
1084
|
-
* showing.
|
|
1085
|
-
*
|
|
1086
|
-
* `config.hscale`, and a lane's `period` / `phase`, all change what the
|
|
1087
|
-
* engine paints and none of them is modelled here (see the toolbar comment
|
|
1088
|
-
* for the measurements). Drawing a confident picture that ignores them is
|
|
1089
|
-
* the failure this whole overlay is arranged to avoid, so a document that
|
|
1090
|
-
* carries any of them says so, names them, and points at the preview —
|
|
1091
|
-
* which IS faithful, because it is the engine.
|
|
1092
|
-
*/
|
|
1093
|
-
function renderUnmodelled(doc) {
|
|
1094
|
-
const found = [];
|
|
1095
|
-
const cfg = doc.config;
|
|
1096
|
-
if (cfg !== null && typeof cfg === 'object' && cfg.hscale !== undefined) {
|
|
1097
|
-
found.push('config.hscale');
|
|
1098
|
-
}
|
|
1099
|
-
for (const path of codec.lanePaths(doc)) {
|
|
1100
|
-
const lane = valueAt(doc, path);
|
|
1101
|
-
if (lane === null || typeof lane !== 'object') continue;
|
|
1102
|
-
if (lane.period !== undefined && found.indexOf('period') === -1) found.push('period');
|
|
1103
|
-
if (lane.phase !== undefined && found.indexOf('phase') === -1) found.push('phase');
|
|
1104
|
-
}
|
|
1105
|
-
overlay.setAttribute('data-wave-unmodelled', found.join(' '));
|
|
1106
|
-
unmodelled.hidden = found.length === 0;
|
|
1107
|
-
unmodelled.textContent = found.length === 0 ? '' :
|
|
1108
|
-
'這份文件用了 ' + found.join('、') +
|
|
1109
|
-
',左邊的手繪波形不表現它們(寬度與相位會不一樣)——以右邊的 WaveDrom 預覽為準。';
|
|
1110
|
-
|
|
1111
|
-
// …and the bigger version of the same honesty. This editor edits
|
|
1112
|
-
// `signal:` and nothing else, so a `reg:` or `assign:` block opens a
|
|
1113
|
-
// canvas with no lanes on it: `lanePaths()` is empty, `addLane()` hands
|
|
1114
|
-
// the document straight back, and pressing + produced one status line
|
|
1115
|
-
//「這個動作沒有改變任何東西」— true, and no explanation at all. An
|
|
1116
|
-
// editor that looks broken is worse than one that says what it cannot
|
|
1117
|
-
// do, and the notice above is the precedent: the drawing already tells
|
|
1118
|
-
// the user which properties it is not showing them.
|
|
1119
|
-
const kinds = [];
|
|
1120
|
-
for (const key of ['reg', 'assign']) {
|
|
1121
|
-
if (doc[key] !== undefined) kinds.push(key + ':');
|
|
1122
|
-
}
|
|
1123
|
-
const noLanes = codec.lanePaths(doc).length === 0;
|
|
1124
|
-
overlay.setAttribute('data-wave-nolanes', noLanes ? '1' : '0');
|
|
1125
|
-
noSignal.hidden = !noLanes;
|
|
1126
|
-
noSignal.textContent = !noLanes ? '' :
|
|
1127
|
-
(kinds.length === 0
|
|
1128
|
-
? '這個區塊裡沒有 signal:,所以沒有可以編輯的 lane。'
|
|
1129
|
-
: '這個區塊用的是 ' + kinds.join(' / ') +
|
|
1130
|
-
',這個編輯器只編輯 signal: 的 lane。') +
|
|
1131
|
-
'右邊的 WaveDrom 預覽仍然是引擎畫的原圖;要改這個區塊請用「⠿ → MD 原始碼」。';
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
2591
|
/**
|
|
1135
|
-
* The
|
|
2592
|
+
* The in-place editor for one bus cell's `data` label — this dialog's
|
|
2593
|
+
* OTHER sub-panel, same `{cancel, retire}` shape as `renameGroup` just
|
|
2594
|
+
* above and opened the same two ways every other gesture in this file
|
|
2595
|
+
* is: a pointer press (`onCanvasDown`) or the keyboard's own Enter
|
|
2596
|
+
* (`handleDrawingKey`), both of which have already asked
|
|
2597
|
+
* `codec.dataSlotOf` before calling this, so `slot` below is never
|
|
2598
|
+
* recomputed from a belief of its own about which cycles consume one.
|
|
1136
2599
|
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
// lane's own controls stay clickable over them.
|
|
1150
|
-
for (const span of groupSpansOf(doc, layout)) {
|
|
1151
|
-
const tag = d.createElement('button');
|
|
1152
|
-
tag.type = 'button';
|
|
1153
|
-
tag.className = 'ed-wave-group';
|
|
1154
|
-
tag.setAttribute('data-focus-key', 'group-' + span.from);
|
|
1155
|
-
tag.textContent = span.title;
|
|
1156
|
-
tag.title = '群組「' + span.title + '」——點一下改名';
|
|
1157
|
-
tag.setAttribute('data-group-from', String(span.from));
|
|
1158
|
-
tag.setAttribute('data-group-to', String(span.to));
|
|
1159
|
-
tag.style.top = (span.from * layout.laneHeight) + 'px';
|
|
1160
|
-
tag.style.height = ((span.to - span.from + 1) * layout.laneHeight) + 'px';
|
|
1161
|
-
tag.style.left = ((span.depth - 1) * 16) + 'px';
|
|
1162
|
-
tag.addEventListener('click', function () { renameGroup(span); });
|
|
1163
|
-
rows.appendChild(tag);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
1167
|
-
rows.appendChild(laneRow(doc, layout, i));
|
|
1168
|
-
}
|
|
1169
|
-
// The tail insert. `laneInsertPath(doc, laneCount)` goes on the end of
|
|
1170
|
-
// `signal` itself, which is what makes the group asymmetry visible here
|
|
1171
|
-
// rather than surprising afterwards.
|
|
1172
|
-
rows.appendChild(insertButton(doc, layout.lanes.length, layout, true));
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function laneRow(doc, layout, i) {
|
|
1176
|
-
const row = d.createElement('div');
|
|
1177
|
-
row.className = 'ed-wave-lane-row';
|
|
1178
|
-
row.setAttribute('data-lane', String(i));
|
|
1179
|
-
row.style.top = (i * layout.laneHeight) + 'px';
|
|
1180
|
-
row.style.height = layout.laneHeight + 'px';
|
|
1181
|
-
row.style.paddingLeft = (layout.lanes[i].depth * 16) + 'px';
|
|
1182
|
-
if (selection !== null && selection.laneIndex === i) row.classList.add('is-selected');
|
|
1183
|
-
|
|
1184
|
-
row.appendChild(insertButton(doc, i, layout, false));
|
|
1185
|
-
|
|
1186
|
-
const name = d.createElement('input');
|
|
1187
|
-
name.type = 'text';
|
|
1188
|
-
name.className = 'ed-wave-lane-name';
|
|
1189
|
-
name.setAttribute('data-focus-key', 'lane-name-' + i);
|
|
1190
|
-
const lane = layout.lanes[i].lane;
|
|
1191
|
-
name.value = typeof lane.name === 'string' ? lane.name : '';
|
|
1192
|
-
name.addEventListener('change', function () {
|
|
1193
|
-
commit('rename', function (doc2) { return codec.renameLane(doc2, i, name.value); });
|
|
1194
|
-
});
|
|
1195
|
-
row.appendChild(name);
|
|
1196
|
-
|
|
1197
|
-
const up = d.createElement('button');
|
|
1198
|
-
up.type = 'button';
|
|
1199
|
-
up.className = 'ed-wave-lane-up';
|
|
1200
|
-
up.setAttribute('data-focus-key', 'lane-up-' + i);
|
|
1201
|
-
up.textContent = '▲';
|
|
1202
|
-
up.title = '往上移一格';
|
|
1203
|
-
up.disabled = i === 0;
|
|
1204
|
-
up.addEventListener('click', function () {
|
|
1205
|
-
// The button this lane arrives at, EXCEPT at the top, where `lane-up-0`
|
|
1206
|
-
// is disabled and `.focus()` on it is inert — measured, walking a lane
|
|
1207
|
-
// to the top with three ▲ put the cursor on `lane-up-2`, `lane-up-1`
|
|
1208
|
-
// and then BODY, which is the state F7 exists to prevent and the fourth
|
|
1209
|
-
// press then did nothing at all. At the top the cursor goes to the one
|
|
1210
|
-
// control on that row that is never disabled there: ▼.
|
|
1211
|
-
focusOverride = i - 1 === 0 ? 'lane-down-0' : 'lane-up-' + (i - 1);
|
|
1212
|
-
commit('move', function (doc2) { return codec.moveLane(doc2, i, i - 1); });
|
|
1213
|
-
});
|
|
1214
|
-
row.appendChild(up);
|
|
1215
|
-
|
|
1216
|
-
const down = d.createElement('button');
|
|
1217
|
-
down.type = 'button';
|
|
1218
|
-
down.className = 'ed-wave-lane-down';
|
|
1219
|
-
down.setAttribute('data-focus-key', 'lane-down-' + i);
|
|
1220
|
-
down.textContent = '▼';
|
|
1221
|
-
down.title = '往下移一格';
|
|
1222
|
-
down.disabled = i === layout.lanes.length - 1;
|
|
1223
|
-
down.addEventListener('click', function () {
|
|
1224
|
-
// Mirror of ▲ above: `lane-down-<last>` is disabled, so the bottom row
|
|
1225
|
-
// hands the cursor to ▲ instead.
|
|
1226
|
-
focusOverride = i + 1 === layout.lanes.length - 1
|
|
1227
|
-
? 'lane-up-' + (i + 1) : 'lane-down-' + (i + 1);
|
|
1228
|
-
commit('move', function (doc2) { return codec.moveLane(doc2, i, i + 1); });
|
|
1229
|
-
});
|
|
1230
|
-
row.appendChild(down);
|
|
1231
|
-
|
|
1232
|
-
const del = d.createElement('button');
|
|
1233
|
-
del.type = 'button';
|
|
1234
|
-
del.className = 'ed-wave-lane-remove';
|
|
1235
|
-
del.setAttribute('data-focus-key', 'lane-remove-' + i);
|
|
1236
|
-
del.textContent = '✕';
|
|
1237
|
-
del.title = '刪掉這條 lane(如果群組因此空掉,群組也會一起走)';
|
|
1238
|
-
del.addEventListener('click', function () {
|
|
1239
|
-
selection = null;
|
|
1240
|
-
// The row this button belongs to is about to stop existing, so the
|
|
1241
|
-
// keyboard goes to the + that occupies that position instead — the
|
|
1242
|
-
// one control at that spot that survives every removal, tail included.
|
|
1243
|
-
focusOverride = 'lane-add-' + i;
|
|
1244
|
-
commit('remove-lane', function (doc2) { return codec.removeLane(doc2, i); });
|
|
1245
|
-
});
|
|
1246
|
-
row.appendChild(del);
|
|
1247
|
-
return row;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
/**
|
|
1251
|
-
* Where a lane inserted at display position `index` would actually land,
|
|
1252
|
-
* asked of the bridge rather than guessed.
|
|
1253
|
-
*
|
|
1254
|
-
* This is constraint 4 made visible. `laneInsertPath` puts an insert at a
|
|
1255
|
-
* group's FIRST lane inside that group, and an insert at the position just
|
|
1256
|
-
* past its LAST lane outside it — a group can be joined at its head and
|
|
1257
|
-
* never at its tail. That asymmetry is not something a label can argue
|
|
1258
|
-
* away, so the label states it: the button says which container it will
|
|
1259
|
-
* land in, before it is pressed, and the status line says it again after.
|
|
1260
|
-
*/
|
|
1261
|
-
function landingOf(doc, index) {
|
|
1262
|
-
const path = codec.laneInsertPath(doc, index);
|
|
1263
|
-
if (path === null) return { path: null, title: null };
|
|
1264
|
-
const container = path.slice(0, path.length - 1);
|
|
1265
|
-
const held = valueAt(doc, container);
|
|
1266
|
-
const title = Array.isArray(held) && typeof held[0] === 'string' ? held[0] : null;
|
|
1267
|
-
return { path: path, title: title };
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function insertButton(doc, index, layout, isTail) {
|
|
1271
|
-
const b = d.createElement('button');
|
|
1272
|
-
b.type = 'button';
|
|
1273
|
-
b.className = isTail ? 'ed-wave-lane-add ed-wave-lane-add-tail' : 'ed-wave-lane-add';
|
|
1274
|
-
b.textContent = '+';
|
|
1275
|
-
b.setAttribute('data-insert-at', String(index));
|
|
1276
|
-
b.setAttribute('data-focus-key', 'lane-add-' + index);
|
|
1277
|
-
const landing = landingOf(doc, index);
|
|
1278
|
-
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
1279
|
-
b.setAttribute('data-lands-in', landing.title === null ? '' : landing.title);
|
|
1280
|
-
b.title = '在這裡加一條 lane —— 會落在' + where;
|
|
1281
|
-
if (isTail) {
|
|
1282
|
-
b.style.top = (layout.lanes.length * layout.laneHeight) + 'px';
|
|
1283
|
-
}
|
|
1284
|
-
b.addEventListener('click', function () {
|
|
1285
|
-
// Straight into the new lane's name field: it is what the user is going
|
|
1286
|
-
// to type next, and it is the only control the new row introduces.
|
|
1287
|
-
focusOverride = 'lane-name-' + index;
|
|
1288
|
-
const added = commit('add-lane', function (doc2) {
|
|
1289
|
-
return codec.addLane(doc2, index, { name: '', wave: 'x' });
|
|
1290
|
-
});
|
|
1291
|
-
if (added) say('新增的 lane 落在' + where);
|
|
1292
|
-
});
|
|
1293
|
-
return b;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
/**
|
|
1297
|
-
* The rail's rename field: this dialog's one sub-panel.
|
|
1298
|
-
*
|
|
1299
|
-
* The field is INSERTED beside its tag and the tag is hidden, rather than
|
|
1300
|
-
* the two being swapped, and taking it down again touches only those two
|
|
1301
|
-
* nodes. That is not tidiness. Taking it down used to mean a full
|
|
1302
|
-
* `render()` — a rebuild of the entire rail — and a rebuild that runs
|
|
1303
|
-
* inside a `blur` handler destroys the element the user is in the middle
|
|
1304
|
-
* of pressing. Measured before this: open the field, then press any rail
|
|
1305
|
-
* button; the mousedown blurred the field, the rail was rebuilt, the
|
|
1306
|
-
* mouseup landed on a fresh element so no `click` ever fired, the press
|
|
1307
|
-
* did nothing at all, and the keyboard ended on `document.body` inside a
|
|
1308
|
-
* modal with no visible cursor.
|
|
2600
|
+
* Positioned with `geometry.cellRect` — the exact numbers the keyboard
|
|
2601
|
+
* cursor and the pointer's own hit test already share — offset by
|
|
2602
|
+
* `canvas.offsetLeft`/`offsetTop` rather than the wrap's own padding
|
|
2603
|
+
* written out as a constant: the field is a plain sibling of the SVG
|
|
2604
|
+
* inside `.ed-wave-canvas-wrap` (now `position: relative`, see
|
|
2605
|
+
* `lib/md2doc.js`), so reading the SVG's OWN measured offset is what
|
|
2606
|
+
* keeps the two from drifting apart if that padding ever changes.
|
|
2607
|
+
*
|
|
2608
|
+
* Unlike `renameGroup`'s field, this one's node sits beside a canvas
|
|
2609
|
+
* `renderCanvas` rebuilds on every cursor move — see that function's own
|
|
2610
|
+
* first line for why `retire()` has to be called from there too, not
|
|
2611
|
+
* only from `render()`.
|
|
1309
2612
|
*/
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
2613
|
+
function openDataEdit(laneIndex, cycle) {
|
|
2614
|
+
if (canvas === null) return;
|
|
2615
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
2616
|
+
const row = layout.lanes[laneIndex];
|
|
2617
|
+
const lane = row === undefined ? null : row.lane;
|
|
2618
|
+
const slot = lane === null ? null : codec.dataSlotOf(lane, cycle);
|
|
2619
|
+
if (slot === null) return;
|
|
2620
|
+
const box = geometry.cellRect(layout, laneIndex, cycle);
|
|
2621
|
+
if (box === null) return;
|
|
2622
|
+
// A press or an Enter that lands on a second bus cell while the field
|
|
2623
|
+
// is still open on the first one: the open field never got the blur
|
|
2624
|
+
// that a plain click elsewhere would have given it (the keyboard's
|
|
2625
|
+
// Enter path never moves focus at all), so it is cancelled here,
|
|
2626
|
+
// by hand, before this one opens. Unsaved text in the first field is
|
|
2627
|
+
// discarded — the same trade `renameGroup` makes for its own field on
|
|
2628
|
+
// every route that is not `finish()` itself.
|
|
2629
|
+
if (dataEdit !== null) dataEdit.cancel();
|
|
2630
|
+
|
|
2631
|
+
const current = labelsOf(lane)[slot];
|
|
2632
|
+
const startValue = typeof current === 'string' ? current : '';
|
|
1315
2633
|
|
|
1316
2634
|
const input = d.createElement('input');
|
|
1317
2635
|
input.type = 'text';
|
|
1318
|
-
input.className = 'ed-wave-
|
|
1319
|
-
input.setAttribute('data-focus-key', '
|
|
1320
|
-
input.value =
|
|
1321
|
-
input.style.
|
|
1322
|
-
input.style.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
2636
|
+
input.className = 'ed-wave-data-input';
|
|
2637
|
+
input.setAttribute('data-focus-key', 'data-input-' + laneIndex + '-' + cycle);
|
|
2638
|
+
input.value = startValue;
|
|
2639
|
+
input.style.left = (canvas.offsetLeft + box.x) + 'px';
|
|
2640
|
+
input.style.top = (canvas.offsetTop + box.y) + 'px';
|
|
2641
|
+
input.style.width = box.width + 'px';
|
|
2642
|
+
input.style.height = box.height + 'px';
|
|
2643
|
+
|
|
2644
|
+
/** The field off. A no-op once a repaint has already taken it away. */
|
|
1326
2645
|
const takeDown = function () {
|
|
1327
2646
|
if (input.parentNode !== null) input.parentNode.removeChild(input);
|
|
1328
|
-
if (d.contains(tag)) tag.hidden = false;
|
|
1329
2647
|
};
|
|
1330
|
-
/**
|
|
1331
|
-
*
|
|
1332
|
-
* above it renumbers the key) — whatever now carries that key, and
|
|
1333
|
-
* failing that the dialog itself. Never nothing: nothing is
|
|
1334
|
-
* `document.body`. */
|
|
1335
|
-
const focusTag = function () {
|
|
1336
|
-
const live = d.contains(tag) ? tag
|
|
1337
|
-
: overlay.querySelector('[data-focus-key="group-' + span.from + '"]');
|
|
1338
|
-
if (live !== null) live.focus();
|
|
1339
|
-
else panel.focus();
|
|
1340
|
-
};
|
|
1341
|
-
/** Whether the keyboard is IN the field right now. False on the `blur`
|
|
1342
|
-
* path — the browser has already moved it by then — which is how the
|
|
1343
|
-
* two are told apart without asking which device did anything. */
|
|
2648
|
+
/** Whether the keyboard is IN the field right now. False on the
|
|
2649
|
+
* `blur` path — the browser has already moved it by then. */
|
|
1344
2650
|
const holdsKeyboard = function () { return d.activeElement === input; };
|
|
1345
2651
|
|
|
1346
2652
|
const finish = function () {
|
|
1347
|
-
// Once. `
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1351
|
-
|
|
2653
|
+
// Once. `blur` and this file's own explicit Enter handler can both
|
|
2654
|
+
// reach here for the one commit (Enter removes the field, which
|
|
2655
|
+
// fires a `blur` of its own) — the latch is the same "whoever gets
|
|
2656
|
+
// here first owns the gesture" rule `renameGroup.finish` uses.
|
|
2657
|
+
if (dataEdit === null) return;
|
|
2658
|
+
dataEdit = null;
|
|
1352
2659
|
const hadKeyboard = holdsKeyboard();
|
|
1353
2660
|
const value = input.value;
|
|
1354
2661
|
takeDown();
|
|
1355
|
-
if (value ===
|
|
1356
|
-
// Nothing to write, so nothing to repaint —
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
// pressed, and that control is still there to receive it now that
|
|
1360
|
-
// this path no longer rebuilds the rail.
|
|
1361
|
-
if (hadKeyboard) focusTag();
|
|
2662
|
+
if (value === startValue) {
|
|
2663
|
+
// Nothing to write, so nothing to repaint — `canvas` is already
|
|
2664
|
+
// the element it was before this field opened.
|
|
2665
|
+
if (hadKeyboard) canvas.focus();
|
|
1362
2666
|
return;
|
|
1363
2667
|
}
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
2668
|
+
// The keyboard is only NAMED when it would otherwise be lost — same
|
|
2669
|
+
// condition `renameGroup.finish` uses: either this field had it, or
|
|
2670
|
+
// whatever DOES have it right now is not even inside the overlay
|
|
2671
|
+
// (both mean the coming repaint would otherwise leave it on
|
|
2672
|
+
// `document.body`). `focusOverride` already set by someone else's
|
|
2673
|
+
// more specific promise is left alone.
|
|
2674
|
+
if (focusOverride === null && (hadKeyboard || !overlay.contains(d.activeElement))) {
|
|
2675
|
+
focusOverride = 'canvas';
|
|
1370
2676
|
}
|
|
1371
|
-
commit('
|
|
1372
|
-
return
|
|
2677
|
+
commit('data-label', function (doc) {
|
|
2678
|
+
return codec.setDataAt(doc, laneIndex, slot, value);
|
|
1373
2679
|
});
|
|
1374
2680
|
};
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
// focus would be left on a node that is no longer in the document — which
|
|
1381
|
-
// is the wedge that lost every subsequent keystroke.
|
|
2681
|
+
/** The canvas is about to be rebuilt underneath this field by
|
|
2682
|
+
* `renderCanvas` — an undo, a redo, any gesture that repaints while
|
|
2683
|
+
* it is open. The node goes with that rebuild; what matters is
|
|
2684
|
+
* dropping the latch and naming where the keyboard lands, exactly
|
|
2685
|
+
* the same reason `renameGroup.retire` exists. */
|
|
1382
2686
|
const retire = function () {
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1385
|
-
if (focusOverride === null && holdsKeyboard())
|
|
1386
|
-
focusOverride = 'group-' + span.from;
|
|
1387
|
-
}
|
|
2687
|
+
if (dataEdit === null) return;
|
|
2688
|
+
dataEdit = null;
|
|
2689
|
+
if (focusOverride === null && holdsKeyboard()) focusOverride = 'canvas';
|
|
1388
2690
|
};
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
// whole of this task exists to make unreachable. No repaint at all: the
|
|
1393
|
-
// tag was never destroyed.
|
|
2691
|
+
/** Escape's half: the field goes, the label does not change, the
|
|
2692
|
+
* session stands, and the keyboard goes back to the drawing — never
|
|
2693
|
+
* to `document.body`. No repaint: the canvas was never touched. */
|
|
1394
2694
|
const cancel = function () {
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
2695
|
+
if (dataEdit === null) return;
|
|
2696
|
+
dataEdit = null;
|
|
1397
2697
|
takeDown();
|
|
1398
|
-
|
|
2698
|
+
canvas.focus();
|
|
1399
2699
|
};
|
|
1400
|
-
|
|
2700
|
+
// The explicit commit key: Enter. Left to a plain native `change`
|
|
2701
|
+
// event the way `renameGroup`'s field is would depend on whether this
|
|
2702
|
+
// browser fires `change` for a lone text input on Enter alone, which
|
|
2703
|
+
// is not a promise this file makes elsewhere — asked for outright
|
|
2704
|
+
// instead, the same way Escape is asked for explicitly one level up
|
|
2705
|
+
// in `onKeyDown` rather than left to a default.
|
|
2706
|
+
input.addEventListener('keydown', function (ev) {
|
|
2707
|
+
if (ev.key !== 'Enter') return;
|
|
2708
|
+
ev.preventDefault();
|
|
2709
|
+
ev.stopPropagation();
|
|
2710
|
+
finish();
|
|
2711
|
+
});
|
|
1401
2712
|
input.addEventListener('change', finish);
|
|
1402
2713
|
input.addEventListener('blur', finish);
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
// Three facts, and only the three that are read: that a rename is
|
|
1406
|
-
// standing (this being non-null), how to take it down on purpose, and how
|
|
1407
|
-
// to let go of it when a repaint takes it down for us. A copy of the span
|
|
1408
|
-
// or of the field itself would be a second record of something the
|
|
1409
|
-
// closure above already holds.
|
|
1410
|
-
groupRename = { cancel: cancel, retire: retire };
|
|
2714
|
+
canvasWrap.appendChild(input);
|
|
2715
|
+
dataEdit = { cancel: cancel, retire: retire };
|
|
1411
2716
|
input.focus();
|
|
1412
2717
|
input.select();
|
|
1413
2718
|
}
|
|
1414
2719
|
|
|
1415
2720
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
2721
|
+
* Where a press or an Enter on cycle `cycle` of `lane` should open the
|
|
2722
|
+
* data-label field — or whether it must refuse the gesture outright
|
|
2723
|
+
* instead of letting it fall through to an ordinary paint.
|
|
2724
|
+
*
|
|
2725
|
+
* v3.5.0 Task 11 fix round 1: a cycle INSIDE a bus run that itself owns
|
|
2726
|
+
* no slot (`codec.dataSlotOf` answers `null` — a `.` continuation or a
|
|
2727
|
+
* `|` gap) used to fall straight through to the paint-arming code with
|
|
2728
|
+
* nothing catching it. Reviewer-traced: the press silently repainted
|
|
2729
|
+
* that continuation with whatever brush was current, turning it into a
|
|
2730
|
+
* fresh EXPLICIT value cycle and shifting every LATER label in the lane
|
|
2731
|
+
* one slot to the right — silent data corruption, not an inert click,
|
|
2732
|
+
* because the box this cycle sits inside is exactly what this task now
|
|
2733
|
+
* invites the user to press.
|
|
2734
|
+
*
|
|
2735
|
+
* `isBus(brickOf(...))` is the same test `drawLane` uses to decide
|
|
2736
|
+
* whether a cycle is a bus box at all for the PICTURE, asked here to
|
|
2737
|
+
* decide it for the GESTURE — so a plain non-bus cycle (`0`, `1`, `x`, a
|
|
2738
|
+
* clock, …) is untouched and keeps painting exactly as it always has;
|
|
2739
|
+
* only a cycle INSIDE a bus box is ever diverted. Walking backward for
|
|
2740
|
+
* the owning cycle stops the moment the resolved LEVEL changes — two
|
|
2741
|
+
* adjacent bus runs of the identical value (`2.2.` — measured against
|
|
2742
|
+
* `codec`'s own doc comment: two separate labels, not one) must not
|
|
2743
|
+
* have a later cycle resolve into the EARLIER run's slot.
|
|
2744
|
+
*
|
|
2745
|
+
* Returns:
|
|
2746
|
+
* `{cycle, slot}` — open the field for THIS cycle's slot: either
|
|
2747
|
+
* `cycle` itself owns one, or the walk back to its
|
|
2748
|
+
* run's owning explicit cycle found one.
|
|
2749
|
+
* `'blocked'` — inside a bus box, but no owning cycle exists (a
|
|
2750
|
+
* leading repeater with nothing to continue —
|
|
2751
|
+
* MEASURED, `levelsOf` draws such a lane as `x`,
|
|
2752
|
+
* so this should be unreachable in practice, but
|
|
2753
|
+
* the check is kept rather than assumed so a
|
|
2754
|
+
* future change to that rule fails LOUD here
|
|
2755
|
+
* rather than silently reaching paint again).
|
|
2756
|
+
* Callers must refuse the gesture on this answer,
|
|
2757
|
+
* never fall through.
|
|
2758
|
+
* `null` — not a bus box; ordinary behaviour is unaffected.
|
|
1424
2759
|
*/
|
|
1425
|
-
function
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
return
|
|
1433
|
-
}
|
|
1434
|
-
const out = Object.assign({}, node);
|
|
1435
|
-
out[seg] = child;
|
|
1436
|
-
return out;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* The hand-drawn waveform.
|
|
1441
|
-
*
|
|
1442
|
-
* Every cycle's picture comes from `codec.levelsOf` through `brickOf`, and
|
|
1443
|
-
* from nowhere else — constraint 3. The levels and the bricks are both
|
|
1444
|
-
* published on the element (`data-levels-N`, `data-bricks-N`) so the
|
|
1445
|
-
* agreement with the preview beside it is something a test can measure
|
|
1446
|
-
* rather than something this comment can promise: the preview's own SVG
|
|
1447
|
-
* names its bricks in its `<use xlink:href="#…">` list.
|
|
1448
|
-
*/
|
|
1449
|
-
function renderCanvas(doc, layout) {
|
|
1450
|
-
// Every repaint REPLACES the canvas element, so a canvas that had the
|
|
1451
|
-
// keyboard loses it to `document.body` the instant it is detached —
|
|
1452
|
-
// exactly the state F7 and T6h exist to prevent, and here it would fire
|
|
1453
|
-
// on every arrow key. `render()`'s own `data-focus-key` restore covers
|
|
1454
|
-
// the full-repaint path; this covers the three that call renderCanvas
|
|
1455
|
-
// directly (a drag's two, and the keyboard's own), so the property holds
|
|
1456
|
-
// for the canvas no matter which road the repaint came down.
|
|
1457
|
-
const hadFocus = canvas !== null && d.activeElement === canvas;
|
|
1458
|
-
// The cursor is an index pair, and the document under it can get shorter:
|
|
1459
|
-
// deleting cycles or removing the bottom lane leaves it pointing past the
|
|
1460
|
-
// end. Clamped HERE, where the layout that decides what "past the end"
|
|
1461
|
-
// means is already in hand — so nothing downstream ever has to ask.
|
|
1462
|
-
if (cursor !== null) {
|
|
1463
|
-
if (layout.lanes.length === 0 || layout.cycles === 0) {
|
|
1464
|
-
cursor = null;
|
|
1465
|
-
} else {
|
|
1466
|
-
cursor = {
|
|
1467
|
-
laneIndex: Math.max(0, Math.min(cursor.laneIndex, layout.lanes.length - 1)),
|
|
1468
|
-
cycle: Math.max(0, Math.min(cursor.cycle, layout.cycles - 1)),
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
// …and `selection` with it, which is the other half of the same fact and
|
|
1473
|
-
// was left out of the first cut. Measured before this: Shift-select four
|
|
1474
|
-
// cycles, press 刪除 cycle, and a one-cycle document was left with the
|
|
1475
|
-
// cursor on cycle 1, no visible box, 複製 reporting「複製了 4 個 cycle」
|
|
1476
|
-
// and 貼上(插入)growing the document back to five. The toolbar was
|
|
1477
|
-
// acting on a range the drawing no longer had anywhere to put.
|
|
1478
|
-
//
|
|
1479
|
-
// The two axes are not treated alike, and that is the point: a CYCLE
|
|
1480
|
-
// range that overshoots is the same range in a shorter document, so it
|
|
1481
|
-
// clamps; a LANE that is gone is not a lane the user can have meant, so
|
|
1482
|
-
// the selection goes rather than silently moving to whichever lane
|
|
1483
|
-
// inherited the index. (`lane ✕` already nulls it on its own path; this
|
|
1484
|
-
// catches the undo/redo of that.)
|
|
1485
|
-
//
|
|
1486
|
-
// It does NOT catch every route, and the earlier wording here said it
|
|
1487
|
-
// did. MEASURED: `+` at 0, drag-select cycles 1-3 on the new lane 0,
|
|
1488
|
-
// then Ctrl+Z — the selection box and the rail highlight both stay on
|
|
1489
|
-
// row 0, which is now `clk`. The trace-derived marks above fix the
|
|
1490
|
-
// CURSOR for that case; the selection is still carried by row number,
|
|
1491
|
-
// so it lands on whoever inherited the row. The cost is confined to the
|
|
1492
|
-
// highlight and to where the next Shift+arrow anchors, because every
|
|
1493
|
-
// cycle operation is document-wide — but it is a real gap, not a
|
|
1494
|
-
// covered one.
|
|
1495
|
-
if (selection !== null) {
|
|
1496
|
-
if (layout.lanes.length === 0 || layout.cycles === 0 ||
|
|
1497
|
-
selection.laneIndex < 0 || selection.laneIndex >= layout.lanes.length) {
|
|
1498
|
-
selection = null;
|
|
1499
|
-
} else {
|
|
1500
|
-
selection = {
|
|
1501
|
-
laneIndex: selection.laneIndex,
|
|
1502
|
-
from: Math.max(0, Math.min(selection.from, layout.cycles - 1)),
|
|
1503
|
-
to: Math.max(0, Math.min(selection.to, layout.cycles - 1)),
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
canvasWrap.textContent = '';
|
|
1508
|
-
canvas = d.createElementNS(SVGNS, 'svg');
|
|
1509
|
-
canvas.setAttribute('class', 'ed-wave-canvas');
|
|
1510
|
-
// In the Tab cycle, and therefore in the focus trap, like every other
|
|
1511
|
-
// control in here: the drawing is this dialog's main surface and was the
|
|
1512
|
-
// one thing in it a keyboard could not reach.
|
|
1513
|
-
canvas.setAttribute('tabindex', '0');
|
|
1514
|
-
canvas.setAttribute('data-focus-key', 'canvas');
|
|
1515
|
-
canvas.setAttribute('aria-label',
|
|
1516
|
-
'波形繪圖區:方向鍵移動游標,Shift+左右鍵選一段 cycle,' +
|
|
1517
|
-
'按電位字元(' + BRUSHES.join(' ') + ')塗上去');
|
|
1518
|
-
const width = Math.max(layout.width, SIZES.cycleWidth);
|
|
1519
|
-
canvas.setAttribute('width', String(width));
|
|
1520
|
-
canvas.setAttribute('height', String(Math.max(layout.height, layout.laneHeight)));
|
|
1521
|
-
canvas.setAttribute('viewBox', '0 0 ' + width + ' ' +
|
|
1522
|
-
Math.max(layout.height, layout.laneHeight));
|
|
1523
|
-
canvas.setAttribute('data-lane-count', String(layout.lanes.length));
|
|
1524
|
-
canvas.setAttribute('data-cycle-count', String(layout.cycles));
|
|
1525
|
-
|
|
1526
|
-
// cycle grid
|
|
1527
|
-
for (let c = 0; c <= layout.cycles; c++) {
|
|
1528
|
-
const g = d.createElementNS(SVGNS, 'line');
|
|
1529
|
-
g.setAttribute('class', 'ed-wave-grid');
|
|
1530
|
-
g.setAttribute('x1', String(c * layout.cycleWidth));
|
|
1531
|
-
g.setAttribute('x2', String(c * layout.cycleWidth));
|
|
1532
|
-
g.setAttribute('y1', '0');
|
|
1533
|
-
g.setAttribute('y2', String(layout.height));
|
|
1534
|
-
canvas.appendChild(g);
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
1538
|
-
drawLane(canvas, layout, i);
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
if (selection !== null) {
|
|
1542
|
-
const from = Math.min(selection.from, selection.to);
|
|
1543
|
-
const to = Math.max(selection.from, selection.to);
|
|
1544
|
-
const box = d.createElementNS(SVGNS, 'rect');
|
|
1545
|
-
box.setAttribute('class', 'ed-wave-selection');
|
|
1546
|
-
box.setAttribute('x', String(from * layout.cycleWidth));
|
|
1547
|
-
box.setAttribute('y', String(selection.laneIndex * layout.laneHeight));
|
|
1548
|
-
box.setAttribute('width', String((to - from + 1) * layout.cycleWidth));
|
|
1549
|
-
box.setAttribute('height', String(layout.laneHeight));
|
|
1550
|
-
canvas.appendChild(box);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// The keyboard's cell, drawn last so it is over the selection tint and
|
|
1554
|
-
// over the trace. Its geometry comes from `wave-geometry.cellRect` — the
|
|
1555
|
-
// inverse of the hit test the pointer uses — so the cell the cursor is
|
|
1556
|
-
// DRAWN on and the cell an edit lands in cannot drift apart.
|
|
1557
|
-
let cursorCell = '';
|
|
1558
|
-
if (cursor !== null) {
|
|
1559
|
-
const cell = geometry.cellRect(layout, cursor.laneIndex, cursor.cycle);
|
|
1560
|
-
if (cell !== null) {
|
|
1561
|
-
cursorCell = cursor.laneIndex + ',' + cursor.cycle;
|
|
1562
|
-
const cur = d.createElementNS(SVGNS, 'rect');
|
|
1563
|
-
cur.setAttribute('class', 'ed-wave-cursor');
|
|
1564
|
-
cur.setAttribute('data-ed-wave-cursor', '');
|
|
1565
|
-
cur.setAttribute('data-cell', cursorCell);
|
|
1566
|
-
cur.setAttribute('x', String(cell.x));
|
|
1567
|
-
cur.setAttribute('y', String(cell.y));
|
|
1568
|
-
cur.setAttribute('width', String(cell.width));
|
|
1569
|
-
cur.setAttribute('height', String(cell.height));
|
|
1570
|
-
canvas.appendChild(cur);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
// The same value on the overlay: what a scenario reads when the rect is
|
|
1574
|
-
// not the thing under test. It is assigned INSIDE the branch that draws
|
|
1575
|
-
// the rect and defaults to empty, so "there is no rect" and "the overlay
|
|
1576
|
-
// says none" are one decision rather than two that happen to agree —
|
|
1577
|
-
// they did not, in the first cut: this line read `cursor` directly, so a
|
|
1578
|
-
// cursor the clamp had not caught published a cell that `cellRect`
|
|
1579
|
-
// refused to give a rectangle to, and a build with the clamp removed
|
|
1580
|
-
// reported `{cell: null, published: "5,4"}`.
|
|
1581
|
-
overlay.setAttribute('data-wave-cursor', cursorCell);
|
|
1582
|
-
|
|
1583
|
-
canvas.addEventListener('mousedown', onCanvasDown);
|
|
1584
|
-
// Tabbing onto the drawing is an entry like any other, so it puts the
|
|
1585
|
-
// cursor on screen rather than leaving a focused surface with no visible
|
|
1586
|
-
// cursor in it. Re-entrant only once: `enterDrawing()` repaints, the
|
|
1587
|
-
// repaint re-focuses the new canvas and fires this again, and by then
|
|
1588
|
-
// `cursor` is set.
|
|
1589
|
-
canvas.addEventListener('focus', function () {
|
|
1590
|
-
if (cursor === null) enterDrawing();
|
|
1591
|
-
});
|
|
1592
|
-
canvasWrap.appendChild(canvas);
|
|
1593
|
-
if (hadFocus) canvas.focus();
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
function drawLane(svg, layout, i) {
|
|
1597
|
-
const row = layout.lanes[i];
|
|
1598
|
-
const lane = row.lane;
|
|
1599
|
-
// An ABSENT `wave` and an EMPTY one are two different pictures, and
|
|
1600
|
-
// collapsing them into one was the regression the previous round shipped
|
|
1601
|
-
// on the very axis it was fixing. MEASURED against the pinned engine,
|
|
1602
|
-
// second half-brick per cycle:
|
|
1603
|
-
//
|
|
1604
|
-
// {name:'a', wave:''} ["xxx","xxx"] ONE x cycle
|
|
1605
|
-
// {name:'a'} [] NOTHING
|
|
1606
|
-
// {} (spacer) [] NOTHING
|
|
1607
|
-
// {name:'a', wave:1234} [] NOTHING
|
|
1608
|
-
// {name:'a', wave:null} [] NOTHING
|
|
1609
|
-
//
|
|
1610
|
-
// and in a mixed diagram the engine simply emits no bricks for that row:
|
|
1611
|
-
// `{signal:[{a,'0101'},{},{b,'1010'}]}` is 16 uses, i.e. 2 lanes × 4 × 2.
|
|
1612
|
-
// So the rule is exactly "a lane draws cycles only when `wave` is a
|
|
1613
|
-
// string"; `''` is a string, which is why it gets its one cycle, and
|
|
1614
|
-
// `{}` — the canonical WaveDrom blank-row spacer — gets none.
|
|
1615
|
-
//
|
|
1616
|
-
// The empty-string case is not a corner either: `deleteCycles` covering
|
|
1617
|
-
// every cycle sets every lane's wave to `''`, two clicks away.
|
|
1618
|
-
const hasWave = typeof lane.wave === 'string';
|
|
1619
|
-
const wave = hasWave ? lane.wave : '';
|
|
1620
|
-
const cells = hasWave ? codec.expandWave(wave) : [];
|
|
1621
|
-
const levels = !hasWave ? [] : (wave === '' ? ['x'] : codec.levelsOf(cells));
|
|
1622
|
-
const bricks = levels.map(brickOf);
|
|
1623
|
-
// Which cycles carry the engine's discontinuity marker. `levelsOf`
|
|
1624
|
-
// deliberately erases `|` — it answers what a cycle SHOWS, and a gap
|
|
1625
|
-
// shows whatever is in front of it — so the marker has to be read off the
|
|
1626
|
-
// cells. Measured: `01|10` renders one `<use xlink:href="#gap">` at
|
|
1627
|
-
// translate(100), which is the centre of cycle 2, the `|` cycle.
|
|
1628
|
-
const gaps = [];
|
|
1629
|
-
for (let c = 0; c < cells.length; c++) {
|
|
1630
|
-
if (cells[c].held !== true && cells[c].ch === '|') gaps.push(c);
|
|
1631
|
-
}
|
|
1632
|
-
// `data-wave-<i>` cannot carry the difference between "no `wave` key" and
|
|
1633
|
-
// `wave: ''` — both are the empty string — and that difference is exactly
|
|
1634
|
-
// what the engine draws differently, so it gets its own attribute.
|
|
1635
|
-
svg.setAttribute('data-haswave-' + i, hasWave ? '1' : '0');
|
|
1636
|
-
svg.setAttribute('data-wave-' + i, wave);
|
|
1637
|
-
svg.setAttribute('data-levels-' + i, levels.join(''));
|
|
1638
|
-
svg.setAttribute('data-bricks-' + i, bricks.join(' '));
|
|
1639
|
-
svg.setAttribute('data-gaps-' + i, gaps.join(' '));
|
|
1640
|
-
|
|
1641
|
-
const hi = row.y + 7;
|
|
1642
|
-
const lo = row.y + row.height - 9;
|
|
1643
|
-
const mid = (hi + lo) / 2;
|
|
1644
|
-
const cw = layout.cycleWidth;
|
|
1645
|
-
const labels = labelsOf(lane);
|
|
1646
|
-
// Which cycles carry a `data` label: the explicit value cycles, in order.
|
|
1647
|
-
// `levelsOf` repeats a bus's level through its continuation cycles, so the
|
|
1648
|
-
// label belongs to the cycle that STARTS the run, which is what the run
|
|
1649
|
-
// grouping below already knows.
|
|
1650
|
-
let labelSlot = 0;
|
|
1651
|
-
|
|
1652
|
-
// Runs of one picture. Clock bricks never merge — each `p` cycle is a
|
|
1653
|
-
// whole clock period, and a merged run would draw one long one.
|
|
1654
|
-
const runs = [];
|
|
1655
|
-
for (let c = 0; c < bricks.length; c++) {
|
|
1656
|
-
const last = runs.length === 0 ? null : runs[runs.length - 1];
|
|
1657
|
-
if (last !== null && last.brick === bricks[c] && !isClock(bricks[c])) last.to = c;
|
|
1658
|
-
else runs.push({ brick: bricks[c], from: c, to: c });
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
function bandOf(brick) {
|
|
1662
|
-
if (brick === '111' || brick === 'uuu') return [hi, hi];
|
|
1663
|
-
if (brick === '000' || brick === 'ddd') return [lo, lo];
|
|
1664
|
-
if (brick === 'zzz') return [mid, mid];
|
|
1665
|
-
return [hi, lo];
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
for (let r = 0; r < runs.length; r++) {
|
|
1669
|
-
const run = runs[r];
|
|
1670
|
-
const x0 = run.from * cw;
|
|
1671
|
-
const x1 = (run.to + 1) * cw;
|
|
1672
|
-
const brick = run.brick;
|
|
1673
|
-
if (isBus(brick)) {
|
|
1674
|
-
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
1675
|
-
const s = Math.min(6, cw / 4);
|
|
1676
|
-
poly.setAttribute('class', 'ed-wave-bus ed-wave-bus-' + brick.slice(4));
|
|
1677
|
-
poly.setAttribute('points',
|
|
1678
|
-
(x0 + s) + ',' + hi + ' ' + (x1 - s) + ',' + hi + ' ' +
|
|
1679
|
-
x1 + ',' + mid + ' ' + (x1 - s) + ',' + lo + ' ' +
|
|
1680
|
-
(x0 + s) + ',' + lo + ' ' + x0 + ',' + mid);
|
|
1681
|
-
svg.appendChild(poly);
|
|
1682
|
-
const text = labels[labelSlot];
|
|
1683
|
-
if (typeof text === 'string' && text !== '') {
|
|
1684
|
-
const t = d.createElementNS(SVGNS, 'text');
|
|
1685
|
-
t.setAttribute('class', 'ed-wave-buslabel');
|
|
1686
|
-
t.setAttribute('x', String((x0 + x1) / 2));
|
|
1687
|
-
t.setAttribute('y', String(mid + 4));
|
|
1688
|
-
t.setAttribute('text-anchor', 'middle');
|
|
1689
|
-
t.textContent = text;
|
|
1690
|
-
svg.appendChild(t);
|
|
1691
|
-
}
|
|
1692
|
-
labelSlot++;
|
|
1693
|
-
} else if (brick === 'xxx') {
|
|
1694
|
-
const rect = d.createElementNS(SVGNS, 'rect');
|
|
1695
|
-
rect.setAttribute('class', 'ed-wave-x');
|
|
1696
|
-
rect.setAttribute('x', String(x0));
|
|
1697
|
-
rect.setAttribute('y', String(hi));
|
|
1698
|
-
rect.setAttribute('width', String(x1 - x0));
|
|
1699
|
-
rect.setAttribute('height', String(lo - hi));
|
|
1700
|
-
svg.appendChild(rect);
|
|
1701
|
-
} else if (isClock(brick)) {
|
|
1702
|
-
const xm = (x0 + x1) / 2;
|
|
1703
|
-
const p = d.createElementNS(SVGNS, 'path');
|
|
1704
|
-
p.setAttribute('class', 'ed-wave-line ed-wave-clock');
|
|
1705
|
-
p.setAttribute('d', brick === 'nclk'
|
|
1706
|
-
? 'M' + x0 + ',' + lo + ' L' + x0 + ',' + hi + ' L' + xm + ',' + hi +
|
|
1707
|
-
' L' + xm + ',' + lo + ' L' + x1 + ',' + lo
|
|
1708
|
-
: 'M' + x0 + ',' + hi + ' L' + x0 + ',' + lo + ' L' + xm + ',' + lo +
|
|
1709
|
-
' L' + xm + ',' + hi + ' L' + x1 + ',' + hi);
|
|
1710
|
-
svg.appendChild(p);
|
|
1711
|
-
} else {
|
|
1712
|
-
const y = bandOf(brick)[0];
|
|
1713
|
-
const line = d.createElementNS(SVGNS, 'line');
|
|
1714
|
-
line.setAttribute('class', brick === 'uuu' || brick === 'ddd'
|
|
1715
|
-
? 'ed-wave-line ed-wave-weak' : 'ed-wave-line');
|
|
1716
|
-
line.setAttribute('x1', String(x0));
|
|
1717
|
-
line.setAttribute('x2', String(x1));
|
|
1718
|
-
line.setAttribute('y1', String(y));
|
|
1719
|
-
line.setAttribute('y2', String(y));
|
|
1720
|
-
svg.appendChild(line);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// The edge into this run. Clock bricks draw their own.
|
|
1724
|
-
if (r > 0 && !isClock(brick) && !isClock(runs[r - 1].brick)) {
|
|
1725
|
-
const a = bandOf(runs[r - 1].brick);
|
|
1726
|
-
const b = bandOf(brick);
|
|
1727
|
-
const top = Math.min(a[0], a[1], b[0], b[1]);
|
|
1728
|
-
const bot = Math.max(a[0], a[1], b[0], b[1]);
|
|
1729
|
-
if (bot > top) {
|
|
1730
|
-
const edge = d.createElementNS(SVGNS, 'line');
|
|
1731
|
-
edge.setAttribute('class', 'ed-wave-edge');
|
|
1732
|
-
edge.setAttribute('x1', String(x0));
|
|
1733
|
-
edge.setAttribute('x2', String(x0));
|
|
1734
|
-
edge.setAttribute('y1', String(top));
|
|
1735
|
-
edge.setAttribute('y2', String(bot));
|
|
1736
|
-
svg.appendChild(edge);
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
// The gap marker, last so it sits over the trace it interrupts. The
|
|
1742
|
-
// engine draws one per `|`; drawing none made the two pictures disagree
|
|
1743
|
-
// on a lane whose levels agreed exactly, which is the quietest form of
|
|
1744
|
-
// the disagreement this overlay exists to surface.
|
|
1745
|
-
for (const c of gaps) {
|
|
1746
|
-
const xm = (c + 0.5) * cw;
|
|
1747
|
-
const mark = d.createElementNS(SVGNS, 'path');
|
|
1748
|
-
mark.setAttribute('class', 'ed-wave-gap');
|
|
1749
|
-
mark.setAttribute('d',
|
|
1750
|
-
'M' + (xm - 5) + ',' + (lo + 3) + ' L' + (xm + 1) + ',' + (mid) +
|
|
1751
|
-
' L' + (xm - 5) + ',' + (hi - 3) +
|
|
1752
|
-
' M' + (xm - 1) + ',' + (lo + 3) + ' L' + (xm + 5) + ',' + (mid) +
|
|
1753
|
-
' L' + (xm - 1) + ',' + (hi - 3));
|
|
1754
|
-
svg.appendChild(mark);
|
|
2760
|
+
function resolveBusTarget(lane, cycle) {
|
|
2761
|
+
const slot = codec.dataSlotOf(lane, cycle);
|
|
2762
|
+
if (slot !== null) return { cycle: cycle, slot: slot };
|
|
2763
|
+
const levels = codec.levelsOf(typeof lane.wave === 'string' ? lane.wave : '');
|
|
2764
|
+
if (!drawer.isBus(drawer.brickOf(levels[cycle]))) return null;
|
|
2765
|
+
for (let i = cycle - 1; i >= 0 && levels[i] === levels[cycle]; i--) {
|
|
2766
|
+
const s = codec.dataSlotOf(lane, i);
|
|
2767
|
+
if (s !== null) return { cycle: i, slot: s };
|
|
1755
2768
|
}
|
|
2769
|
+
return 'blocked';
|
|
1756
2770
|
}
|
|
1757
2771
|
|
|
1758
2772
|
// ── the keyboard on the drawing ───────────────────────────────────────
|
|
@@ -1776,6 +2790,17 @@
|
|
|
1776
2790
|
* brush is on. Everything else the dialog can do is a real `<button>` or
|
|
1777
2791
|
* `<input>` that Tab already reaches.
|
|
1778
2792
|
*
|
|
2793
|
+
* That sentence has NO exceptions and no mode-dependence, which is a
|
|
2794
|
+
* property v3.6.0 Task 16 briefly broke and fix round 3 restored. Round 0
|
|
2795
|
+
* bound the transition jump over plain ←/→ while the 關聯線 gesture was
|
|
2796
|
+
* armed, so the keymap above became false for two keys at a state this
|
|
2797
|
+
* function itself decides — and, worse than the documentation problem,
|
|
2798
|
+
* several cells stopped being reachable from the keyboard at all (T9b).
|
|
2799
|
+
* Round 3 moved that jump to Alt+←/→, which cannot arrive here: the call
|
|
2800
|
+
* site is gated on `!ev.altKey`. So every Alt chord this dialog has —
|
|
2801
|
+
* Alt+↑/↓ for the lane, Alt+←/→ for the jump — is claimed one level up in
|
|
2802
|
+
* `onKeyDown` and is deliberately NOT part of this keymap.
|
|
2803
|
+
*
|
|
1779
2804
|
* Two targets are accepted, and only two:
|
|
1780
2805
|
* the canvas — the surface itself, `tabindex="0"`, so it is in the Tab
|
|
1781
2806
|
* cycle and in the focus trap like every other control.
|
|
@@ -1801,20 +2826,97 @@
|
|
|
1801
2826
|
enterDrawing();
|
|
1802
2827
|
return true;
|
|
1803
2828
|
}
|
|
2829
|
+
// v3.6.0 Task 16 fix round 3: THE ARROWS ARE PER-CELL IN EVERY STATE,
|
|
2830
|
+
// and the transition jump no longer lives here.
|
|
2831
|
+
//
|
|
2832
|
+
// Round 0 bound the jump over plain ← / → while armed. A jump can only
|
|
2833
|
+
// ever land on a transition, so that removed the keyboard's only
|
|
2834
|
+
// cell-precise positioning on the one surface that needs it: on
|
|
2835
|
+
// `p....` (transitions `[0]`) cell 1 became unreachable, and on
|
|
2836
|
+
// `0.1.0` (`[0,2,4]`) cells 1 and 3 did. Home/End do not rescue it —
|
|
2837
|
+
// they reach the two ends and nothing in between. T9b in
|
|
2838
|
+
// `test/editor-journey.test.js` is the invariant that caught it: the
|
|
2839
|
+
// keyboard's Enter/Enter route must write a BYTE-IDENTICAL source to
|
|
2840
|
+
// the mouse drag, and the mouse aims at exactly those cells. MEASURED
|
|
2841
|
+
// with the jump bound here — mouse `node: '.a'` / `node: '...b'`
|
|
2842
|
+
// against keyboard `node: 'a'` / `node: '....b'`: different cells,
|
|
2843
|
+
// different file, invariant gone.
|
|
2844
|
+
//
|
|
2845
|
+
// The jump is a convenience; per-cell movement is the primitive
|
|
2846
|
+
// underneath it, and a convenience may not be bound over a primitive
|
|
2847
|
+
// when that leaves no way back to it. So the jump moved to Alt+←/→,
|
|
2848
|
+
// claimed in `onKeyDown` beside Alt+↑/↓ — see that branch for why that
|
|
2849
|
+
// is the only home available to it (this function cannot be reached
|
|
2850
|
+
// with Alt held) and for what Alt means across the four arrows.
|
|
1804
2851
|
if (nav) {
|
|
1805
2852
|
ev.preventDefault();
|
|
1806
2853
|
ev.stopPropagation();
|
|
1807
2854
|
moveCursor(key, ev.shiftKey === true);
|
|
1808
2855
|
return true;
|
|
1809
2856
|
}
|
|
2857
|
+
// v3.5.0 Task 7: the keyboard's two-step version of the drag, ahead of
|
|
2858
|
+
// the plain Enter-paints case below so an armed Enter never falls
|
|
2859
|
+
// through to it. First Enter marks `cursor` as `pendingFrom` (arrows
|
|
2860
|
+
// above already moved it, exactly as a mouse press would have);
|
|
2861
|
+
// second Enter — from wherever the cursor walked to since — is the
|
|
2862
|
+
// SAME `finishEdgeDrag` the mouse's `onCanvasUp` calls, so the three
|
|
2863
|
+
// no-op drops and the one-armed-if-nothing-happened rule are decided
|
|
2864
|
+
// in that one place, not re-decided here.
|
|
2865
|
+
if (edgeMode === 'armed' && key === 'Enter' && cursor !== null) {
|
|
2866
|
+
ev.preventDefault();
|
|
2867
|
+
ev.stopPropagation();
|
|
2868
|
+
if (pendingFrom === null) {
|
|
2869
|
+
pendingFrom = { at: cursor.laneIndex, cell: cursor.cycle };
|
|
2870
|
+
panels.say('起點已標記,移到終點再按一次 Enter');
|
|
2871
|
+
render();
|
|
2872
|
+
} else {
|
|
2873
|
+
finishEdgeDrag(cursor.laneIndex, cursor.cycle);
|
|
2874
|
+
}
|
|
2875
|
+
return true;
|
|
2876
|
+
}
|
|
2877
|
+
// v3.5.0 Task 11: Enter on a cell inside a bus box opens that box's
|
|
2878
|
+
// label field instead of painting over it with the brush — the
|
|
2879
|
+
// keyboard's own version of the pointer's click below in
|
|
2880
|
+
// `onCanvasDown`. `resolveBusTarget` is the SAME resolution
|
|
2881
|
+
// `onCanvasDown` uses (fix round 1: a cursor resting on a `.`/`|`
|
|
2882
|
+
// continuation inside a run used to fall straight through to
|
|
2883
|
+
// `paintAtCursor()` below with nothing catching it — the identical
|
|
2884
|
+
// silent-corruption hazard the pointer path had, closed the same way
|
|
2885
|
+
// here rather than only on the mouse), so the two input devices can
|
|
2886
|
+
// never disagree about which cells are labellable or where a press
|
|
2887
|
+
// on a continuation actually lands. Placed AFTER the armed-edge Enter
|
|
2888
|
+
// above (arming an edge outranks opening a label field — the same
|
|
2889
|
+
// press cannot mean both) and BEFORE the generic Enter-paints case
|
|
2890
|
+
// below, which it must pre-empt.
|
|
2891
|
+
if (key === 'Enter' && cursor !== null) {
|
|
2892
|
+
const dlLayout = geometry.layoutOf(store.doc, SIZES);
|
|
2893
|
+
const dlRow = dlLayout.lanes[cursor.laneIndex];
|
|
2894
|
+
const dlLane = dlRow === undefined ? null : dlRow.lane;
|
|
2895
|
+
if (dlLane !== null) {
|
|
2896
|
+
const dlTarget = resolveBusTarget(dlLane, cursor.cycle);
|
|
2897
|
+
if (dlTarget === 'blocked') {
|
|
2898
|
+
ev.preventDefault();
|
|
2899
|
+
ev.stopPropagation();
|
|
2900
|
+
panels.say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
2901
|
+
return true;
|
|
2902
|
+
}
|
|
2903
|
+
if (dlTarget !== null) {
|
|
2904
|
+
ev.preventDefault();
|
|
2905
|
+
ev.stopPropagation();
|
|
2906
|
+
cursor = { laneIndex: cursor.laneIndex, cycle: dlTarget.cycle };
|
|
2907
|
+
openDataEdit(cursor.laneIndex, dlTarget.cycle);
|
|
2908
|
+
return true;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
1810
2912
|
// A brush character both PICKS the brush and paints with it. Picking
|
|
1811
2913
|
// without painting would need a second keystroke to do the thing the
|
|
1812
2914
|
// user already said, and the toolbar's own brush buttons are still there
|
|
1813
|
-
// for「選起來待會再用」. `
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
if (
|
|
2915
|
+
// for「選起來待會再用」. `isBrushKey` is the admission rule — see its
|
|
2916
|
+
// own comment (this file's top) for why it is not keyed on
|
|
2917
|
+
// `ev.shiftKey` (v3.5.0 Task 10 fix round 1: that used to block
|
|
2918
|
+
// `P`/`N`/`|`, all typed with Shift, from the keyboard entirely).
|
|
2919
|
+
if (isBrushKey(key)) {
|
|
1818
2920
|
ev.preventDefault();
|
|
1819
2921
|
ev.stopPropagation();
|
|
1820
2922
|
brush = key;
|
|
@@ -1859,9 +2961,16 @@
|
|
|
1859
2961
|
* ONE lane, a run of cycles — is exactly what a cursor plus Shift can
|
|
1860
2962
|
* describe. A pointer press already collapses it to the pressed cell; a
|
|
1861
2963
|
* plain arrow does the same, and Shift+←/→ keeps the anchor it was made
|
|
1862
|
-
* with. Shift+↑/↓ collapses
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
2964
|
+
* with. Shift+↑/↓ collapses the CYCLE range onto the lane it lands on.
|
|
2965
|
+
*
|
|
2966
|
+
* v3.6.0 Task 14 narrowed what that sentence claims. It used to say a
|
|
2967
|
+
* two-lane selection was not a thing this document model had; that is
|
|
2968
|
+
* no longer true — `selection.laneIndex`..`laneTo` is a lane range, and
|
|
2969
|
+
* 建立群組 / 訊號's 刪除 act on it. What is still true is that the
|
|
2970
|
+
* cycle range belongs to ONE lane (`laneIndex`), so an arrow key that
|
|
2971
|
+
* changes lanes has no cycle anchor left to extend from. Widening the
|
|
2972
|
+
* LANE range from the keyboard is a gesture this dialog does not have
|
|
2973
|
+
* yet; the rail's Shift+click is the one that makes it.
|
|
1865
2974
|
*/
|
|
1866
2975
|
function moveCursor(key, extend) {
|
|
1867
2976
|
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
@@ -1880,15 +2989,103 @@
|
|
|
1880
2989
|
const sameLane = lane === cursor.laneIndex;
|
|
1881
2990
|
cursor = { laneIndex: lane, cycle: cyc };
|
|
1882
2991
|
if (extend && sameLane && selection !== null && selection.laneIndex === lane) {
|
|
1883
|
-
selection = { laneIndex: lane, from: selection.from, to: cyc };
|
|
2992
|
+
selection = { laneIndex: lane, laneTo: lane, from: selection.from, to: cyc };
|
|
1884
2993
|
} else {
|
|
1885
|
-
selection = { laneIndex: lane, from: cyc, to: cyc };
|
|
2994
|
+
selection = { laneIndex: lane, laneTo: lane, from: cyc, to: cyc };
|
|
1886
2995
|
}
|
|
1887
2996
|
repaintDrawing(layout);
|
|
1888
2997
|
scrollCursorIntoView(layout);
|
|
1889
2998
|
sayCursor(layout);
|
|
1890
2999
|
}
|
|
1891
3000
|
|
|
3001
|
+
/**
|
|
3002
|
+
* v3.6.0 Task 16: while armed, walk the cursor between this lane's
|
|
3003
|
+
* TRANSITIONS — `dir === 1` rightwards, `dir === -1` leftwards. Reached
|
|
3004
|
+
* by **Alt+←/→**, claimed in `onKeyDown`; fix round 3 moved it off plain
|
|
3005
|
+
* ←/→, which it had been bound over.
|
|
3006
|
+
*
|
|
3007
|
+
* What Alt+← / Alt+→ is for is picking the cell an edge will anchor to,
|
|
3008
|
+
* and an edge anchors at a transition. Walking one cycle at a time makes
|
|
3009
|
+
* a user press the key eight times to cross an eight-cycle flat run and
|
|
3010
|
+
* land on seven cells they cannot usefully aim at.
|
|
3011
|
+
*
|
|
3012
|
+
* It is an ADDITION to per-cell movement, never a replacement for it —
|
|
3013
|
+
* that distinction is the whole of fix round 3. Plain ←/→ still walks one
|
|
3014
|
+
* cell in every state, armed included, because a jump can only land on a
|
|
3015
|
+
* transition and any cell that is not one would otherwise have no
|
|
3016
|
+
* keyboard route at all. The POINTER could always drop an endpoint
|
|
3017
|
+
* anywhere along a flat run (hanging a label in the middle of a bus needs
|
|
3018
|
+
* exactly that); after round 3 the keyboard can too, which is what T9b
|
|
3019
|
+
* pins byte for byte.
|
|
3020
|
+
*
|
|
3021
|
+
* Which cells count is `geometry.transitionsOf`'s answer, the SAME one
|
|
3022
|
+
* `wave-draw.js` paints the dots from, so a key can never stop somewhere
|
|
3023
|
+
* the drawing did not put a dot. It is not re-derived here, and it is not
|
|
3024
|
+
* "a cycle whose level differs from the one before" — see that function's
|
|
3025
|
+
* own docstring for why that is a different (and wrong) rule.
|
|
3026
|
+
*
|
|
3027
|
+
* THE TWO ENDS CLAMP, and that has one consequence worth stating because
|
|
3028
|
+
* it looks backwards for one press: the armed cursor only ever rests on a
|
|
3029
|
+
* transition, so → with nothing further right goes to the RIGHTMOST
|
|
3030
|
+
* transition and ← with nothing further left goes to the leftmost. When
|
|
3031
|
+
* the cursor was parked to the right of every transition — arming does
|
|
3032
|
+
* not move it, so that is reachable in one step — the first → therefore
|
|
3033
|
+
* snaps LEFT, onto the rightmost anchor. Every press after that is a
|
|
3034
|
+
* no-op at the same cell. Pinned both ways in `test/editor-journey.test.js`.
|
|
3035
|
+
*
|
|
3036
|
+
* `selection` is collapsed onto the landing cell exactly as `moveCursor`'s
|
|
3037
|
+
* own non-extend branch does it: `paintAtCursor` paints `selection`'s run
|
|
3038
|
+
* whenever it belongs to the cursor's lane, so a jump that left a stale
|
|
3039
|
+
* run standing would make the next brush key paint a stretch of cycles
|
|
3040
|
+
* the cursor is no longer anywhere near. Alt+Shift+←/→ does NOT extend
|
|
3041
|
+
* the selection — an edge endpoint is one cell, so a jump has no range to
|
|
3042
|
+
* grow. Plain Shift+←/→ still extends, exactly as it always did; round 3
|
|
3043
|
+
* gave that back by taking the jump off the unmodified arrows.
|
|
3044
|
+
*
|
|
3045
|
+
* Ends in the same three calls `moveCursor` does, for the same reason: no
|
|
3046
|
+
* document content changed, so this is a repaint of the drawing and the
|
|
3047
|
+
* rail, not a `render()` that would also rebuild the preview to paint the
|
|
3048
|
+
* identical picture.
|
|
3049
|
+
*/
|
|
3050
|
+
function jumpTransition(dir) {
|
|
3051
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3052
|
+
if (layout.lanes.length === 0 || layout.cycles === 0) return;
|
|
3053
|
+
// Arriving is not choosing — the same rule `moveCursor` and
|
|
3054
|
+
// `enterDrawing` already state. A first keystroke that both entered
|
|
3055
|
+
// the drawing and jumped somewhere would never show the user which
|
|
3056
|
+
// cell it started counting from.
|
|
3057
|
+
if (cursor === null) { enterDrawing(); return; }
|
|
3058
|
+
const lane = cursor.laneIndex;
|
|
3059
|
+
const cells = geometry.transitionsOf(store.doc, lane);
|
|
3060
|
+
// A lane with nothing to anchor to: WaveDrom's `{}` spacer is the
|
|
3061
|
+
// fixture case, and a lane whose wave is all continuations is the
|
|
3062
|
+
// other. Neither is an error, and neither may move the cursor to a
|
|
3063
|
+
// cell this rule says it is not allowed to rest on.
|
|
3064
|
+
if (cells.length === 0) {
|
|
3065
|
+
panels.say('這條 lane 上沒有轉態點可以停,關聯線接不上去');
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const now = cursor.cycle;
|
|
3069
|
+
let next;
|
|
3070
|
+
if (dir > 0) {
|
|
3071
|
+
next = cells.find(function (c) { return c > now; });
|
|
3072
|
+
if (next === undefined) next = cells[cells.length - 1];
|
|
3073
|
+
} else {
|
|
3074
|
+
const before = cells.filter(function (c) { return c < now; });
|
|
3075
|
+
next = before.length === 0 ? cells[0] : before[before.length - 1];
|
|
3076
|
+
}
|
|
3077
|
+
cursor = { laneIndex: lane, cycle: next };
|
|
3078
|
+
selection = { laneIndex: lane, laneTo: lane, from: next, to: next };
|
|
3079
|
+
// `cell`, not `cycle`: `hoverBoundary` carries `geometry.boundaryAt`'s
|
|
3080
|
+
// own field names all the way to `drawTransitions`, which compares
|
|
3081
|
+
// `.cell` — the two spellings are deliberately different and a mix-up
|
|
3082
|
+
// fails silently there, as that function's own comment says.
|
|
3083
|
+
hoverBoundary = { laneIndex: lane, cell: next };
|
|
3084
|
+
repaintDrawing(layout);
|
|
3085
|
+
scrollCursorIntoView(layout);
|
|
3086
|
+
sayCursor(layout);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
1892
3089
|
/** Paint the cursor's run with the current brush: one gesture, one
|
|
1893
3090
|
* `commit`, exactly like the pointer's `mouseup`. The range comes from
|
|
1894
3091
|
* `selection` when it belongs to the cursor's own lane — which every
|
|
@@ -1930,7 +3127,7 @@
|
|
|
1930
3127
|
const to = selection === null ? cursor.cycle : Math.max(selection.from, selection.to);
|
|
1931
3128
|
const span = (selection !== null && selection.laneIndex === cursor.laneIndex && to > from)
|
|
1932
3129
|
? ',選了 cycle ' + (from + 1) + '–' + (to + 1) : '';
|
|
1933
|
-
say('游標:' + name + ' cycle ' + (cursor.cycle + 1) + span);
|
|
3130
|
+
panels.say('游標:' + name + ' cycle ' + (cursor.cycle + 1) + span);
|
|
1934
3131
|
}
|
|
1935
3132
|
|
|
1936
3133
|
/** The lane list and the drawing, repainted together and without touching
|
|
@@ -1941,8 +3138,29 @@
|
|
|
1941
3138
|
function repaintDrawing(layout) {
|
|
1942
3139
|
const l = layout === undefined || layout === null
|
|
1943
3140
|
? geometry.layoutOf(store.doc, SIZES) : layout;
|
|
1944
|
-
renderLanes(store.doc, l);
|
|
1945
|
-
|
|
3141
|
+
panels.renderLanes(store.doc, l, { selection: selection });
|
|
3142
|
+
const wds = {
|
|
3143
|
+
cursor: cursor,
|
|
3144
|
+
selection: selection,
|
|
3145
|
+
pendingFrom: pendingFrom,
|
|
3146
|
+
endpointDrag: endpointDrag,
|
|
3147
|
+
selectedEdge: selectedEdge,
|
|
3148
|
+
overlay: overlay,
|
|
3149
|
+
canvasWrap: canvasWrap,
|
|
3150
|
+
dataEdit: dataEdit,
|
|
3151
|
+
enterDrawing: enterDrawing,
|
|
3152
|
+
onCanvasDown: onCanvasDown,
|
|
3153
|
+
onCanvasHover: onCanvasHover,
|
|
3154
|
+
edgeMode: edgeMode,
|
|
3155
|
+
hoverBoundary: hoverBoundary,
|
|
3156
|
+
};
|
|
3157
|
+
canvas = drawer.renderCanvas(canvas, store.doc, l, wds);
|
|
3158
|
+
cursor = wds.cursor;
|
|
3159
|
+
selection = wds.selection;
|
|
3160
|
+
// AFTER the drawer, which is what clamps `selection` into the document
|
|
3161
|
+
// it just drew — reading it before would arm a button against a range
|
|
3162
|
+
// the drawing has already refused.
|
|
3163
|
+
paintSelectionState(store.doc, l);
|
|
1946
3164
|
}
|
|
1947
3165
|
|
|
1948
3166
|
/**
|
|
@@ -1990,11 +3208,262 @@
|
|
|
1990
3208
|
function onCanvasDown(ev) {
|
|
1991
3209
|
if (ev.button !== 0) return;
|
|
1992
3210
|
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3211
|
+
// v3.5.0 Task 7: armed edge-creation claims the press outright, before
|
|
3212
|
+
// the edge-select / deselect / paint dispatch below ever runs — the
|
|
3213
|
+
// whole point of arming is that the NEXT press starts the drag, so it
|
|
3214
|
+
// must not be shadowed by "did this land on an existing edge" or "did
|
|
3215
|
+
// this land on a cell to paint". A press that misses every lane
|
|
3216
|
+
// (`at === null`, e.g. the canvas's own padding) leaves the button
|
|
3217
|
+
// armed rather than eating it for nothing: missing the diagram is not
|
|
3218
|
+
// the same as aiming at the wrong cell, but the recovery is the same —
|
|
3219
|
+
// try the press again.
|
|
3220
|
+
if (edgeMode === 'armed') {
|
|
3221
|
+
const at = cellFromEvent(ev, layout);
|
|
3222
|
+
if (at === null) return;
|
|
3223
|
+
ev.preventDefault();
|
|
3224
|
+
pendingFrom = { at: at.laneIndex, cell: at.cycle };
|
|
3225
|
+
setEdgeMode('dragging');
|
|
3226
|
+
// The keyboard follows the pointer here for the same reason it does
|
|
3227
|
+
// in the paint path below: `cursor` is the ONE notion of "where is
|
|
3228
|
+
// the other end right now" that the mouse move handler, the preview
|
|
3229
|
+
// line and (on the keyboard's own path) `handleDrawingKey` all read,
|
|
3230
|
+
// so drag and keystroke can never disagree about the live endpoint.
|
|
3231
|
+
cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
3232
|
+
canvas.focus();
|
|
3233
|
+
render();
|
|
3234
|
+
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3235
|
+
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
// v3.5.0 Task 8: a press on the SELECTED edge's own handle starts an
|
|
3239
|
+
// endpoint drag, ahead of the plain select/deselect dispatch a few
|
|
3240
|
+
// lines down — that dispatch's own `edgeHitAt` would otherwise see the
|
|
3241
|
+
// exact same press and read it as "re-select the edge that is already
|
|
3242
|
+
// selected", which is a real answer but the wrong one for a press ON
|
|
3243
|
+
// its handle. Recomputed fresh here (`geometry.edgeLayout`, not a
|
|
3244
|
+
// cached list) for the same reason `cellFromEvent`/`edgeHitAt`
|
|
3245
|
+
// already are — a press can never be judged against a layout
|
|
3246
|
+
// `renderCanvas` has moved on from.
|
|
3247
|
+
//
|
|
3248
|
+
// Scoped to `handle.index === selectedEdge` on purpose:
|
|
3249
|
+
// `geometry.edgeHandleAt` answers for EVERY edge's endpoints
|
|
3250
|
+
// regardless of selection (see its own comment), but `renderEdges`
|
|
3251
|
+
// only ever DRAWS handles on the selected one — a press near a
|
|
3252
|
+
// DIFFERENT edge's endpoint has no handle on screen to have landed on,
|
|
3253
|
+
// and falls through to the ordinary select-by-path/point dispatch
|
|
3254
|
+
// below exactly as it always has.
|
|
3255
|
+
if (selectedEdge !== null) {
|
|
3256
|
+
const edges = geometry.edgeLayout(store.doc, layout);
|
|
3257
|
+
const box = canvas.getBoundingClientRect();
|
|
3258
|
+
const handle = geometry.edgeHandleAt(edges, ev.clientX - box.left, ev.clientY - box.top);
|
|
3259
|
+
if (handle !== null && handle.index === selectedEdge) {
|
|
3260
|
+
ev.preventDefault();
|
|
3261
|
+
// Fix round 1, item 1: a self-loop (`from === to`) puts both ends
|
|
3262
|
+
// at the exact same pixel, so `geometry.edgeHandleAt`'s own
|
|
3263
|
+
// `['to', 'from']` iteration always resolves the press to `'to'` —
|
|
3264
|
+
// there is no position this file (or the user) can use to tell
|
|
3265
|
+
// the two apart, and inferring one would just be a guess dressed
|
|
3266
|
+
// up as an answer. What was missing was not a fix, it was the
|
|
3267
|
+
// honest half: before this, the press silently moved `'to'` with
|
|
3268
|
+
// nothing on screen saying that is what happened, or that `'from'`
|
|
3269
|
+
// was never reachable at all.
|
|
3270
|
+
const dragged = edges.filter(function (e) { return e.index === handle.index; })[0];
|
|
3271
|
+
if (dragged !== undefined && dragged.edge.from === dragged.edge.to) {
|
|
3272
|
+
panels.say('這是一條 self-loop(起點跟終點在同一格),兩端的把手疊在同一個位置:' +
|
|
3273
|
+
'只有拖得到的這一端可以搬,另一端目前搬不到。');
|
|
3274
|
+
}
|
|
3275
|
+
endpointDrag = { index: handle.index, end: handle.end, at: null, cell: null };
|
|
3276
|
+
canvas.focus();
|
|
3277
|
+
// Fix round 1, item 2: without a repaint here the press looks
|
|
3278
|
+
// inert until the first `pointermove` — `.is-drag-source` (set
|
|
3279
|
+
// from this same `endpointDrag`, read by `renderEdges`) would not
|
|
3280
|
+
// fade the dragged edge until then, unlike the sibling
|
|
3281
|
+
// `edgeMode === 'armed'` branch above, which repaints immediately
|
|
3282
|
+
// on its own press. `renderCanvas`, not the full `render()`: this
|
|
3283
|
+
// state change touches only the canvas SVG (the faded path; the
|
|
3284
|
+
// dashed landing preview has nothing to draw yet since `at` is
|
|
3285
|
+
// still `null`) — `selectedEdge` is unchanged, so the edge
|
|
3286
|
+
// inspector row and the lane list are both already showing exactly
|
|
3287
|
+
// what they should, and paying for `renderLanes`/`renderEdgeBar`
|
|
3288
|
+
// again would be the same needless cost the existing
|
|
3289
|
+
// `hadSelectedEdge`-gated branch further down in this function
|
|
3290
|
+
// already avoids.
|
|
3291
|
+
const wds = {
|
|
3292
|
+
cursor: cursor,
|
|
3293
|
+
selection: selection,
|
|
3294
|
+
pendingFrom: pendingFrom,
|
|
3295
|
+
endpointDrag: endpointDrag,
|
|
3296
|
+
selectedEdge: selectedEdge,
|
|
3297
|
+
overlay: overlay,
|
|
3298
|
+
canvasWrap: canvasWrap,
|
|
3299
|
+
dataEdit: dataEdit,
|
|
3300
|
+
enterDrawing: enterDrawing,
|
|
3301
|
+
onCanvasDown: onCanvasDown,
|
|
3302
|
+
onCanvasHover: onCanvasHover,
|
|
3303
|
+
edgeMode: edgeMode,
|
|
3304
|
+
hoverBoundary: hoverBoundary,
|
|
3305
|
+
};
|
|
3306
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3307
|
+
cursor = wds.cursor;
|
|
3308
|
+
selection = wds.selection;
|
|
3309
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3310
|
+
paintSelectionState(store.doc, layout);
|
|
3311
|
+
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3312
|
+
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3313
|
+
return; // 不塗、不選格
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
// v3.5.0 Task 11 fix round 2 (widened in round 3): a press that lands
|
|
3317
|
+
// on the label's own mark — the `<text>` when the run has a label, or
|
|
3318
|
+
// (round 3) the small `<circle class="ed-wave-buslabel-empty">`
|
|
3319
|
+
// `drawLane` draws in its place when the run has none — opens the
|
|
3320
|
+
// data-label field instead of arming a paint drag, rather than merely
|
|
3321
|
+
// somewhere inside the box the mark sits in. Round 1 gated this on
|
|
3322
|
+
// the CELL (`codec.dataSlotOf`/`resolveBusTarget` alone, asked off
|
|
3323
|
+
// the pressed cycle with no regard to what was actually under the
|
|
3324
|
+
// pointer) and that was reviewer-verified to be its own regression:
|
|
3325
|
+
// T6b paints `N` onto a bus cell with the mouse, and painting a
|
|
3326
|
+
// bus cell is exactly what twenty-two brushes exist for — a gesture
|
|
3327
|
+
// that swallows every press anywhere in the box leaves the user no
|
|
3328
|
+
// pointer route to repaint one at all. The string is what a press on
|
|
3329
|
+
// its mark means to change, so the mark is the target; a press
|
|
3330
|
+
// anywhere else in the SAME box — the polygon, its bevelled ends, a
|
|
3331
|
+
// held continuation's own cell — falls straight through to the
|
|
3332
|
+
// ordinary paint dispatch below exactly as it always has, ROUND 1's
|
|
3333
|
+
// fix included: that fix's `resolveBusTarget` still runs, just gated
|
|
3334
|
+
// on the mark hit rather than on cell membership, so a press on the
|
|
3335
|
+
// label of a merged run still resolves to the run's owning explicit
|
|
3336
|
+
// cycle regardless of which cycle's pixels the mark happens to
|
|
3337
|
+
// straddle (see `resolveBusTarget`'s own comment — its answer is the
|
|
3338
|
+
// same whichever cycle a press on that shared mark geometrically
|
|
3339
|
+
// lands on). Both marks carry the SAME `ed-wave-buslabel` class —
|
|
3340
|
+
// round 3's placeholder ADDS `ed-wave-buslabel-empty` on top of it
|
|
3341
|
+
// rather than replacing it — so this check needed no change at all
|
|
3342
|
+
// to cover the empty case; it was already asking for the class, not
|
|
3343
|
+
// for an element type. `.ed-wave-buslabel`'s `pointer-events:
|
|
3344
|
+
// visiblePainted` (`lib/md2doc.js`) plus paint order — each mark is
|
|
3345
|
+
// appended AFTER its box's `<polygon>` in `drawLane`, so it sits on
|
|
3346
|
+
// top and wins the hit test in its own footprint — is what makes the
|
|
3347
|
+
// two targets disjoint instead of the mark being invisibly swallowed
|
|
3348
|
+
// by the polygon underneath it.
|
|
3349
|
+
//
|
|
3350
|
+
// final review finding 4: this block moved AHEAD of `edgeHitAt` below
|
|
3351
|
+
// (it used to run after it), and its condition now asks
|
|
3352
|
+
// `busLabelHitAt` instead of testing `ev.target` directly.
|
|
3353
|
+
//
|
|
3354
|
+
// Gating `edgeHandleAt` on `selectedEdge` (the change in `edgeHitAt`
|
|
3355
|
+
// itself, above) stops a PHANTOM, undrawn handle from claiming a press
|
|
3356
|
+
// by pure coordinate math — but MEASURED against a real page, that
|
|
3357
|
+
// alone does not make this block reachable: `renderEdges` draws
|
|
3358
|
+
// `.ed-wave-edge-hit` (a 10px-wide, invisible-but-hit-testable stroke,
|
|
3359
|
+
// `pointer-events: stroke`) AFTER every lane's own content, so for any
|
|
3360
|
+
// edge whose endpoint lands on a bus cell, that invisible stroke
|
|
3361
|
+
// paints ON TOP of the cell's `.ed-wave-buslabel` mark. `ev.target` —
|
|
3362
|
+
// the browser's own topmost-element answer — is `path.ed-wave-edge-
|
|
3363
|
+
// hit` at that pixel regardless of source order in THIS function:
|
|
3364
|
+
// moving this block ahead of `edgeHitAt` does not change what
|
|
3365
|
+
// `ev.target` IS, only when it gets asked. Reordering plus the
|
|
3366
|
+
// original `ev.target` check was tried first and measured to still
|
|
3367
|
+
// fail — `elementFromPoint` at the labelled cell's own centre resolved
|
|
3368
|
+
// to the hit path, not the label, exactly as before.
|
|
3369
|
+
//
|
|
3370
|
+
// `busLabelHitAt` answers a different question: not "what is the
|
|
3371
|
+
// TOPMOST element here" but "is there a bus-label mark ANYWHERE in the
|
|
3372
|
+
// stack of elements under this press", via `elementsFromPoint`
|
|
3373
|
+
// (plural) — the standard way to see past one hit-testable layer
|
|
3374
|
+
// sitting on top of another at the same point. That is a real,
|
|
3375
|
+
// measured difference in outcome, not a matter of code order.
|
|
3376
|
+
if (busLabelHitAt(ev) !== null) {
|
|
3377
|
+
const busAt = cellFromEvent(ev, layout);
|
|
3378
|
+
if (busAt !== null) {
|
|
3379
|
+
const busLane = layout.lanes[busAt.laneIndex].lane;
|
|
3380
|
+
const busTarget = resolveBusTarget(busLane, busAt.cycle);
|
|
3381
|
+
// `resolveBusTarget` should never actually answer `null` here —
|
|
3382
|
+
// the label only ever exists over a cycle the codec itself calls
|
|
3383
|
+
// a bus level — but the check is kept rather than assumed, same
|
|
3384
|
+
// reasoning as the `'blocked'` branch just below it.
|
|
3385
|
+
if (busTarget === 'blocked') {
|
|
3386
|
+
ev.preventDefault();
|
|
3387
|
+
panels.say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
if (busTarget !== null) {
|
|
3391
|
+
ev.preventDefault();
|
|
3392
|
+
const busHadSelectedEdge = selectedEdge !== null;
|
|
3393
|
+
selectedEdge = null;
|
|
3394
|
+
cursor = { laneIndex: busAt.laneIndex, cycle: busTarget.cycle };
|
|
3395
|
+
selection = {
|
|
3396
|
+
laneIndex: busAt.laneIndex, laneTo: busAt.laneIndex,
|
|
3397
|
+
from: busTarget.cycle, to: busTarget.cycle,
|
|
3398
|
+
};
|
|
3399
|
+
canvas.focus();
|
|
3400
|
+
if (busHadSelectedEdge) render();
|
|
3401
|
+
else {
|
|
3402
|
+
const wds = {
|
|
3403
|
+
cursor: cursor,
|
|
3404
|
+
selection: selection,
|
|
3405
|
+
pendingFrom: pendingFrom,
|
|
3406
|
+
endpointDrag: endpointDrag,
|
|
3407
|
+
selectedEdge: selectedEdge,
|
|
3408
|
+
overlay: overlay,
|
|
3409
|
+
canvasWrap: canvasWrap,
|
|
3410
|
+
dataEdit: dataEdit,
|
|
3411
|
+
enterDrawing: enterDrawing,
|
|
3412
|
+
onCanvasDown: onCanvasDown,
|
|
3413
|
+
onCanvasHover: onCanvasHover,
|
|
3414
|
+
edgeMode: edgeMode,
|
|
3415
|
+
hoverBoundary: hoverBoundary,
|
|
3416
|
+
};
|
|
3417
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3418
|
+
cursor = wds.cursor;
|
|
3419
|
+
selection = wds.selection;
|
|
3420
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3421
|
+
paintSelectionState(store.doc, layout);
|
|
3422
|
+
}
|
|
3423
|
+
openDataEdit(busAt.laneIndex, busTarget.cycle);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
// An edge — its endpoint handle first (it sits ON TOP of the cell it
|
|
3429
|
+
// anchors to, and a selected edge's handle must win the press even
|
|
3430
|
+
// though the cell underneath is perfectly paintable), then its path —
|
|
3431
|
+
// is asked BEFORE the cell/brush dispatch below (but AFTER the bus
|
|
3432
|
+
// label block above — see its own comment for why that one now goes
|
|
3433
|
+
// first). A hit selects and returns without arming `drag`: this
|
|
3434
|
+
// dispatch draws and selects edges; a press on the SELECTED edge's own
|
|
3435
|
+
// handle never reaches here at all (see the drag-start branch above),
|
|
3436
|
+
// so this remains a plain select — it does not itself drag anything.
|
|
3437
|
+
const edgeHit = edgeHitAt(ev, layout);
|
|
3438
|
+
if (edgeHit !== null) {
|
|
3439
|
+
ev.preventDefault();
|
|
3440
|
+
selectedEdge = edgeHit;
|
|
3441
|
+
canvas.focus();
|
|
3442
|
+
render();
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
// A press that hit neither an edge's handle nor its path deselects
|
|
3446
|
+
// whatever WAS selected. The inspector row is the only visible sign a
|
|
3447
|
+
// selection exists, and a selection that outlives a click elsewhere
|
|
3448
|
+
// would let a later 刪除這條線 act on an edge the user has stopped
|
|
3449
|
+
// looking at. This is IN ADDITION to the paint gesture below, on the
|
|
3450
|
+
// very same press — not instead of it.
|
|
3451
|
+
const hadSelectedEdge = selectedEdge !== null;
|
|
3452
|
+
selectedEdge = null;
|
|
1993
3453
|
const at = cellFromEvent(ev, layout);
|
|
1994
|
-
if (at === null)
|
|
3454
|
+
if (at === null) {
|
|
3455
|
+
// Missed the diagram entirely (e.g. the canvas's own padding). Only
|
|
3456
|
+
// the deselect, if there was one to make, needs a repaint; nothing
|
|
3457
|
+
// else here changed.
|
|
3458
|
+
if (hadSelectedEdge) render();
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
1995
3461
|
ev.preventDefault();
|
|
1996
3462
|
drag = { laneIndex: at.laneIndex, from: at.cycle, to: at.cycle };
|
|
1997
|
-
selection = {
|
|
3463
|
+
selection = {
|
|
3464
|
+
laneIndex: at.laneIndex, laneTo: at.laneIndex,
|
|
3465
|
+
from: at.cycle, to: at.cycle,
|
|
3466
|
+
};
|
|
1998
3467
|
// The keyboard follows the pointer, for two reasons that are really one:
|
|
1999
3468
|
// the two cursors may not disagree about which cell is current, and
|
|
2000
3469
|
// `preventDefault()` above suppresses the focus a press would otherwise
|
|
@@ -2002,12 +3471,131 @@
|
|
|
2002
3471
|
// then pressing `1` would paint wherever the keyboard had been left.
|
|
2003
3472
|
cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
2004
3473
|
canvas.focus();
|
|
2005
|
-
|
|
3474
|
+
// The common case — no edge was selected — keeps the cheap
|
|
3475
|
+
// `renderCanvas`-only repaint this always did. A full `render()` is
|
|
3476
|
+
// only paid on the transition that actually needs it: hiding the
|
|
3477
|
+
// inspector row, which lives in the toolbar `renderCanvas` never
|
|
3478
|
+
// touches.
|
|
3479
|
+
if (hadSelectedEdge) render();
|
|
3480
|
+
else {
|
|
3481
|
+
const wds = {
|
|
3482
|
+
cursor: cursor,
|
|
3483
|
+
selection: selection,
|
|
3484
|
+
pendingFrom: pendingFrom,
|
|
3485
|
+
endpointDrag: endpointDrag,
|
|
3486
|
+
selectedEdge: selectedEdge,
|
|
3487
|
+
overlay: overlay,
|
|
3488
|
+
canvasWrap: canvasWrap,
|
|
3489
|
+
dataEdit: dataEdit,
|
|
3490
|
+
enterDrawing: enterDrawing,
|
|
3491
|
+
onCanvasDown: onCanvasDown,
|
|
3492
|
+
onCanvasHover: onCanvasHover,
|
|
3493
|
+
edgeMode: edgeMode,
|
|
3494
|
+
hoverBoundary: hoverBoundary,
|
|
3495
|
+
};
|
|
3496
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3497
|
+
cursor = wds.cursor;
|
|
3498
|
+
selection = wds.selection;
|
|
3499
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3500
|
+
paintSelectionState(store.doc, layout);
|
|
3501
|
+
}
|
|
2006
3502
|
d.addEventListener('mousemove', onCanvasMove, true);
|
|
2007
3503
|
d.addEventListener('mouseup', onCanvasUp, true);
|
|
2008
3504
|
}
|
|
2009
3505
|
|
|
2010
3506
|
function onCanvasMove(ev) {
|
|
3507
|
+
if (endpointDrag !== null) {
|
|
3508
|
+
// Same abandoned-button recovery as the two drags below: the button
|
|
3509
|
+
// came up somewhere this page never heard about.
|
|
3510
|
+
if (ev.buttons === 0) { onCanvasUp(); return; }
|
|
3511
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3512
|
+
const at = cellFromEvent(ev, layout);
|
|
3513
|
+
// A move that strays off the diagram leaves `at`/`cell` exactly
|
|
3514
|
+
// where they last were, same recovery as `edgeMode === 'dragging'`
|
|
3515
|
+
// below and for the same reason: the drop target the user is
|
|
3516
|
+
// currently over is still the last cell the pointer was actually on
|
|
3517
|
+
// top of, not "nowhere".
|
|
3518
|
+
if (at !== null) {
|
|
3519
|
+
endpointDrag = { index: endpointDrag.index, end: endpointDrag.end,
|
|
3520
|
+
at: at.laneIndex, cell: at.cycle };
|
|
3521
|
+
}
|
|
3522
|
+
// `hoverBoundary` is left alone here: `drawTransitions` only ever
|
|
3523
|
+
// draws while `edgeMode !== 'idle'` (see its own guard, unchanged
|
|
3524
|
+
// by this task), and an endpoint drag never moves `edgeMode` away
|
|
3525
|
+
// from 'idle' (it is a wholly separate gesture from edge-creation —
|
|
3526
|
+
// see `endpointDrag`'s own declaration comment), so there is no dot
|
|
3527
|
+
// on screen for a hot mark to name here. `cellFromEvent` still
|
|
3528
|
+
// snaps this drag's OWN drop target to the nearest anchor via
|
|
3529
|
+
// `boundaryAt` — that part of Task 11 applies regardless — this is
|
|
3530
|
+
// only about the separate hover-dot chrome.
|
|
3531
|
+
// The document is untouched during the drag (rule 3): this repaints
|
|
3532
|
+
// the preview only, never a `commit`.
|
|
3533
|
+
const wds = {
|
|
3534
|
+
cursor: cursor,
|
|
3535
|
+
selection: selection,
|
|
3536
|
+
pendingFrom: pendingFrom,
|
|
3537
|
+
endpointDrag: endpointDrag,
|
|
3538
|
+
selectedEdge: selectedEdge,
|
|
3539
|
+
overlay: overlay,
|
|
3540
|
+
canvasWrap: canvasWrap,
|
|
3541
|
+
dataEdit: dataEdit,
|
|
3542
|
+
enterDrawing: enterDrawing,
|
|
3543
|
+
onCanvasDown: onCanvasDown,
|
|
3544
|
+
onCanvasHover: onCanvasHover,
|
|
3545
|
+
edgeMode: edgeMode,
|
|
3546
|
+
hoverBoundary: hoverBoundary,
|
|
3547
|
+
};
|
|
3548
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3549
|
+
cursor = wds.cursor;
|
|
3550
|
+
selection = wds.selection;
|
|
3551
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3552
|
+
paintSelectionState(store.doc, layout);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
if (edgeMode === 'dragging') {
|
|
3556
|
+
// Same abandoned-button recovery as the paint drag below: the button
|
|
3557
|
+
// came up somewhere this page never heard about.
|
|
3558
|
+
if (ev.buttons === 0) { onCanvasUp(); return; }
|
|
3559
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3560
|
+
const at = cellFromEvent(ev, layout);
|
|
3561
|
+
// A move that strays off the diagram leaves `cursor` — and so the
|
|
3562
|
+
// preview line — exactly where it last was, rather than snapping the
|
|
3563
|
+
// line away: the drop target the user is currently over is still the
|
|
3564
|
+
// last cell the pointer was actually on top of.
|
|
3565
|
+
if (at !== null) {
|
|
3566
|
+
cursor = { laneIndex: at.laneIndex, cycle: at.cycle };
|
|
3567
|
+
// v3.6.0 Task 11: the hot dot keeps following the drag once it
|
|
3568
|
+
// starts, rather than freezing at whatever it last showed during
|
|
3569
|
+
// the pre-press hover (or never lighting up at all, if the drag
|
|
3570
|
+
// started from a keyboard Enter with no hover first) — same
|
|
3571
|
+
// `cycle`-to-`cell` conversion as `onCanvasHover`, at this same
|
|
3572
|
+
// boundary.
|
|
3573
|
+
hoverBoundary = { laneIndex: at.laneIndex, cell: at.cycle };
|
|
3574
|
+
}
|
|
3575
|
+
// The document is untouched during the drag (rule 2): this repaints
|
|
3576
|
+
// the preview line only, never a `commit`.
|
|
3577
|
+
const wds = {
|
|
3578
|
+
cursor: cursor,
|
|
3579
|
+
selection: selection,
|
|
3580
|
+
pendingFrom: pendingFrom,
|
|
3581
|
+
endpointDrag: endpointDrag,
|
|
3582
|
+
selectedEdge: selectedEdge,
|
|
3583
|
+
overlay: overlay,
|
|
3584
|
+
canvasWrap: canvasWrap,
|
|
3585
|
+
dataEdit: dataEdit,
|
|
3586
|
+
enterDrawing: enterDrawing,
|
|
3587
|
+
onCanvasDown: onCanvasDown,
|
|
3588
|
+
onCanvasHover: onCanvasHover,
|
|
3589
|
+
edgeMode: edgeMode,
|
|
3590
|
+
hoverBoundary: hoverBoundary,
|
|
3591
|
+
};
|
|
3592
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3593
|
+
cursor = wds.cursor;
|
|
3594
|
+
selection = wds.selection;
|
|
3595
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3596
|
+
paintSelectionState(store.doc, layout);
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
2011
3599
|
if (drag === null) return;
|
|
2012
3600
|
// The button came up somewhere this page never heard about — outside the
|
|
2013
3601
|
// window, or over the browser's own chrome. Without this the drag stays
|
|
@@ -2022,16 +3610,53 @@
|
|
|
2022
3610
|
// The lane is fixed by the press: a drag that strays onto a neighbour is
|
|
2023
3611
|
// still painting the lane it started on.
|
|
2024
3612
|
drag.to = at.cycle;
|
|
2025
|
-
selection = {
|
|
3613
|
+
selection = {
|
|
3614
|
+
laneIndex: drag.laneIndex, laneTo: drag.laneIndex,
|
|
3615
|
+
from: drag.from, to: drag.to,
|
|
3616
|
+
};
|
|
2026
3617
|
// The drag's live end is where the cursor is, so a paint continued with
|
|
2027
3618
|
// Shift+←/→ after the button comes up carries on from the same cell.
|
|
2028
3619
|
cursor = { laneIndex: drag.laneIndex, cycle: drag.to };
|
|
2029
|
-
|
|
3620
|
+
const wds = {
|
|
3621
|
+
cursor: cursor,
|
|
3622
|
+
selection: selection,
|
|
3623
|
+
pendingFrom: pendingFrom,
|
|
3624
|
+
endpointDrag: endpointDrag,
|
|
3625
|
+
selectedEdge: selectedEdge,
|
|
3626
|
+
overlay: overlay,
|
|
3627
|
+
canvasWrap: canvasWrap,
|
|
3628
|
+
dataEdit: dataEdit,
|
|
3629
|
+
enterDrawing: enterDrawing,
|
|
3630
|
+
onCanvasDown: onCanvasDown,
|
|
3631
|
+
onCanvasHover: onCanvasHover,
|
|
3632
|
+
edgeMode: edgeMode,
|
|
3633
|
+
hoverBoundary: hoverBoundary,
|
|
3634
|
+
};
|
|
3635
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3636
|
+
cursor = wds.cursor;
|
|
3637
|
+
selection = wds.selection;
|
|
3638
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3639
|
+
paintSelectionState(store.doc, layout);
|
|
2030
3640
|
}
|
|
2031
3641
|
|
|
2032
3642
|
function onCanvasUp() {
|
|
2033
3643
|
d.removeEventListener('mousemove', onCanvasMove, true);
|
|
2034
3644
|
d.removeEventListener('mouseup', onCanvasUp, true);
|
|
3645
|
+
if (endpointDrag !== null) {
|
|
3646
|
+
finishEndpointDrag();
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (edgeMode === 'dragging') {
|
|
3650
|
+
// `cursor` is never null here: `onCanvasDown` set it to the press
|
|
3651
|
+
// cell before this drag was ever armed, and every move since then
|
|
3652
|
+
// only ever moved it to another real cell (see `onCanvasMove`
|
|
3653
|
+
// above). The null-safe form costs nothing and matches the
|
|
3654
|
+
// `finishEdgeDrag`/`codec.ensureNode` contract exactly rather than
|
|
3655
|
+
// asserting a fact this file does not have to assert.
|
|
3656
|
+
finishEdgeDrag(cursor === null ? null : cursor.laneIndex,
|
|
3657
|
+
cursor === null ? null : cursor.cycle);
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
2035
3660
|
if (drag === null) return;
|
|
2036
3661
|
const g = drag;
|
|
2037
3662
|
drag = null;
|
|
@@ -2040,53 +3665,347 @@
|
|
|
2040
3665
|
});
|
|
2041
3666
|
}
|
|
2042
3667
|
|
|
2043
|
-
/**
|
|
3668
|
+
/**
|
|
3669
|
+
* The end of an endpoint drag on the SELECTED edge — one `commit`, so one
|
|
3670
|
+
* undo step, matching every other gesture in this file (constraint 5).
|
|
3671
|
+
*
|
|
3672
|
+
* Unlike `finishEdgeDrag` below, this has no keyboard-driven twin and
|
|
3673
|
+
* takes no parameters: there is no second input device sharing `cursor`
|
|
3674
|
+
* to aim it, so everything the drop needs already lives in `endpointDrag`
|
|
3675
|
+
* itself, read and cleared in the same first two lines the same way
|
|
3676
|
+
* `finishEdgeDrag` clears `pendingFrom`'s owning mode up front.
|
|
3677
|
+
*
|
|
3678
|
+
* `drag.at === null` means the pointer never landed on a real cell for
|
|
3679
|
+
* the whole gesture (pressed the handle, released off the diagram or
|
|
3680
|
+
* without ever moving) — nothing to drop onto, so this only repaints to
|
|
3681
|
+
* take the (already absent) preview line off screen and does not call
|
|
3682
|
+
* `commit` at all; a `commit` with a non-integer `at` would just be
|
|
3683
|
+
* `codec.moveEdgeEnd`'s own bounds check refusing it one function later
|
|
3684
|
+
* for no benefit.
|
|
3685
|
+
*
|
|
3686
|
+
* Every other landing — including the three no-ops `moveEdgeEnd` itself
|
|
3687
|
+
* refuses (dropping back on the end's own cell, dropping on the cell
|
|
3688
|
+
* holding the edge's OTHER end, and an out-of-range target) — reaches
|
|
3689
|
+
* `commit` exactly once and is judged by `commit`'s own return value,
|
|
3690
|
+
* the same "one test of did-anything-change" rule `finishEdgeDrag`
|
|
3691
|
+
* documents at length below: nothing here re-derives any of those three
|
|
3692
|
+
* checks, that would be a second belief about the same refusal living
|
|
3693
|
+
* next to `moveEdgeEnd`'s own.
|
|
3694
|
+
*/
|
|
3695
|
+
function finishEndpointDrag() {
|
|
3696
|
+
const drag = endpointDrag;
|
|
3697
|
+
endpointDrag = null;
|
|
3698
|
+
if (drag === null) return;
|
|
3699
|
+
if (drag.at === null) { render(); return; }
|
|
3700
|
+
commit('edge-move', function (doc) {
|
|
3701
|
+
return codec.moveEdgeEnd(doc, drag.index, drag.end, drag.at, drag.cell);
|
|
3702
|
+
});
|
|
3703
|
+
render();
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* The end of an edge-creation gesture, reached from both input devices —
|
|
3708
|
+
* the mouse's `onCanvasUp` above and the keyboard's second Enter in
|
|
3709
|
+
* `handleDrawingKey` below. Constraint 4: one function, not two
|
|
3710
|
+
* histories of the same decision.
|
|
3711
|
+
*
|
|
3712
|
+
* `at`/`cell` name the drop cell, or `null`/`null` when the gesture did
|
|
3713
|
+
* not land on any lane at all. Nothing below special-cases the no-op
|
|
3714
|
+
* drops rule 3 lists — dropping back on the start cell, dropping on a
|
|
3715
|
+
* row with no waveform (a spacer — a group's own title is never a row
|
|
3716
|
+
* to drop on in the first place: `codec.isLane` rejects the title
|
|
3717
|
+
* string, so `flattenLanes` never allocates one, and there is no
|
|
3718
|
+
* coordinate here to mis-hit), dropping on the cell that already holds
|
|
3719
|
+
* this edge's OTHER end, and — fix round 1 — repeating the exact same
|
|
3720
|
+
* drag a second time:
|
|
3721
|
+
*
|
|
3722
|
+
* - a spacer's row IS a real, hittable cell (`geometry.cellAt` answers
|
|
3723
|
+
* it like any other), but `codec.ensureNode`'s own lane-local bound
|
|
3724
|
+
* refuses it: an empty `wave` reads as zero cells, so no cycle is
|
|
3725
|
+
* ever in range. The "missed the diagram entirely" case
|
|
3726
|
+
* (`at === null`) falls through the SAME check for a different
|
|
3727
|
+
* reason — neither `at` nor `cell` is an integer `ensureNode`'s
|
|
3728
|
+
* bounds test accepts;
|
|
3729
|
+
* - the start cell and the drop cell landing on the same anchor —
|
|
3730
|
+
* whether because they truly are the same cell, or because the drop
|
|
3731
|
+
* cell is wherever this edge's other end already lives — is exactly
|
|
3732
|
+
* `a.letter === b.letter` below;
|
|
3733
|
+
* - a repeat of this exact gesture between the same two cells would,
|
|
3734
|
+
* without the check below, silently push a byte-identical second
|
|
3735
|
+
* `doc.edge` entry — two arrows drawn exactly on top of each other,
|
|
3736
|
+
* indistinguishable on screen, plus a second undo step for nothing
|
|
3737
|
+
* visible (MEASURED against the real store: drag (0,1)->(0,3) twice,
|
|
3738
|
+
* `doc.edge` becomes `["a~>b","a~>b"]`). Refused here rather than
|
|
3739
|
+
* before `commit`, same as the other three: an entry with the same
|
|
3740
|
+
* four fields (`from`, `to`, `shape`, `label`) already in `doc.edge`
|
|
3741
|
+
* makes this call's own output redundant. This is narrower than
|
|
3742
|
+
* "refuse any second edge between these two anchors" — two
|
|
3743
|
+
* DIFFERENT annotations between the same pair (`a~>b setup` next to
|
|
3744
|
+
* `a-b hold`) are legitimate and stay possible; only a verbatim
|
|
3745
|
+
* repeat of THIS gesture's own fixed output (`shape: '~>'`,
|
|
3746
|
+
* `label: ''`) is refused.
|
|
3747
|
+
*
|
|
3748
|
+
* So there is one test of "did anything change", not four: `commit`'s
|
|
3749
|
+
* own return value. A refused/no-op commit pushes no undo step (`commit`
|
|
3750
|
+
* already guarantees that) and — rule 1 — leaves the gesture `'armed'`
|
|
3751
|
+
* rather than disarming it, because the user pressed the button and got
|
|
3752
|
+
* nothing for it; a real edge disarms back to `'idle'`. Either way this
|
|
3753
|
+
* repaints once more on its own, because `commit` only repaints when it
|
|
3754
|
+
* changed something, and the pending-line preview has to come off the
|
|
3755
|
+
* screen in the no-op case too.
|
|
3756
|
+
*
|
|
3757
|
+
* Fix round 3: `pendingFrom === null` on entry is no longer read as
|
|
3758
|
+
* "nothing to do" and left at that. `carryMarks` can now drop
|
|
3759
|
+
* `pendingFrom` to `null` on its own (round 2, both when a structural
|
|
3760
|
+
* change makes it untraceable and when no `laneTrace` is injected at
|
|
3761
|
+
* all) WITHOUT touching `edgeMode` — before round 2 the only code path
|
|
3762
|
+
* that ever nulled `pendingFrom` was `setEdgeMode('idle')` itself, so
|
|
3763
|
+
* the two could never come apart. Reached from `onCanvasUp` while
|
|
3764
|
+
* `edgeMode === 'dragging'` (the mouse button was down when the
|
|
3765
|
+
* document changed out from under it), leaving `edgeMode` untouched
|
|
3766
|
+
* here would strand it at `'dragging'` forever: `onCanvasMove` and
|
|
3767
|
+
* `onCanvasUp` both check that mode FIRST, so every later paint drag's
|
|
3768
|
+
* `mousemove`/`mouseup` would be swallowed by this function instead of
|
|
3769
|
+
* painting, silently and with no error. So this resyncs the mode to
|
|
3770
|
+
* the mark it depends on before doing anything else.
|
|
3771
|
+
*/
|
|
3772
|
+
function finishEdgeDrag(at, cell) {
|
|
3773
|
+
if (pendingFrom === null) { setEdgeMode('idle'); render(); return; }
|
|
3774
|
+
const from = pendingFrom;
|
|
3775
|
+
const changed = commit('edge-add', function (doc) {
|
|
3776
|
+
const a = codec.ensureNode(doc, from.at, from.cell);
|
|
3777
|
+
if (a.letter === null) return doc;
|
|
3778
|
+
const b = codec.ensureNode(a.doc, at, cell);
|
|
3779
|
+
if (b.letter === null) return doc;
|
|
3780
|
+
if (a.letter === b.letter) return doc; // same anchor both ends
|
|
3781
|
+
const dup = (b.doc.edge || []).some(function (entry) {
|
|
3782
|
+
const e = codec.parseEdge(entry);
|
|
3783
|
+
return e !== null && e.from === a.letter && e.to === b.letter &&
|
|
3784
|
+
e.shape === '~>' && e.label === '';
|
|
3785
|
+
});
|
|
3786
|
+
if (dup) return doc; // byte-identical repeat of this gesture
|
|
3787
|
+
return codec.addEdge(b.doc, { from: a.letter, to: b.letter, shape: '~>', label: '' });
|
|
3788
|
+
});
|
|
3789
|
+
setEdgeMode(changed ? 'idle' : 'armed');
|
|
3790
|
+
render();
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Pointer coordinates in the drawing's own space, then the codec's cell.
|
|
3795
|
+
*
|
|
3796
|
+
* v3.6.0 Task 11: while an edge gesture is live — `edgeMode !== 'idle'`
|
|
3797
|
+
* (armed, marking a start; or dragging, aiming the other end) or an
|
|
3798
|
+
* `endpointDrag` is in flight — the target is the ANCHOR the engine
|
|
3799
|
+
* actually draws the line to, not the cell under the pointer.
|
|
3800
|
+
* `geometry.boundaryAt` rounds to the nearest one; `geometry.cellAt`
|
|
3801
|
+
* floors to the cell the pointer is over. Painting still wants the
|
|
3802
|
+
* floor: the brush's target is the cell you are over, and unifying the
|
|
3803
|
+
* two here is exactly the regression this task exists to prevent (see
|
|
3804
|
+
* `boundaryAt`'s own comment in `wave-geometry.js`).
|
|
3805
|
+
*
|
|
3806
|
+
* `boundaryAt` returns `{laneIndex, cell}`; every caller of this
|
|
3807
|
+
* function already destructures `{laneIndex, cycle}` from `cellAt`'s
|
|
3808
|
+
* shape, so the field is renamed HERE, at the one place the two
|
|
3809
|
+
* definitions meet, rather than letting a `cell` leak into a `cycle`
|
|
3810
|
+
* slot downstream.
|
|
3811
|
+
*/
|
|
2044
3812
|
function cellFromEvent(ev, layout) {
|
|
2045
3813
|
if (canvas === null) return null;
|
|
2046
3814
|
const box = canvas.getBoundingClientRect();
|
|
2047
|
-
|
|
3815
|
+
const x = ev.clientX - box.left;
|
|
3816
|
+
const y = ev.clientY - box.top;
|
|
3817
|
+
if (edgeMode !== 'idle' || endpointDrag !== null) {
|
|
3818
|
+
const b = geometry.boundaryAt(layout, x, y);
|
|
3819
|
+
return b === null ? null : { laneIndex: b.laneIndex, cycle: b.cell };
|
|
3820
|
+
}
|
|
3821
|
+
return geometry.cellAt(layout, x, y);
|
|
2048
3822
|
}
|
|
2049
3823
|
|
|
2050
|
-
// ── the preview ───────────────────────────────────────────────────────
|
|
2051
3824
|
/**
|
|
2052
|
-
*
|
|
3825
|
+
* Pure hover, no button down, nothing committed — which anchor the
|
|
3826
|
+
* pointer is nearest while armed and not yet dragging, purely so
|
|
3827
|
+
* `drawTransitions`'s hot dot can light up before the user presses.
|
|
2053
3828
|
*
|
|
2054
|
-
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
3829
|
+
* A canvas-own listener rather than the document-level
|
|
3830
|
+
* `onCanvasMove`/`onCanvasUp` pair: those two are only added once a
|
|
3831
|
+
* drag actually starts (`onCanvasDown`'s 'armed' and endpoint-drag
|
|
3832
|
+
* branches), so a plain hover before the first press has nothing else
|
|
3833
|
+
* watching it. Once a drag IS live, `onCanvasMove`'s own branches
|
|
3834
|
+
* already repaint on every move and set `hoverBoundary` themselves
|
|
3835
|
+
* (below), so this steps out of the way rather than doing the same
|
|
3836
|
+
* work under a second name — the `edgeMode !== 'armed'` guard covers
|
|
3837
|
+
* both: `edgeMode === 'dragging'` fails it directly, and an
|
|
3838
|
+
* `endpointDrag` never moves `edgeMode` away from 'idle' in the first
|
|
3839
|
+
* place (it is a wholly separate gesture from edge-creation), so it
|
|
3840
|
+
* fails the same guard too.
|
|
2057
3841
|
*/
|
|
2058
|
-
function
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
3842
|
+
function onCanvasHover(ev) {
|
|
3843
|
+
if (edgeMode !== 'armed') return;
|
|
3844
|
+
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
3845
|
+
const at = cellFromEvent(ev, layout);
|
|
3846
|
+
const next = at === null ? null : { laneIndex: at.laneIndex, cell: at.cycle };
|
|
3847
|
+
const same = (hoverBoundary === null && next === null) ||
|
|
3848
|
+
(hoverBoundary !== null && next !== null &&
|
|
3849
|
+
hoverBoundary.laneIndex === next.laneIndex && hoverBoundary.cell === next.cell);
|
|
3850
|
+
if (same) return;
|
|
3851
|
+
hoverBoundary = next;
|
|
3852
|
+
const wds = {
|
|
3853
|
+
cursor: cursor,
|
|
3854
|
+
selection: selection,
|
|
3855
|
+
pendingFrom: pendingFrom,
|
|
3856
|
+
endpointDrag: endpointDrag,
|
|
3857
|
+
selectedEdge: selectedEdge,
|
|
3858
|
+
overlay: overlay,
|
|
3859
|
+
canvasWrap: canvasWrap,
|
|
3860
|
+
dataEdit: dataEdit,
|
|
3861
|
+
enterDrawing: enterDrawing,
|
|
3862
|
+
onCanvasDown: onCanvasDown,
|
|
3863
|
+
onCanvasHover: onCanvasHover,
|
|
3864
|
+
edgeMode: edgeMode,
|
|
3865
|
+
hoverBoundary: hoverBoundary,
|
|
3866
|
+
};
|
|
3867
|
+
canvas = drawer.renderCanvas(canvas, store.doc, layout, wds);
|
|
3868
|
+
cursor = wds.cursor;
|
|
3869
|
+
selection = wds.selection;
|
|
3870
|
+
// The toolbar reads `selection`; this repaint can change it (fix round 1).
|
|
3871
|
+
paintSelectionState(store.doc, layout);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
/**
|
|
3875
|
+
* The `doc.edge` index the press landed on, or `null`.
|
|
3876
|
+
*
|
|
3877
|
+
* Two questions, in order, both against `geometry.edgeLayout(store.doc,
|
|
3878
|
+
* layout)` computed fresh here — the same recompute-don't-cache rule
|
|
3879
|
+
* `cellFromEvent`/`geometry.cellAt` already follow, so a press can never
|
|
3880
|
+
* be judged against a layout `renderCanvas` has moved on from:
|
|
3881
|
+
*
|
|
3882
|
+
* 1. `geometry.edgeHandleAt` — a handle is a small rect centred on an
|
|
3883
|
+
* endpoint, asked in the SAME layout-local space `cellFromEvent`
|
|
3884
|
+
* uses (the canvas's own `getBoundingClientRect()`). v3.6.0 Task 6:
|
|
3885
|
+
* `SIZES.nameColWidth` is 120, not 0 — but `ev.clientX - box.left`
|
|
3886
|
+
* still needs nothing added or subtracted for it, because the name
|
|
3887
|
+
* column's width is already baked into every cell's own `originX`
|
|
3888
|
+
* (`wave-geometry.layoutOf`), the same layout-local coordinate
|
|
3889
|
+
* `geometry.edgeHandleAt`/`cellAt` read. One coordinate space, not
|
|
3890
|
+
* two reconciled by an offset — a nonzero name column moves where
|
|
3891
|
+
* cycle 0 STARTS in that space, it does not add a second term on
|
|
3892
|
+
* top of it. final review finding 4: trusted when it names the
|
|
3893
|
+
* edge that is actually SELECTED — `renderEdges` only DRAWS a
|
|
3894
|
+
* handle on the selected edge, so its answer for any OTHER edge is a
|
|
3895
|
+
* rectangle with nothing on screen, and letting that phantom
|
|
3896
|
+
* rectangle claim the press is what used to swallow a bus label's
|
|
3897
|
+
* press whenever the label's cell doubled as an unselected edge's
|
|
3898
|
+
* endpoint (see `onCanvasDown`'s `busLabelHitAt` check, which now
|
|
3899
|
+
* runs before this function is even called, for the other half of
|
|
3900
|
+
* that fix). A press within a handle-sized target of the SELECTED
|
|
3901
|
+
* edge's own endpoint still selects it rather than painting the
|
|
3902
|
+
* cell under it. final review re-review, item 1: ALSO trusted when
|
|
3903
|
+
* the named edge is a self-loop (`from === to`), selected or not —
|
|
3904
|
+
* a self-loop's `pathFor` output is a zero-length path (every
|
|
3905
|
+
* command's endpoint equals its start), which Chrome gives no hit
|
|
3906
|
+
* area at all, so #2 below can never find one; the handle is the
|
|
3907
|
+
* only thing a self-loop can ever be selected by, and a self-loop
|
|
3908
|
+
* the user hand-writes into the markdown must stay selectable and
|
|
3909
|
+
* deletable even before it has ever been clicked once.
|
|
3910
|
+
* 2. The `<path class="ed-wave-edge-hit">` twin `renderEdges` layers
|
|
3911
|
+
* over each visible edge, read back through `document.elementFromPoint`
|
|
3912
|
+
* at the press's SCREEN coordinates (no conversion needed — that is
|
|
3913
|
+
* what `elementFromPoint` already takes).
|
|
3914
|
+
*/
|
|
3915
|
+
function edgeHitAt(ev, layout) {
|
|
3916
|
+
if (canvas === null) return null;
|
|
3917
|
+
const edges = geometry.edgeLayout(store.doc, layout);
|
|
3918
|
+
if (edges.length === 0) return null;
|
|
3919
|
+
const box = canvas.getBoundingClientRect();
|
|
3920
|
+
const handle = geometry.edgeHandleAt(edges, ev.clientX - box.left, ev.clientY - box.top);
|
|
3921
|
+
// final review finding 4: only trust the handle answer when it names
|
|
3922
|
+
// the edge that is actually SELECTED — the same scoping the
|
|
3923
|
+
// endpoint-drag branch in `onCanvasDown` already applies, for the same
|
|
3924
|
+
// reason (see its comment): `renderEdges` only ever DRAWS a handle on
|
|
3925
|
+
// the selected edge, so `geometry.edgeHandleAt`'s answer for any OTHER
|
|
3926
|
+
// edge index is a rectangle with nothing on screen at all. Before this,
|
|
3927
|
+
// that undrawn rectangle still won the press here — a bus label whose
|
|
3928
|
+
// cell happened to double as an unselected edge's endpoint could never
|
|
3929
|
+
// open its label editor, because this branch claimed the press first.
|
|
3930
|
+
// An edge stays selectable regardless: the fall-through below still
|
|
3931
|
+
// hit-tests every edge's own 10px `.ed-wave-edge-hit` path.
|
|
3932
|
+
//
|
|
3933
|
+
// final review re-review, item 1: EXCEPT a self-loop (`from === to`),
|
|
3934
|
+
// which this "an edge stays selectable regardless" promise does not
|
|
3935
|
+
// actually cover — MEASURED regression, not a hypothetical: `pathFor`
|
|
3936
|
+
// degenerates to a zero-length path for every shape family when
|
|
3937
|
+
// `from === to` (every `L`/`C` command's endpoint equals its start),
|
|
3938
|
+
// and Chrome gives a zero-length, butt-cap path NO hit area at all —
|
|
3939
|
+
// `elementsFromPoint` returns nothing there, so the `.ed-wave-edge-hit`
|
|
3940
|
+
// fallback below can never find it either. The phantom handle this
|
|
3941
|
+
// finding gates off was the ONLY thing that could ever select a
|
|
3942
|
+
// self-loop that has never been selected before (`selectedEdge` is set
|
|
3943
|
+
// from exactly this function's return value, nowhere else) — a
|
|
3944
|
+
// self-loop the user hand-writes into the markdown (the UI itself
|
|
3945
|
+
// never creates one; `finishEdgeDrag` refuses a same-letter drag) would
|
|
3946
|
+
// otherwise be permanently unselectable and undeletable by mouse. So a
|
|
3947
|
+
// handle match is trusted when it is drawn (`handle.index ===
|
|
3948
|
+
// selectedEdge`, as before) OR when the edge it names is a self-loop —
|
|
3949
|
+
// a self-loop's handle is the one hit target it has, selected or not.
|
|
3950
|
+
if (handle !== null) {
|
|
3951
|
+
if (handle.index === selectedEdge) return handle.index;
|
|
3952
|
+
const named = edges.filter(function (e) { return e.index === handle.index; })[0];
|
|
3953
|
+
if (named !== undefined && named.edge.from === named.edge.to) return handle.index;
|
|
2065
3954
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
3955
|
+
if (typeof d.elementFromPoint !== 'function') return null;
|
|
3956
|
+
const el = d.elementFromPoint(ev.clientX, ev.clientY);
|
|
3957
|
+
const hitPath = el !== null && el !== undefined && typeof el.closest === 'function'
|
|
3958
|
+
? el.closest('.ed-wave-edge-hit') : null;
|
|
3959
|
+
if (hitPath === null || !canvas.contains(hitPath)) return null;
|
|
3960
|
+
const idx = hitPath.getAttribute('data-edge-index');
|
|
3961
|
+
return idx === null ? null : Number(idx);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
/**
|
|
3965
|
+
* The `.ed-wave-buslabel` mark under a press, or `null` — final review
|
|
3966
|
+
* finding 4.
|
|
3967
|
+
*
|
|
3968
|
+
* NOT `ev.target`: `renderEdges` draws `.ed-wave-edge-hit` (invisible,
|
|
3969
|
+
* `pointer-events: stroke`, 10px wide) AFTER every lane's own content, so
|
|
3970
|
+
* whenever an edge's endpoint lands on a bus cell, that stroke paints ON
|
|
3971
|
+
* TOP of the cell's own label mark — `ev.target` at that pixel is the
|
|
3972
|
+
* edge-hit path, never the label underneath it, and no amount of
|
|
3973
|
+
* reordering the CALLS in `onCanvasDown` changes that, because `ev.target`
|
|
3974
|
+
* is the browser's own topmost-element answer, decided at dispatch time
|
|
3975
|
+
* by paint order, not by which `if` this file happens to ask first.
|
|
3976
|
+
* MEASURED against a real page: moving the bus-label check ahead of
|
|
3977
|
+
* `edgeHitAt` while still reading `ev.target` left the label unreachable,
|
|
3978
|
+
* identically to before.
|
|
3979
|
+
*
|
|
3980
|
+
* `document.elementsFromPoint` (plural — distinct from the singular
|
|
3981
|
+
* `elementFromPoint` `edgeHitAt` above reads) answers a different
|
|
3982
|
+
* question: the WHOLE stack of elements under the point, in z-order, not
|
|
3983
|
+
* only the topmost one. Walking it for a `.ed-wave-buslabel` finds the
|
|
3984
|
+
* mark even when something else is painted over it, which is exactly
|
|
3985
|
+
* this finding's fix. The walk stops at `canvas` itself so a label-shaped
|
|
3986
|
+
* class elsewhere in the document (outside the canvas this press is
|
|
3987
|
+
* even inside) can never match.
|
|
3988
|
+
*
|
|
3989
|
+
* Falls back to the old `ev.target`-only check when `elementsFromPoint`
|
|
3990
|
+
* is not available at all (older engines) — that keeps every OTHER bus
|
|
3991
|
+
* label (the overwhelming majority, which do not sit under an edge's
|
|
3992
|
+
* endpoint) working exactly as it always has; only the overlap case this
|
|
3993
|
+
* finding is about stays unfixed on such an engine.
|
|
3994
|
+
*/
|
|
3995
|
+
function busLabelHitAt(ev) {
|
|
3996
|
+
function isLabel(el) {
|
|
3997
|
+
return el !== null && el !== undefined && typeof el.classList === 'object' &&
|
|
3998
|
+
el.classList !== null && el.classList.contains('ed-wave-buslabel');
|
|
2089
3999
|
}
|
|
4000
|
+
if (typeof d.elementsFromPoint === 'function') {
|
|
4001
|
+
const stack = d.elementsFromPoint(ev.clientX, ev.clientY);
|
|
4002
|
+
for (const el of stack) {
|
|
4003
|
+
if (isLabel(el)) return el;
|
|
4004
|
+
if (el === canvas) break;
|
|
4005
|
+
}
|
|
4006
|
+
return null;
|
|
4007
|
+
}
|
|
4008
|
+
return isLabel(ev.target) ? ev.target : null;
|
|
2090
4009
|
}
|
|
2091
4010
|
|
|
2092
4011
|
// Mounted BEFORE the first paint, not after: the preview is rendered by the
|
|
@@ -2101,8 +4020,24 @@
|
|
|
2101
4020
|
render();
|
|
2102
4021
|
// The opening line described a pointer and only a pointer, which is how a
|
|
2103
4022
|
// surface ends up with no keyboard: the instructions never mentioned one.
|
|
2104
|
-
|
|
2105
|
-
|
|
4023
|
+
//
|
|
4024
|
+
// v3.6.0 Task 16 fix round 1 applies that same reasoning to this task's
|
|
4025
|
+
// own gesture. Reordering a lane became keyboard-reachable here, and the
|
|
4026
|
+
// only place that said so was a HOVER tooltip on the rail's drag handle
|
|
4027
|
+
// — a `<span>` with deliberately no focus key, i.e. the one surface a
|
|
4028
|
+
// keyboard user can never read. This is a one-shot line, not the
|
|
4029
|
+
// per-move announcement `sayCursor` owns, so a hint belongs in it (see
|
|
4030
|
+
// `sayCursor`'s own docstring for why the same hint may NOT go there).
|
|
4031
|
+
//
|
|
4032
|
+
// fix round 3 re-points the last clause. Round 1 wrote it as an EXCEPTION
|
|
4033
|
+
// ("武裝關聯線之後,左右鍵改成在轉態點之間跳") because round 0's binding
|
|
4034
|
+
// really did change what two keys this same sentence had just promised.
|
|
4035
|
+
// Round 3 moved the jump to Alt+←/→, so the promise above holds in every
|
|
4036
|
+
// state and the clause is now an extra key rather than a retraction.
|
|
4037
|
+
panels.say('選一個電位,然後在波形上按住拖過去;或直接按方向鍵進波形,' +
|
|
4038
|
+
'Shift+左右鍵選一段,再按電位字元塗上去。' +
|
|
4039
|
+
'Alt+↑/Alt+↓ 把選取的 lane 往上下搬;' +
|
|
4040
|
+
'武裝關聯線之後,Alt+左右鍵可以在轉態點之間跳。');
|
|
2106
4041
|
|
|
2107
4042
|
return {
|
|
2108
4043
|
el: overlay,
|
|
@@ -2115,10 +4050,9 @@
|
|
|
2115
4050
|
|
|
2116
4051
|
return {
|
|
2117
4052
|
createWaveEditor: createWaveEditor,
|
|
2118
|
-
brickOf: brickOf,
|
|
2119
|
-
groupSpansOf: groupSpansOf,
|
|
2120
4053
|
mountedInsideContent: mountedInsideContent,
|
|
2121
4054
|
BRUSHES: BRUSHES,
|
|
4055
|
+
isBrushKey: isBrushKey,
|
|
2122
4056
|
SIZES: SIZES,
|
|
2123
4057
|
};
|
|
2124
4058
|
});
|