@helping-ai-workflow/md2doc 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/editor/client.js +44 -5
- package/lib/editor/server.js +21 -2
- package/lib/editor/wave-codec.js +160 -3
- package/lib/editor/wave-draw.js +1470 -0
- package/lib/editor/wave-geometry.js +469 -19
- package/lib/editor/wave-panels.js +1102 -0
- package/lib/editor/wave-store.js +36 -1
- package/lib/editor/wave-ui.js +1807 -1802
- package/lib/md2doc.js +520 -50
- package/package.json +2 -2
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:
|
|
@@ -103,8 +104,25 @@
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/** Drawing sizes. Handed to `wave-geometry` so the hit test and the paint
|
|
106
|
-
* cannot drift; nothing here re-derives a row position.
|
|
107
|
-
|
|
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 };
|
|
108
126
|
|
|
109
127
|
/**
|
|
110
128
|
* The index the preview is rendered under, and why it is not 0.
|
|
@@ -131,52 +149,6 @@
|
|
|
131
149
|
const PREVIEW_INDEX = 9000;
|
|
132
150
|
const PREVIEW_ID_PREFIX = 'md2doc-wave-preview-';
|
|
133
151
|
|
|
134
|
-
/**
|
|
135
|
-
* What wavedrom 3.5.0 actually draws for one level, by its own brick id.
|
|
136
|
-
*
|
|
137
|
-
* Every row was MEASURED in this session against the pinned engine, by
|
|
138
|
-
* rendering `wave: 'z' + c + c` (the `z` anchors the char so it is never at
|
|
139
|
-
* cycle 0) and reading the second half-brick of each cycle out of the
|
|
140
|
-
* rendered SVG's `<use xlink:href="#…">` list:
|
|
141
|
-
*
|
|
142
|
-
* 0 000 1 111 x xxx z zzz u uuu d ddd
|
|
143
|
-
* h 111 l 000 H 111 L 000
|
|
144
|
-
* p nclk P nclk n pclk N pclk
|
|
145
|
-
* = vvv-2 2 vvv-2 3 vvv-3 … 9 vvv-9
|
|
146
|
-
* anything else xxx (measured with 'Q' and 'ä')
|
|
147
|
-
*
|
|
148
|
-
* So this is not a re-implementation of the engine's level model — `levelsOf`
|
|
149
|
-
* is that, and it lives in the codec. This is the last step: which PICTURE
|
|
150
|
-
* the engine paints for a level it has already decided. Keeping it in
|
|
151
|
-
* production code rather than in the test is deliberate: the drawing has to
|
|
152
|
-
* make exactly this decision anyway (an `h` lane is drawn high, an `=` lane
|
|
153
|
-
* is drawn as a bus), and a copy of it living only in a test would be a
|
|
154
|
-
* second belief about the engine with nothing pinning it to the first.
|
|
155
|
-
*/
|
|
156
|
-
function brickOf(level) {
|
|
157
|
-
if (typeof level !== 'string' || level.length === 0) return 'xxx';
|
|
158
|
-
if (level === '0' || level === 'l' || level === 'L') return '000';
|
|
159
|
-
if (level === '1' || level === 'h' || level === 'H') return '111';
|
|
160
|
-
if (level === 'z') return 'zzz';
|
|
161
|
-
if (level === 'u') return 'uuu';
|
|
162
|
-
if (level === 'd') return 'ddd';
|
|
163
|
-
if (level === 'p' || level === 'P') return 'nclk';
|
|
164
|
-
if (level === 'n' || level === 'N') return 'pclk';
|
|
165
|
-
if (level === '=') return 'vvv-2';
|
|
166
|
-
if (level >= '2' && level <= '9') return 'vvv-' + level;
|
|
167
|
-
return 'xxx';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** A brick that draws a whole clock period and therefore never merges with
|
|
171
|
-
* its neighbour: two `p` cycles are two clock periods, not one long one. */
|
|
172
|
-
function isClock(brick) {
|
|
173
|
-
return brick === 'nclk' || brick === 'pclk';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function isBus(brick) {
|
|
177
|
-
return brick.slice(0, 4) === 'vvv-';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
152
|
/**
|
|
181
153
|
* Whether `el` is inside the editor's serialisation surface.
|
|
182
154
|
*
|
|
@@ -194,28 +166,6 @@
|
|
|
194
166
|
return false;
|
|
195
167
|
}
|
|
196
168
|
|
|
197
|
-
/**
|
|
198
|
-
* A plain, unfrozen copy of a document.
|
|
199
|
-
*
|
|
200
|
-
* The store's documents are deep-frozen — that is what stops a stray write
|
|
201
|
-
* from corrupting an undo entry — and wavedrom's renderer WRITES INTO the
|
|
202
|
-
* source object it is handed. Under this file's strict prologue that is a
|
|
203
|
-
* throw, so the preview gets its own copy every time. Bounded by the codec's
|
|
204
|
-
* own nesting ceiling so a pathological document cannot run the stack out
|
|
205
|
-
* here either.
|
|
206
|
-
*/
|
|
207
|
-
function plainCopy(value, depth) {
|
|
208
|
-
if (depth > 64 || value === null || typeof value !== 'object') return value;
|
|
209
|
-
if (Array.isArray(value)) {
|
|
210
|
-
const out = [];
|
|
211
|
-
for (let i = 0; i < value.length; i++) out.push(plainCopy(value[i], depth + 1));
|
|
212
|
-
return out;
|
|
213
|
-
}
|
|
214
|
-
const out = {};
|
|
215
|
-
for (const key of Object.keys(value)) out[key] = plainCopy(value[key], depth + 1);
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
169
|
/** Line and column of a byte offset, 1-based — what a parse failure has to
|
|
220
170
|
* say beyond its own message for the offset to mean anything to a person. */
|
|
221
171
|
function whereIs(text, offset) {
|
|
@@ -224,82 +174,14 @@
|
|
|
224
174
|
return { line: lines.length, column: lines[lines.length - 1].length + 1 };
|
|
225
175
|
}
|
|
226
176
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* would put the lane list one row out of step with the waveform beside it for
|
|
236
|
-
* every lane below the group, which is exactly the class of silent
|
|
237
|
-
* disagreement this batch keeps producing.
|
|
238
|
-
*
|
|
239
|
-
* The span is derived from the lanes' own paths — `layout.lanes[i].path` is
|
|
240
|
-
* the codec's — so a group with no lanes under it has no span and is not
|
|
241
|
-
* shown here; it is not addressable through the bridge either.
|
|
242
|
-
*/
|
|
243
|
-
function groupSpansOf(doc, layout) {
|
|
244
|
-
const byKey = new Map();
|
|
245
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
246
|
-
const path = layout.lanes[i].path;
|
|
247
|
-
// Every proper ancestor between `signal` and the lane itself is a group.
|
|
248
|
-
for (let cut = 2; cut < path.length; cut++) {
|
|
249
|
-
const prefix = path.slice(0, cut);
|
|
250
|
-
const key = JSON.stringify(prefix);
|
|
251
|
-
const seen = byKey.get(key);
|
|
252
|
-
if (seen === undefined) {
|
|
253
|
-
byKey.set(key, { path: prefix, depth: cut - 1, from: i, to: i });
|
|
254
|
-
} else {
|
|
255
|
-
seen.to = i;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const out = [];
|
|
260
|
-
for (const span of byKey.values()) {
|
|
261
|
-
const container = valueAt(doc, span.path);
|
|
262
|
-
const title = Array.isArray(container) && typeof container[0] === 'string'
|
|
263
|
-
? container[0] : null;
|
|
264
|
-
if (title === null) continue; // an untitled group has nothing to show
|
|
265
|
-
out.push({ path: span.path, title: title, depth: span.depth,
|
|
266
|
-
from: span.from, to: span.to });
|
|
267
|
-
}
|
|
268
|
-
out.sort(function (a, b) { return a.from - b.from || a.depth - b.depth; });
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* v3.5.0 Task 12: the lowest display position inside `span`'s range whose
|
|
274
|
-
* lane is a DIRECT member of `span`'s own group array — one nested exactly
|
|
275
|
-
* one level deeper than `span.path` — rather than one that lives inside a
|
|
276
|
-
* sub-group `span` merely contains. This is the one lane `codec.ungroupLanes`
|
|
277
|
-
* can be asked about to name THIS group and not some inner one: a lane's
|
|
278
|
-
* immediate parent is unambiguous (see that function's own comment), so a
|
|
279
|
-
* group is addressable through it exactly when it has at least one lane
|
|
280
|
-
* that belongs to it directly.
|
|
281
|
-
*
|
|
282
|
-
* `null` when it does not — a group built entirely out of sub-groups, with
|
|
283
|
-
* no lane of its own at any depth of one. That group's tag still renders
|
|
284
|
-
* (it has a title, and `groupSpansOf` shows every titled one), but nothing
|
|
285
|
-
* in the rail offers to dissolve it, because there is no `at` this codec
|
|
286
|
-
* shape can express that names it and only it.
|
|
287
|
-
*/
|
|
288
|
-
function directFirstLaneOf(layout, span) {
|
|
289
|
-
for (let i = span.from; i <= span.to; i++) {
|
|
290
|
-
if (layout.lanes[i].path.length === span.path.length + 1) return i;
|
|
291
|
-
}
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function valueAt(doc, path) {
|
|
296
|
-
let node = doc;
|
|
297
|
-
for (const seg of path) {
|
|
298
|
-
if (node === null || typeof node !== 'object') return undefined;
|
|
299
|
-
node = node[seg];
|
|
300
|
-
}
|
|
301
|
-
return node;
|
|
302
|
-
}
|
|
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.
|
|
303
185
|
|
|
304
186
|
/** The labels a lane's `data` supplies, one per explicit value cycle. */
|
|
305
187
|
function labelsOf(lane) {
|
|
@@ -350,9 +232,63 @@
|
|
|
350
232
|
throw new Error('wave-ui: the overlay may not be mounted inside .content ' +
|
|
351
233
|
'(its children are serialised back into the user markdown)');
|
|
352
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
|
+
}
|
|
353
252
|
|
|
354
253
|
const store = opts.createStore(opts.source);
|
|
355
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
|
+
|
|
356
292
|
const overlay = d.createElement('div');
|
|
357
293
|
overlay.className = 'ed-wave-overlay';
|
|
358
294
|
overlay.setAttribute('role', 'dialog');
|
|
@@ -367,15 +303,32 @@
|
|
|
367
303
|
// closing.
|
|
368
304
|
let brush = '1';
|
|
369
305
|
let clip = null;
|
|
370
|
-
|
|
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;
|
|
371
319
|
let drag = null; // live pointer drag, never pushed until mouseup
|
|
372
320
|
let gestures = 0;
|
|
373
321
|
let lastPatch = null;
|
|
374
322
|
// Where the keyboard should end up after the NEXT repaint, when that is not
|
|
375
|
-
// simply "wherever it was". A
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// 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.)
|
|
379
332
|
let focusOverride = null;
|
|
380
333
|
// Where the KEYBOARD is on the drawing: one cell, and the keyboard's whole
|
|
381
334
|
// equivalent of the pointer. Null until the keyboard has actually entered
|
|
@@ -396,6 +349,12 @@
|
|
|
396
349
|
// (deleted, or its entry no longer parses), so a stale index can never
|
|
397
350
|
// reach `renderEdgeBar` or a shape/label commit.
|
|
398
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;
|
|
399
358
|
// v3.5.0 Task 7: the edge-CREATION gesture's own state, shared verbatim
|
|
400
359
|
// by the pointer and the keyboard — one state machine, two input sources.
|
|
401
360
|
// `edgeMode` is explicit rather than inferred from "is a key held" or "is
|
|
@@ -418,6 +377,15 @@
|
|
|
418
377
|
// Enter still means "finish from where I already stood".
|
|
419
378
|
let edgeMode = 'idle';
|
|
420
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;
|
|
421
389
|
/** The one place `edgeMode` changes. Keeps the arm button's own visual
|
|
422
390
|
* state, the overlay's `data-wave-edgemode` (what a test or a future
|
|
423
391
|
* task reads instead of reaching into this closure) and the "is a start
|
|
@@ -428,6 +396,13 @@
|
|
|
428
396
|
edgeBtn.classList.toggle('is-on', next !== 'idle');
|
|
429
397
|
overlay.setAttribute('data-wave-edgemode', next);
|
|
430
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;
|
|
431
406
|
}
|
|
432
407
|
// v3.5.0 Task 8: dragging either END of the SELECTED edge to another
|
|
433
408
|
// cell. `{index, end, at, cell} | null` — `index` is the same original
|
|
@@ -460,28 +435,6 @@
|
|
|
460
435
|
// mid-drag undo) has to die with it rather than commit a move onto
|
|
461
436
|
// whatever entry now happens to sit at that array position.
|
|
462
437
|
let endpointDrag = null;
|
|
463
|
-
// v3.5.0 Task 12: which lane rows are Shift+click multi-selected on the
|
|
464
|
-
// rail, for「建立群組」— an array of display-position integers, in
|
|
465
|
-
// whatever order the clicks added them, or `null` when nothing is
|
|
466
|
-
// picked (never `[]`; the empty case is collapsed to `null` the moment
|
|
467
|
-
// the last member is toggled off, so every other mark's "is anything
|
|
468
|
-
// standing" check — a plain `!== null` — stays true of this one too).
|
|
469
|
-
//
|
|
470
|
-
// This is a fourth mark of the exact family `pendingFrom`/`endpointDrag`
|
|
471
|
-
// already are: a set of lanes the user has committed to for an upcoming
|
|
472
|
-
// STRUCTURAL edit, named by row number, which a reorder or an undo/redo
|
|
473
|
-
// can invalidate out from under it. So it is carried the same way —
|
|
474
|
-
// widening `carryMarks`'s guard and its `rowOf` lookup rather than
|
|
475
|
-
// growing a second belief about lane identity — and dropped the same
|
|
476
|
-
// way: an entry that cannot be traced to a surviving lane is removed
|
|
477
|
-
// from the set rather than left pointing at whatever now sits at that
|
|
478
|
-
// row. `cursor`'s "leave the stale number, let `renderCanvas` clamp it"
|
|
479
|
-
// is the wrong instinct here for the reason `pendingFrom`'s own comment
|
|
480
|
-
// gives — this mark feeds a structural op (`groupLanes`) the moment the
|
|
481
|
-
// button is pressed, not a screen position, and a stale member silently
|
|
482
|
-
// renumbering onto an unrelated lane would group a lane the user never
|
|
483
|
-
// selected with nothing on screen saying so.
|
|
484
|
-
let laneMultiSelect = null;
|
|
485
438
|
// The document the marks' lane numbers are numbers INTO.
|
|
486
439
|
//
|
|
487
440
|
// `cursor` and `selection` name a ROW, and a row number means a different
|
|
@@ -739,6 +692,114 @@
|
|
|
739
692
|
next.focus();
|
|
740
693
|
return;
|
|
741
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
|
+
}
|
|
742
803
|
// The drawing's own keys, claimed here rather than on the canvas element
|
|
743
804
|
// so they are decided in the same place — and in the same phase — as
|
|
744
805
|
// Escape, Tab and undo/redo. Deliberately BELOW those three (a cell
|
|
@@ -768,14 +829,14 @@
|
|
|
768
829
|
ev.preventDefault();
|
|
769
830
|
ev.stopPropagation();
|
|
770
831
|
if (store.undo()) afterStoreMoved('undo');
|
|
771
|
-
else say('沒有可以復原的動作');
|
|
832
|
+
else panels.say('沒有可以復原的動作');
|
|
772
833
|
return;
|
|
773
834
|
}
|
|
774
835
|
if (key === 'y' || (key === 'z' && ev.shiftKey)) {
|
|
775
836
|
ev.preventDefault();
|
|
776
837
|
ev.stopPropagation();
|
|
777
838
|
if (store.redo()) afterStoreMoved('redo');
|
|
778
|
-
else say('沒有可以重做的動作');
|
|
839
|
+
else panels.say('沒有可以重做的動作');
|
|
779
840
|
}
|
|
780
841
|
}
|
|
781
842
|
d.addEventListener('keydown', onKeyDown, true);
|
|
@@ -859,13 +920,101 @@
|
|
|
859
920
|
marksDoc = store.doc;
|
|
860
921
|
|
|
861
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.
|
|
862
928
|
const toolbar = d.createElement('div');
|
|
863
|
-
toolbar.className = 'ed-wave-toolbar';
|
|
929
|
+
toolbar.className = 'ed-wave-toolbar ed-wave-tools';
|
|
864
930
|
panel.appendChild(toolbar);
|
|
865
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);
|
|
866
1015
|
const brushBar = d.createElement('div');
|
|
867
1016
|
brushBar.className = 'ed-wave-brushes';
|
|
868
|
-
|
|
1017
|
+
brushGroup.appendChild(brushBar);
|
|
869
1018
|
const brushButtons = [];
|
|
870
1019
|
for (const ch of BRUSHES) {
|
|
871
1020
|
const b = d.createElement('button');
|
|
@@ -873,7 +1022,21 @@
|
|
|
873
1022
|
b.className = 'ed-wave-brush';
|
|
874
1023
|
b.setAttribute('data-brush', ch);
|
|
875
1024
|
b.setAttribute('data-focus-key', 'brush-' + ch);
|
|
876
|
-
|
|
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);
|
|
877
1040
|
b.addEventListener('click', function () {
|
|
878
1041
|
brush = ch;
|
|
879
1042
|
paintBrushState();
|
|
@@ -881,6 +1044,76 @@
|
|
|
881
1044
|
brushBar.appendChild(b);
|
|
882
1045
|
brushButtons.push(b);
|
|
883
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
|
+
|
|
884
1117
|
function paintBrushState() {
|
|
885
1118
|
for (const b of brushButtons) {
|
|
886
1119
|
b.classList.toggle('is-on', b.getAttribute('data-brush') === brush);
|
|
@@ -888,162 +1121,608 @@
|
|
|
888
1121
|
overlay.setAttribute('data-wave-brush', brush);
|
|
889
1122
|
}
|
|
890
1123
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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);
|
|
894
1142
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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);
|
|
905
1168
|
}
|
|
906
1169
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
}
|
|
1189
|
+
|
|
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);
|
|
932
1258
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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);
|
|
938
1319
|
});
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
afterStoreMoved
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
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.
|
|
958
1377
|
//
|
|
959
1378
|
// The group is created with an EMPTY label and the rename field is
|
|
960
|
-
// opened immediately — the same affordance
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
const groupBtn = toolButton('ed-wave-group-make', '建立群組',
|
|
964
|
-
'
|
|
965
|
-
|
|
966
|
-
|
|
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,才有東西可以群組');
|
|
967
1387
|
return;
|
|
968
1388
|
}
|
|
969
|
-
const ats =
|
|
1389
|
+
const ats = [];
|
|
1390
|
+
for (let k = selection.laneIndex; k <= selection.laneTo; k += 1) ats.push(k);
|
|
970
1391
|
const minAt = ats[0];
|
|
971
1392
|
const committed = commit('group-lanes', function (doc2) {
|
|
972
1393
|
return codec.groupLanes(doc2, ats, '');
|
|
973
1394
|
});
|
|
974
1395
|
if (!committed) {
|
|
975
|
-
say('這幾條 lane 不連續(或選取已經失效),WaveJSON 的群組只能是連續的一段');
|
|
1396
|
+
panels.say('這幾條 lane 不連續(或選取已經失效),WaveJSON 的群組只能是連續的一段');
|
|
976
1397
|
return;
|
|
977
1398
|
}
|
|
978
|
-
laneMultiSelect = null;
|
|
979
1399
|
// The commit above already repainted with the (now stale) highlight
|
|
980
1400
|
// showing; one more repaint clears it before handing the keyboard to
|
|
981
1401
|
// the rename field, the same two-step `renameGroup` itself already
|
|
982
1402
|
// is — a rebuild first, then the sub-panel laid over what it built.
|
|
1403
|
+
selection = null;
|
|
983
1404
|
render();
|
|
984
1405
|
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
985
|
-
const spans = groupSpansOf(store.doc, layout);
|
|
1406
|
+
const spans = geometry.groupSpansOf(store.doc, layout);
|
|
986
1407
|
for (const span of spans) {
|
|
987
|
-
if (span.from === minAt) { renameGroup(span); break; }
|
|
1408
|
+
if (span.from === minAt) { panels.renameGroup(span); break; }
|
|
988
1409
|
}
|
|
989
|
-
});
|
|
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);
|
|
990
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);
|
|
991
1444
|
// v3.5.0 Task 7: arm the edge-creation gesture. One-shot in the sense
|
|
992
1445
|
// that a drag which PRODUCES an edge disarms it (`finishEdgeDrag` below);
|
|
993
1446
|
// pressing the button itself is a plain toggle, so a change of mind
|
|
994
1447
|
// before ever pressing on the canvas costs one more click, not Escape.
|
|
995
|
-
const edgeBtn = toolButton('ed-wave-edge-arm', '關聯線',
|
|
1448
|
+
const edgeBtn = panels.toolButton('ed-wave-edge-arm', '關聯線',
|
|
996
1449
|
'按一下武裝,然後從一格拖到另一格畫出關聯線(Escape 取消)', function () {
|
|
997
1450
|
setEdgeMode(edgeMode === 'idle' ? 'armed' : 'idle');
|
|
998
|
-
|
|
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);
|
|
999
1459
|
setEdgeMode('idle');
|
|
1000
1460
|
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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; }
|
|
1466
|
+
afterStoreMoved('undo');
|
|
1467
|
+
}, historyGroup);
|
|
1468
|
+
const redoBtn = panels.toolButton('ed-wave-redo', '重做', '再做一次剛剛復原掉的動作', function () {
|
|
1469
|
+
if (!store.redo()) { panels.say('沒有可以重做的動作'); return; }
|
|
1470
|
+
afterStoreMoved('redo');
|
|
1471
|
+
}, historyGroup);
|
|
1472
|
+
|
|
1473
|
+
// ── 匯出 ──────────────────────────────────────────────────────────────
|
|
1008
1474
|
//
|
|
1009
|
-
//
|
|
1010
|
-
// {wave:'0101', period:2} engine 16 drawing 4, unchanged
|
|
1011
|
-
// {wave:'0101', phase:0.5} engine 7 drawing 4, unshifted
|
|
1012
|
-
// {wave:'0101'} config.hscale:2 engine 16 drawing 4, unchanged
|
|
1475
|
+
// v3.6.0 Task 15: the three buttons Task 12 created as shape.
|
|
1013
1476
|
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
// a half-model here would be a SECOND belief about cycle geometry in the
|
|
1020
|
-
// 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.
|
|
1021
1482
|
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
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
|
+
}
|
|
1554
|
+
|
|
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('這個瀏覽器不給用剪貼簿,可以從右欄的原始碼面板手動選取');
|
|
1043
1608
|
}
|
|
1044
1609
|
|
|
1045
|
-
|
|
1046
|
-
|
|
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);
|
|
1047
1726
|
// v3.5.0 Task 13, fix round 1: the ruler fields — the COMPLETE set, not
|
|
1048
1727
|
// the three Task 13's own brief happened to name. Measured directly
|
|
1049
1728
|
// against the pinned engine rather than recalled: `renderMarks` in
|
|
@@ -1072,27 +1751,74 @@
|
|
|
1072
1751
|
// bare, since those never had a same-named sibling to begin with.
|
|
1073
1752
|
// Grouped head-then-foot in DOM order (four fields, then four fields)
|
|
1074
1753
|
// rather than the first round's ad hoc growth order, for the same
|
|
1075
|
-
// readability reason.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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); });
|
|
1086
1786
|
headTockInput.addEventListener('change', function () {
|
|
1087
|
-
commitBanner('head', 'tock', headTockInput.value);
|
|
1787
|
+
panels.commitBanner('head', 'tock', headTockInput.value);
|
|
1088
1788
|
});
|
|
1089
|
-
headEveryInput.addEventListener('change', function () { commitBanner('head', 'every', headEveryInput.value); });
|
|
1789
|
+
headEveryInput.addEventListener('change', function () { panels.commitBanner('head', 'every', headEveryInput.value); });
|
|
1090
1790
|
footTickInput.addEventListener('change', function () {
|
|
1091
|
-
commitBanner('foot', 'tick', footTickInput.value);
|
|
1791
|
+
panels.commitBanner('foot', 'tick', footTickInput.value);
|
|
1092
1792
|
});
|
|
1093
|
-
footTockInput.addEventListener('change', function () { commitBanner('foot', 'tock', footTockInput.value); });
|
|
1793
|
+
footTockInput.addEventListener('change', function () { panels.commitBanner('foot', 'tock', footTockInput.value); });
|
|
1094
1794
|
footEveryInput.addEventListener('change', function () {
|
|
1095
|
-
commitBanner('foot', 'every', footEveryInput.value);
|
|
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);
|
|
1096
1822
|
});
|
|
1097
1823
|
|
|
1098
1824
|
// ── the edge inspector row ──────────────────────────────────────────
|
|
@@ -1107,7 +1833,10 @@
|
|
|
1107
1833
|
const edgeBar = d.createElement('div');
|
|
1108
1834
|
edgeBar.className = 'ed-wave-edgebar';
|
|
1109
1835
|
edgeBar.hidden = true;
|
|
1110
|
-
toolbar
|
|
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);
|
|
1111
1840
|
|
|
1112
1841
|
const shapeWrap = d.createElement('label');
|
|
1113
1842
|
shapeWrap.className = 'ed-wave-metafield';
|
|
@@ -1177,6 +1906,11 @@
|
|
|
1177
1906
|
});
|
|
1178
1907
|
});
|
|
1179
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;
|
|
1180
1914
|
|
|
1181
1915
|
// The "this drawing is not showing you everything" notice. Empty, and with
|
|
1182
1916
|
// no box on screen, for the documents that carry none of the three.
|
|
@@ -1193,38 +1927,18 @@
|
|
|
1193
1927
|
noSignal.className = 'ed-wave-unmodelled ed-wave-nolanes';
|
|
1194
1928
|
noSignal.hidden = true;
|
|
1195
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;
|
|
1196
1934
|
|
|
1197
|
-
//
|
|
1198
|
-
// — to `(which, key, value)` so the same function also carries the other
|
|
1199
|
-
// five ruler fields.
|
|
1200
|
-
//
|
|
1201
|
-
// Fix round 2: the write itself moved into `codec.setBannerField` rather
|
|
1202
|
-
// than staying inline here. The inline version's own no-op guard —
|
|
1203
|
-
// `value === '' && (hadObj[key] === undefined || hadObj[key] === null)`
|
|
1204
|
-
// — only refused to WRITE when the key had never been set; once a key
|
|
1205
|
-
// WAS set, clearing the field fell through to `next[which][key] = ''`,
|
|
1206
|
-
// an empty string rather than an absent key. `.text` hid that: the
|
|
1207
|
-
// engine's `captext` treats `''` the same as absent (a plain truthy
|
|
1208
|
-
// check). `tick`/`tock`/`every` do not — `ticktock` in
|
|
1209
|
-
// `node_modules/wavedrom/lib/render-marks.js` only skips a field that is
|
|
1210
|
-
// `=== undefined`, so `''` reached its array branch, `Number('')` is
|
|
1211
|
-
// `0`, and the engine drew a full default 0-based ruler on a field that
|
|
1212
|
-
// LOOKED empty on screen — the exact confident-wrong-state class this
|
|
1213
|
-
// file exists to avoid, reintroduced by the write side after the read
|
|
1214
|
-
// side (`tickTockText`) got it right. `setBannerField` deletes the key
|
|
1215
|
-
// (and the parent object, if that empties it) instead of writing `''`,
|
|
1216
|
-
// closing that gap once for all six fields rather than special-casing
|
|
1217
|
-
// `.text`'s accidental safety.
|
|
1935
|
+
// ── body: lane list | drawing | right-hand column ──────────────────────
|
|
1218
1936
|
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
// `
|
|
1222
|
-
// `
|
|
1223
|
-
|
|
1224
|
-
commit(which + '.' + key, function (doc) { return codec.setBannerField(doc, which, key, value); });
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// ── body: lane list | drawing | preview ───────────────────────────────
|
|
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`.
|
|
1228
1942
|
const body = d.createElement('div');
|
|
1229
1943
|
body.className = 'ed-wave-body';
|
|
1230
1944
|
panel.appendChild(body);
|
|
@@ -1232,6 +1946,9 @@
|
|
|
1232
1946
|
const laneCol = d.createElement('div');
|
|
1233
1947
|
laneCol.className = 'ed-wave-lanes';
|
|
1234
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;
|
|
1235
1952
|
|
|
1236
1953
|
const canvasWrap = d.createElement('div');
|
|
1237
1954
|
canvasWrap.className = 'ed-wave-canvas-wrap';
|
|
@@ -1251,7 +1968,15 @@
|
|
|
1251
1968
|
// This preview belongs to neither.
|
|
1252
1969
|
previewHost.id = PREVIEW_ID_PREFIX + PREVIEW_INDEX;
|
|
1253
1970
|
previewWrap.appendChild(previewHost);
|
|
1254
|
-
|
|
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);
|
|
1255
1980
|
|
|
1256
1981
|
const status = d.createElement('div');
|
|
1257
1982
|
status.className = 'ed-wave-status';
|
|
@@ -1260,11 +1985,9 @@
|
|
|
1260
1985
|
// the one place that has to be announced rather than merely painted.
|
|
1261
1986
|
status.setAttribute('role', 'status');
|
|
1262
1987
|
panel.appendChild(status);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
overlay.setAttribute('data-wave-status', text);
|
|
1267
|
-
}
|
|
1988
|
+
// Published for `panels.say`'s lazy read, same reason as `deps.metaBar`
|
|
1989
|
+
// above.
|
|
1990
|
+
deps.status = status;
|
|
1268
1991
|
|
|
1269
1992
|
// ── the one road from a gesture to the document ───────────────────────
|
|
1270
1993
|
/**
|
|
@@ -1289,7 +2012,7 @@
|
|
|
1289
2012
|
if (closed) return false;
|
|
1290
2013
|
const changed = store.apply(name, fn);
|
|
1291
2014
|
if (!changed) {
|
|
1292
|
-
say('這個動作沒有改變任何東西');
|
|
2015
|
+
panels.say('這個動作沒有改變任何東西');
|
|
1293
2016
|
return false;
|
|
1294
2017
|
}
|
|
1295
2018
|
gestures++;
|
|
@@ -1349,13 +2072,12 @@
|
|
|
1349
2072
|
// dragging an endpoint is exactly the same shape of gap `pendingFrom`
|
|
1350
2073
|
// closed — the pointer holds no document capture of its own, only
|
|
1351
2074
|
// this variable's `at` does.
|
|
1352
|
-
// v3.
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
// just with a SET of row numbers instead of one.
|
|
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.
|
|
1356
2078
|
if (before === null ||
|
|
1357
2079
|
(cursor === null && selection === null && pendingFrom === null &&
|
|
1358
|
-
endpointDrag === null
|
|
2080
|
+
endpointDrag === null)) {
|
|
1359
2081
|
return;
|
|
1360
2082
|
}
|
|
1361
2083
|
const trace = typeof opts.laneTrace === 'function'
|
|
@@ -1364,12 +2086,9 @@
|
|
|
1364
2086
|
// to `pendingFrom` exactly as it already does to `cursor`/`selection`
|
|
1365
2087
|
// — dropped, not guessed at. `endpointDrag` joins them: a drag naming
|
|
1366
2088
|
// a lane nothing here can still vouch for is the same hazard either
|
|
1367
|
-
// way.
|
|
1368
|
-
// every other mark: with no trace at all there is nothing to check
|
|
1369
|
-
// any of its members against.
|
|
2089
|
+
// way.
|
|
1370
2090
|
if (trace === null) {
|
|
1371
2091
|
cursor = null; selection = null; pendingFrom = null; endpointDrag = null;
|
|
1372
|
-
laneMultiSelect = null;
|
|
1373
2092
|
return;
|
|
1374
2093
|
}
|
|
1375
2094
|
// Structural or not: a content operation traces as the identity, so this
|
|
@@ -1385,10 +2104,41 @@
|
|
|
1385
2104
|
if (at !== null) cursor = { laneIndex: at, cycle: cursor.cycle };
|
|
1386
2105
|
}
|
|
1387
2106
|
if (selection !== null) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
+
};
|
|
1392
2142
|
}
|
|
1393
2143
|
// `pendingFrom` is a mark of the exact same shape as `cursor` (a row
|
|
1394
2144
|
// number plus a cycle), carried the same way — by lane IDENTITY
|
|
@@ -1428,26 +2178,6 @@
|
|
|
1428
2178
|
endpointDrag = at === null ? null
|
|
1429
2179
|
: { index: endpointDrag.index, end: endpointDrag.end, at: at, cell: endpointDrag.cell };
|
|
1430
2180
|
}
|
|
1431
|
-
// `laneMultiSelect` is a SET of marks of the same shape as
|
|
1432
|
-
// `pendingFrom.at` — each one a lane the user has already committed to
|
|
1433
|
-
// for the group they are about to build — carried and dropped the same
|
|
1434
|
-
// way, member by member rather than all-or-nothing: a reorder that
|
|
1435
|
-
// only invalidates ONE of several selected rows should not throw away
|
|
1436
|
-
// the rest of a selection the user is still building. An entry that
|
|
1437
|
-
// cannot be traced is removed rather than left on a stale number, for
|
|
1438
|
-
// the identical reason `pendingFrom` never keeps one either — a
|
|
1439
|
-
// structural op reads this set directly, not a screen position
|
|
1440
|
-
// `renderCanvas` re-clamps on every repaint. Emptied down to nothing,
|
|
1441
|
-
// it collapses to `null` so every other guard's plain `!== null` check
|
|
1442
|
-
// stays the whole truth about "is anything still selected".
|
|
1443
|
-
if (laneMultiSelect !== null) {
|
|
1444
|
-
const kept = [];
|
|
1445
|
-
for (let m = 0; m < laneMultiSelect.length; m++) {
|
|
1446
|
-
const at = rowOf(laneMultiSelect[m]);
|
|
1447
|
-
if (at !== null) kept.push(at);
|
|
1448
|
-
}
|
|
1449
|
-
laneMultiSelect = kept.length === 0 ? null : kept;
|
|
1450
|
-
}
|
|
1451
2181
|
}
|
|
1452
2182
|
|
|
1453
2183
|
function afterStoreMoved(name) {
|
|
@@ -1459,9 +2189,9 @@
|
|
|
1459
2189
|
lastPatch = store.toPatch();
|
|
1460
2190
|
render();
|
|
1461
2191
|
if (lastPatch.ok === true) {
|
|
1462
|
-
say('已寫回:' + name);
|
|
2192
|
+
panels.say('已寫回:' + name);
|
|
1463
2193
|
} else {
|
|
1464
|
-
say('這個改動沒辦法只改幾個位元組寫回去:' + lastPatch.reason);
|
|
2194
|
+
panels.say('這個改動沒辦法只改幾個位元組寫回去:' + lastPatch.reason);
|
|
1465
2195
|
}
|
|
1466
2196
|
if (typeof opts.onGesture === 'function') {
|
|
1467
2197
|
opts.onGesture({ name: name, patch: lastPatch, store: store });
|
|
@@ -1471,6 +2201,192 @@
|
|
|
1471
2201
|
// ── painting ──────────────────────────────────────────────────────────
|
|
1472
2202
|
let canvas = null;
|
|
1473
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
|
+
|
|
1474
2390
|
/**
|
|
1475
2391
|
* Repaint everything, and give the keyboard cursor back where it was.
|
|
1476
2392
|
*
|
|
@@ -1524,37 +2440,79 @@
|
|
|
1524
2440
|
if (endpointDrag !== null && endpointDrag.index !== selectedEdge) {
|
|
1525
2441
|
endpointDrag = null;
|
|
1526
2442
|
}
|
|
1527
|
-
|
|
1528
|
-
//
|
|
1529
|
-
// `
|
|
1530
|
-
//
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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();
|
|
1551
2513
|
undoBtn.disabled = !store.canUndo();
|
|
1552
2514
|
redoBtn.disabled = !store.canRedo();
|
|
1553
|
-
|
|
1554
|
-
overlay.setAttribute('data-wave-multiselect',
|
|
1555
|
-
laneMultiSelect === null ? '' : laneMultiSelect.slice().sort(function (a, b) {
|
|
1556
|
-
return a - b;
|
|
1557
|
-
}).join(','));
|
|
2515
|
+
paintSelectionState(doc, layout);
|
|
1558
2516
|
overlay.setAttribute('data-wave-gestures', String(gestures));
|
|
1559
2517
|
overlay.setAttribute('data-wave-lanes', String(layout.lanes.length));
|
|
1560
2518
|
overlay.setAttribute('data-wave-cycles', String(layout.cycles));
|
|
@@ -1575,28 +2533,6 @@
|
|
|
1575
2533
|
return false;
|
|
1576
2534
|
}
|
|
1577
2535
|
|
|
1578
|
-
/**
|
|
1579
|
-
* Show or hide the inspector row, and — when it is showing — fill it in
|
|
1580
|
-
* from the selected edge's own entry.
|
|
1581
|
-
*
|
|
1582
|
-
* The ONLY thing that may set `edgeBar.hidden`, so "is an edge selected"
|
|
1583
|
-
* and "is there a box on screen" cannot come apart: precondition T1 at
|
|
1584
|
-
* the top of this file's docstring for the overlay itself, applied to
|
|
1585
|
-
* this row. Called only from `render()`, after `selectedEdge` has already
|
|
1586
|
-
* been clamped, so the `codec.parseEdge` below is not expected to fail —
|
|
1587
|
-
* it is checked anyway because "expected to" is not "guaranteed to".
|
|
1588
|
-
*/
|
|
1589
|
-
function renderEdgeBar(doc) {
|
|
1590
|
-
overlay.setAttribute('data-wave-selected-edge',
|
|
1591
|
-
selectedEdge === null ? '' : String(selectedEdge));
|
|
1592
|
-
edgeBar.hidden = selectedEdge === null;
|
|
1593
|
-
if (selectedEdge === null) return;
|
|
1594
|
-
const cur = codec.parseEdge((doc.edge || [])[selectedEdge]);
|
|
1595
|
-
if (cur === null) { edgeBar.hidden = true; return; }
|
|
1596
|
-
shapeSel.value = cur.shape;
|
|
1597
|
-
edgeLabelInput.value = cur.label;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
2536
|
/** What has the keyboard now, as something that survives a rebuild. */
|
|
1601
2537
|
function focusMark() {
|
|
1602
2538
|
const el = d.activeElement;
|
|
@@ -1618,6 +2554,16 @@
|
|
|
1618
2554
|
const want = focusOverride !== null ? focusOverride : (mark === null ? null : mark.key);
|
|
1619
2555
|
focusOverride = null;
|
|
1620
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; };
|
|
1621
2567
|
let el = overlay.querySelector('[data-focus-key="' + want + '"]');
|
|
1622
2568
|
// A named destination the repaint did not bring back. It happens: a
|
|
1623
2569
|
// group's key is its first lane's row number, so inserting a lane above
|
|
@@ -1626,10 +2572,10 @@
|
|
|
1626
2572
|
// where the keyboard actually was, and failing that to the dialog
|
|
1627
2573
|
// itself. Never to nothing: nothing is `document.body`, and this whole
|
|
1628
2574
|
// task is about not ending there.
|
|
1629
|
-
if (el
|
|
2575
|
+
if (inert(el) && mark !== null && mark.key !== want) {
|
|
1630
2576
|
el = overlay.querySelector('[data-focus-key="' + mark.key + '"]');
|
|
1631
2577
|
}
|
|
1632
|
-
if (el
|
|
2578
|
+
if (inert(el)) {
|
|
1633
2579
|
if (!overlay.contains(d.activeElement)) panel.focus();
|
|
1634
2580
|
return;
|
|
1635
2581
|
}
|
|
@@ -1642,602 +2588,6 @@
|
|
|
1642
2588
|
} catch (e) { /* not a text field */ }
|
|
1643
2589
|
}
|
|
1644
2590
|
|
|
1645
|
-
/**
|
|
1646
|
-
* `tick`/`every`/`tock` are read by the pinned engine as a number, a
|
|
1647
|
-
* string, or an array of either (`ticktock` in
|
|
1648
|
-
* `node_modules/wavedrom/lib/render-marks.js`) — unlike `head`/`foot`'s
|
|
1649
|
-
* own `.text`, which is always a plain string. A text `<input>` can only
|
|
1650
|
-
* hold one shape, so a hand-authored numeric or array value is shown in
|
|
1651
|
-
* its string form (an array space-joined, the same shape the engine's
|
|
1652
|
-
* own `val.trim().split(/\s+/)` turns a plain string back into) rather
|
|
1653
|
-
* than blanked out — blanking a field that actually holds `tick: 0`
|
|
1654
|
-
* would show a confident wrong state, the same failure `renderUnmodelled`
|
|
1655
|
-
* exists to avoid elsewhere in this file. Editing the field always
|
|
1656
|
-
* commits a plain string back, which the engine treats identically to
|
|
1657
|
-
* whichever shape it replaced.
|
|
1658
|
-
*/
|
|
1659
|
-
function tickTockText(v) {
|
|
1660
|
-
if (typeof v === 'string') return v;
|
|
1661
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
1662
|
-
if (Array.isArray(v)) return v.join(' ');
|
|
1663
|
-
return '';
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
function renderMeta(doc) {
|
|
1667
|
-
const h = doc.head;
|
|
1668
|
-
const f = doc.foot;
|
|
1669
|
-
headInput.value = (h !== null && typeof h === 'object' && typeof h.text === 'string')
|
|
1670
|
-
? h.text : '';
|
|
1671
|
-
footInput.value = (f !== null && typeof f === 'object' && typeof f.text === 'string')
|
|
1672
|
-
? f.text : '';
|
|
1673
|
-
headTickInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tick) : '';
|
|
1674
|
-
headEveryInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.every) : '';
|
|
1675
|
-
footTockInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tock) : '';
|
|
1676
|
-
headTockInput.value = (h !== null && typeof h === 'object') ? tickTockText(h.tock) : '';
|
|
1677
|
-
footTickInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.tick) : '';
|
|
1678
|
-
footEveryInput.value = (f !== null && typeof f === 'object') ? tickTockText(f.every) : '';
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
/**
|
|
1682
|
-
* Say out loud which properties of this document the drawing is not
|
|
1683
|
-
* showing.
|
|
1684
|
-
*
|
|
1685
|
-
* `config.hscale`, and a lane's `period` / `phase`, all change what the
|
|
1686
|
-
* engine paints and none of them is modelled here (see the toolbar comment
|
|
1687
|
-
* for the measurements). Drawing a confident picture that ignores them is
|
|
1688
|
-
* the failure this whole overlay is arranged to avoid, so a document that
|
|
1689
|
-
* carries any of them says so, names them, and points at the preview —
|
|
1690
|
-
* which IS faithful, because it is the engine.
|
|
1691
|
-
*
|
|
1692
|
-
* Edges join this same notice for the opposite reason. As of this task
|
|
1693
|
-
* they ARE drawn — `renderEdges` below — so the claim for them is not "the
|
|
1694
|
-
* drawing ignores this property" but "this specific edge cannot be
|
|
1695
|
-
* placed".
|
|
1696
|
-
*
|
|
1697
|
-
* final review finding 3: this used to re-derive only ONE of
|
|
1698
|
-
* `geometry.edgeLayout`'s three skip reasons (the dangling-letter one) by
|
|
1699
|
-
* walking `doc.edge` a second time with its own copy of that check —
|
|
1700
|
-
* which is also the one case the pinned engine agrees with us on
|
|
1701
|
-
* (measured: an edge naming an absent letter draws nothing there either).
|
|
1702
|
-
* The other two — an unparseable shape, and a `node` index that has
|
|
1703
|
-
* outlived its lane's length (reachable from the editor's own
|
|
1704
|
-
* insert/delete-cycles buttons, since neither reindexes `node`) — are
|
|
1705
|
-
* exactly where the engine DOES draw a line and we silently do not,
|
|
1706
|
-
* which is the one divergence this whole overlay exists to prevent, and
|
|
1707
|
-
* the old code said nothing about either.
|
|
1708
|
-
*
|
|
1709
|
-
* So this asks `geometry.edgeLayoutDetail` — the SAME walk `renderEdges`
|
|
1710
|
-
* draws from — which edges it could not place and why, instead of
|
|
1711
|
-
* re-deriving any check of its own. Skips are bucketed by whatever
|
|
1712
|
-
* `reason` string comes back, not by a hardcoded list of the two or three
|
|
1713
|
-
* reasons known today: a fourth `continue` added to `edgeLayoutDetail`
|
|
1714
|
-
* later shows up here as its own bucket (worded generically if
|
|
1715
|
-
* `REASON_TEXT` has not been taught a nicer sentence for it yet) with no
|
|
1716
|
-
* change needed in this function, rather than escaping the notice the way
|
|
1717
|
-
* the third one did until now.
|
|
1718
|
-
*/
|
|
1719
|
-
function renderUnmodelled(doc, layout) {
|
|
1720
|
-
const found = [];
|
|
1721
|
-
const cfg = doc.config;
|
|
1722
|
-
if (cfg !== null && typeof cfg === 'object' && cfg.hscale !== undefined) {
|
|
1723
|
-
found.push('config.hscale');
|
|
1724
|
-
}
|
|
1725
|
-
for (const path of codec.lanePaths(doc)) {
|
|
1726
|
-
const lane = valueAt(doc, path);
|
|
1727
|
-
if (lane === null || typeof lane !== 'object') continue;
|
|
1728
|
-
if (lane.period !== undefined && found.indexOf('period') === -1) found.push('period');
|
|
1729
|
-
if (lane.phase !== undefined && found.indexOf('phase') === -1) found.push('phase');
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
const detail = geometry.edgeLayoutDetail(doc, layout);
|
|
1733
|
-
// Bucket every skip by its own reported `reason` — discovered from what
|
|
1734
|
-
// came back, not enumerated in advance, so nothing here has to change
|
|
1735
|
-
// for a reason this function has never heard of.
|
|
1736
|
-
const buckets = new Map();
|
|
1737
|
-
for (const skip of detail.skipped) {
|
|
1738
|
-
if (!buckets.has(skip.reason)) buckets.set(skip.reason, []);
|
|
1739
|
-
buckets.get(skip.reason).push(skip);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
function letters(list) {
|
|
1743
|
-
const out = [];
|
|
1744
|
-
for (const skip of list) {
|
|
1745
|
-
if (skip.edge === null) continue;
|
|
1746
|
-
if (out.indexOf(skip.edge.from) === -1) out.push(skip.edge.from);
|
|
1747
|
-
if (out.indexOf(skip.edge.to) === -1) out.push(skip.edge.to);
|
|
1748
|
-
}
|
|
1749
|
-
return out;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
// Known reasons get a sentence that says what a user can act on.
|
|
1753
|
-
// `dangling` matches the engine (nothing to reconcile against the
|
|
1754
|
-
// preview); `unparseable` and `out-of-range` are the two the engine
|
|
1755
|
-
// draws and we do not, so both point at the preview as the tie-breaker.
|
|
1756
|
-
const REASON_TEXT = {
|
|
1757
|
-
dangling: function (list) {
|
|
1758
|
-
return '這個區塊有 ' + list.length + ' 條關聯線引用了不存在的 anchor(' +
|
|
1759
|
-
letters(list).join('、') + '),左邊畫不出它們。';
|
|
1760
|
-
},
|
|
1761
|
-
unparseable: function (list) {
|
|
1762
|
-
return '這個區塊有 ' + list.length +
|
|
1763
|
-
' 條關聯線的寫法連引擎也認不得,左邊畫不出它們(跟右邊的 WaveDrom 預覽一致)。';
|
|
1764
|
-
},
|
|
1765
|
-
'out-of-range': function (list) {
|
|
1766
|
-
return '這個區塊有 ' + list.length + ' 條關聯線的 anchor(' +
|
|
1767
|
-
letters(list).join('、') + ')落在那條 lane 目前的長度之外,左邊畫不出它們' +
|
|
1768
|
-
'——但右邊的 WaveDrom 預覽仍然會畫,兩邊會不一致,以預覽為準。';
|
|
1769
|
-
},
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
const sentences = [];
|
|
1773
|
-
if (found.length > 0) {
|
|
1774
|
-
sentences.push('這份文件用了 ' + found.join('、') +
|
|
1775
|
-
',左邊的手繪波形不表現它們(寬度與相位會不一樣)——以右邊的 WaveDrom 預覽為準。');
|
|
1776
|
-
}
|
|
1777
|
-
for (const [reason, list] of buckets) {
|
|
1778
|
-
const text = REASON_TEXT[reason];
|
|
1779
|
-
sentences.push(text ? text(list) :
|
|
1780
|
-
'這個區塊有 ' + list.length + ' 條關聯線因為「' + reason + '」畫不出來,' +
|
|
1781
|
-
'以右邊的 WaveDrom 預覽為準。');
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
const danglingCount = buckets.has('dangling') ? buckets.get('dangling').length : 0;
|
|
1785
|
-
overlay.setAttribute('data-wave-unmodelled', found.join(' '));
|
|
1786
|
-
overlay.setAttribute('data-wave-dangling-edges', String(danglingCount));
|
|
1787
|
-
// The total the dangling-only count above cannot say: every edge entry
|
|
1788
|
-
// `edgeLayoutDetail` could not place, for any reason at all.
|
|
1789
|
-
overlay.setAttribute('data-wave-skipped-edges', String(detail.skipped.length));
|
|
1790
|
-
unmodelled.hidden = sentences.length === 0;
|
|
1791
|
-
unmodelled.textContent = sentences.join(' ');
|
|
1792
|
-
|
|
1793
|
-
// …and the bigger version of the same honesty. This editor edits
|
|
1794
|
-
// `signal:` and nothing else, so a `reg:` or `assign:` block opens a
|
|
1795
|
-
// canvas with no lanes on it: `lanePaths()` is empty, `addLane()` hands
|
|
1796
|
-
// the document straight back, and pressing + produced one status line
|
|
1797
|
-
//「這個動作沒有改變任何東西」— true, and no explanation at all. An
|
|
1798
|
-
// editor that looks broken is worse than one that says what it cannot
|
|
1799
|
-
// do, and the notice above is the precedent: the drawing already tells
|
|
1800
|
-
// the user which properties it is not showing them.
|
|
1801
|
-
const kinds = [];
|
|
1802
|
-
for (const key of ['reg', 'assign']) {
|
|
1803
|
-
if (doc[key] !== undefined) kinds.push(key + ':');
|
|
1804
|
-
}
|
|
1805
|
-
const noLanes = codec.lanePaths(doc).length === 0;
|
|
1806
|
-
overlay.setAttribute('data-wave-nolanes', noLanes ? '1' : '0');
|
|
1807
|
-
noSignal.hidden = !noLanes;
|
|
1808
|
-
noSignal.textContent = !noLanes ? '' :
|
|
1809
|
-
(kinds.length === 0
|
|
1810
|
-
? '這個區塊裡沒有 signal:,所以沒有可以編輯的 lane。'
|
|
1811
|
-
: '這個區塊用的是 ' + kinds.join(' / ') +
|
|
1812
|
-
',這個編輯器只編輯 signal: 的 lane。') +
|
|
1813
|
-
'右邊的 WaveDrom 預覽仍然是引擎畫的原圖;要改這個區塊請用「⠿ → MD 原始碼」。';
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
/**
|
|
1817
|
-
* The lane list.
|
|
1818
|
-
*
|
|
1819
|
-
* Rows are `layout.laneHeight` tall and in `layout.lanes` order, so row k
|
|
1820
|
-
* here and row k of the drawing are the same lane by construction rather
|
|
1821
|
-
* than by a second traversal that could disagree.
|
|
1822
|
-
*/
|
|
1823
|
-
function renderLanes(doc, layout) {
|
|
1824
|
-
laneCol.textContent = '';
|
|
1825
|
-
const rows = d.createElement('div');
|
|
1826
|
-
rows.className = 'ed-wave-lane-rows';
|
|
1827
|
-
rows.style.height = layout.height + 'px';
|
|
1828
|
-
laneCol.appendChild(rows);
|
|
1829
|
-
|
|
1830
|
-
const spans = groupSpansOf(doc, layout);
|
|
1831
|
-
// v3.5.0 Task 12: which row, if any, hosts THIS group's own 解散
|
|
1832
|
-
// button — see `directFirstLaneOf`'s comment for why at most one row
|
|
1833
|
-
// can, and why some groups (made entirely of sub-groups) have none.
|
|
1834
|
-
const ungroupAt = new Map();
|
|
1835
|
-
for (const span of spans) {
|
|
1836
|
-
const at = directFirstLaneOf(layout, span);
|
|
1837
|
-
if (at !== null) ungroupAt.set(at, span);
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// Group titles first, so they sit UNDER the rows in paint order and a
|
|
1841
|
-
// lane's own controls stay clickable over them.
|
|
1842
|
-
for (const span of spans) {
|
|
1843
|
-
const tag = d.createElement('button');
|
|
1844
|
-
tag.type = 'button';
|
|
1845
|
-
tag.className = 'ed-wave-group';
|
|
1846
|
-
tag.setAttribute('data-focus-key', 'group-' + span.from);
|
|
1847
|
-
tag.textContent = span.title;
|
|
1848
|
-
tag.title = '群組「' + span.title + '」——點一下改名';
|
|
1849
|
-
tag.setAttribute('data-group-from', String(span.from));
|
|
1850
|
-
tag.setAttribute('data-group-to', String(span.to));
|
|
1851
|
-
tag.style.top = (span.from * layout.laneHeight) + 'px';
|
|
1852
|
-
tag.style.height = ((span.to - span.from + 1) * layout.laneHeight) + 'px';
|
|
1853
|
-
tag.style.left = ((span.depth - 1) * 16) + 'px';
|
|
1854
|
-
tag.addEventListener('click', function () { renameGroup(span); });
|
|
1855
|
-
rows.appendChild(tag);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
1859
|
-
rows.appendChild(laneRow(doc, layout, i, ungroupAt.get(i)));
|
|
1860
|
-
}
|
|
1861
|
-
// The tail insert. `laneInsertPath(doc, laneCount)` goes on the end of
|
|
1862
|
-
// `signal` itself, which is what makes the group asymmetry visible here
|
|
1863
|
-
// rather than surprising afterwards.
|
|
1864
|
-
rows.appendChild(insertButton(doc, layout.lanes.length, layout, true));
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
/**
|
|
1868
|
-
* `ungroupSpan` is the group THIS row's 解散 button dissolves, or
|
|
1869
|
-
* `undefined` on every row that is not a group's own direct-first-lane —
|
|
1870
|
-
* `renderLanes` hands it over from the one `directFirstLaneOf` pass it
|
|
1871
|
-
* already made rather than this function re-deriving it per row.
|
|
1872
|
-
*/
|
|
1873
|
-
function laneRow(doc, layout, i, ungroupSpan) {
|
|
1874
|
-
const row = d.createElement('div');
|
|
1875
|
-
row.className = 'ed-wave-lane-row';
|
|
1876
|
-
row.setAttribute('data-lane', String(i));
|
|
1877
|
-
row.style.top = (i * layout.laneHeight) + 'px';
|
|
1878
|
-
row.style.height = layout.laneHeight + 'px';
|
|
1879
|
-
row.style.paddingLeft = (layout.lanes[i].depth * 16) + 'px';
|
|
1880
|
-
if (selection !== null && selection.laneIndex === i) row.classList.add('is-selected');
|
|
1881
|
-
if (laneMultiSelect !== null && laneMultiSelect.indexOf(i) !== -1) {
|
|
1882
|
-
row.classList.add('is-multi-selected');
|
|
1883
|
-
}
|
|
1884
|
-
// v3.5.0 Task 12: Shift+click anywhere on the row toggles it into or
|
|
1885
|
-
// out of `laneMultiSelect`, the set「建立群組」reads. Capture phase, so
|
|
1886
|
-
// this decides BEFORE the row's own children do — a Shift+click landing
|
|
1887
|
-
// on ▲/▼/✕/the name field is claimed here rather than moving the lane,
|
|
1888
|
-
// deleting it, or opening its name for editing, the same reason
|
|
1889
|
-
// `onKeyDown` claims Escape/Tab in the capture phase at the dialog
|
|
1890
|
-
// level. An ordinary click (no Shift) is NOT claimed at all: it falls
|
|
1891
|
-
// through untouched to whichever child the user actually pressed,
|
|
1892
|
-
// which is what keeps every existing gesture in this row working
|
|
1893
|
-
// exactly as it did before this task.
|
|
1894
|
-
//
|
|
1895
|
-
// Fix round 1 (item 1): the name `<input>` is excluded from that catch.
|
|
1896
|
-
// `render()` below rebuilds the WHOLE lane rail from `store.doc`, and a
|
|
1897
|
-
// Shift+click that reached this listener while the user was still
|
|
1898
|
-
// typing in that field — no `change` fired yet, so nothing had been
|
|
1899
|
-
// committed — rebuilt the input from the OLD value, discarding
|
|
1900
|
-
// whatever they had typed with nothing on screen explaining it. That
|
|
1901
|
-
// is the exact failure family this batch has already paid for
|
|
1902
|
-
// (`pendingFrom`/`endpointDrag`'s own fix rounds), arriving by a route
|
|
1903
|
-
// THIS task opened: before it, `render()` never ran while the keyboard
|
|
1904
|
-
// was still in a field with no blur. Shift+click inside a text field
|
|
1905
|
-
// is also, on every platform, understood as "extend the text
|
|
1906
|
-
// selection" — leaving it alone here is not just the safe choice, it
|
|
1907
|
-
// is the expected one. MEASURED (a real Puppeteer page, the nested-
|
|
1908
|
-
// group / long-name / ungroup-button-present row — the most crowded
|
|
1909
|
-
// this rail ever gets): the input covers at most 45 of the row's
|
|
1910
|
-
// 200px; 129–155px of every row stays outside it. So there is always a
|
|
1911
|
-
// large, ordinary Shift+click target left — the ▲/▼/✕/+/解散群組
|
|
1912
|
-
// buttons and the row's own padding — and excluding the input costs
|
|
1913
|
-
// nothing a user could actually reach for.
|
|
1914
|
-
row.addEventListener('click', function (ev) {
|
|
1915
|
-
if (ev.shiftKey !== true) return;
|
|
1916
|
-
if (ev.target !== null && typeof ev.target.classList === 'object' &&
|
|
1917
|
-
ev.target.classList.contains('ed-wave-lane-name')) {
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
ev.preventDefault();
|
|
1921
|
-
ev.stopPropagation();
|
|
1922
|
-
if (laneMultiSelect === null) laneMultiSelect = [];
|
|
1923
|
-
const at = laneMultiSelect.indexOf(i);
|
|
1924
|
-
if (at === -1) laneMultiSelect.push(i);
|
|
1925
|
-
else laneMultiSelect.splice(at, 1);
|
|
1926
|
-
if (laneMultiSelect.length === 0) laneMultiSelect = null;
|
|
1927
|
-
render();
|
|
1928
|
-
}, true);
|
|
1929
|
-
|
|
1930
|
-
row.appendChild(insertButton(doc, i, layout, false));
|
|
1931
|
-
|
|
1932
|
-
const name = d.createElement('input');
|
|
1933
|
-
name.type = 'text';
|
|
1934
|
-
name.className = 'ed-wave-lane-name';
|
|
1935
|
-
name.setAttribute('data-focus-key', 'lane-name-' + i);
|
|
1936
|
-
const lane = layout.lanes[i].lane;
|
|
1937
|
-
name.value = typeof lane.name === 'string' ? lane.name : '';
|
|
1938
|
-
name.addEventListener('change', function () {
|
|
1939
|
-
commit('rename', function (doc2) { return codec.renameLane(doc2, i, name.value); });
|
|
1940
|
-
});
|
|
1941
|
-
row.appendChild(name);
|
|
1942
|
-
|
|
1943
|
-
const up = d.createElement('button');
|
|
1944
|
-
up.type = 'button';
|
|
1945
|
-
up.className = 'ed-wave-lane-up';
|
|
1946
|
-
up.setAttribute('data-focus-key', 'lane-up-' + i);
|
|
1947
|
-
up.textContent = '▲';
|
|
1948
|
-
up.title = '往上移一格';
|
|
1949
|
-
up.disabled = i === 0;
|
|
1950
|
-
up.addEventListener('click', function () {
|
|
1951
|
-
// The button this lane arrives at, EXCEPT at the top, where `lane-up-0`
|
|
1952
|
-
// is disabled and `.focus()` on it is inert — measured, walking a lane
|
|
1953
|
-
// to the top with three ▲ put the cursor on `lane-up-2`, `lane-up-1`
|
|
1954
|
-
// and then BODY, which is the state F7 exists to prevent and the fourth
|
|
1955
|
-
// press then did nothing at all. At the top the cursor goes to the one
|
|
1956
|
-
// control on that row that is never disabled there: ▼.
|
|
1957
|
-
focusOverride = i - 1 === 0 ? 'lane-down-0' : 'lane-up-' + (i - 1);
|
|
1958
|
-
commit('move', function (doc2) { return codec.moveLane(doc2, i, i - 1); });
|
|
1959
|
-
});
|
|
1960
|
-
row.appendChild(up);
|
|
1961
|
-
|
|
1962
|
-
const down = d.createElement('button');
|
|
1963
|
-
down.type = 'button';
|
|
1964
|
-
down.className = 'ed-wave-lane-down';
|
|
1965
|
-
down.setAttribute('data-focus-key', 'lane-down-' + i);
|
|
1966
|
-
down.textContent = '▼';
|
|
1967
|
-
down.title = '往下移一格';
|
|
1968
|
-
down.disabled = i === layout.lanes.length - 1;
|
|
1969
|
-
down.addEventListener('click', function () {
|
|
1970
|
-
// Mirror of ▲ above: `lane-down-<last>` is disabled, so the bottom row
|
|
1971
|
-
// hands the cursor to ▲ instead.
|
|
1972
|
-
focusOverride = i + 1 === layout.lanes.length - 1
|
|
1973
|
-
? 'lane-up-' + (i + 1) : 'lane-down-' + (i + 1);
|
|
1974
|
-
commit('move', function (doc2) { return codec.moveLane(doc2, i, i + 1); });
|
|
1975
|
-
});
|
|
1976
|
-
row.appendChild(down);
|
|
1977
|
-
|
|
1978
|
-
const del = d.createElement('button');
|
|
1979
|
-
del.type = 'button';
|
|
1980
|
-
del.className = 'ed-wave-lane-remove';
|
|
1981
|
-
del.setAttribute('data-focus-key', 'lane-remove-' + i);
|
|
1982
|
-
del.textContent = '✕';
|
|
1983
|
-
del.title = '刪掉這條 lane(如果群組因此空掉,群組也會一起走)';
|
|
1984
|
-
del.addEventListener('click', function () {
|
|
1985
|
-
selection = null;
|
|
1986
|
-
// The row this button belongs to is about to stop existing, so the
|
|
1987
|
-
// keyboard goes to the + that occupies that position instead — the
|
|
1988
|
-
// one control at that spot that survives every removal, tail included.
|
|
1989
|
-
focusOverride = 'lane-add-' + i;
|
|
1990
|
-
commit('remove-lane', function (doc2) { return codec.removeLane(doc2, i); });
|
|
1991
|
-
});
|
|
1992
|
-
row.appendChild(del);
|
|
1993
|
-
|
|
1994
|
-
// v3.5.0 Task 13: duplicate this lane — a full deep copy (`data` / any
|
|
1995
|
-
// other key, via `codec.duplicateLane`) landing right after it at
|
|
1996
|
-
// `i + 1`. Same landing-awareness `insertButton` below already gives
|
|
1997
|
-
// the user for an ordinary insert: `duplicateLane` is built on the very
|
|
1998
|
-
// `addLane`/`laneInsertPath` door that button uses, so it inherits the
|
|
1999
|
-
// identical group-tail asymmetry, and the status line says so the same
|
|
2000
|
-
// way.
|
|
2001
|
-
//
|
|
2002
|
-
// final review finding 2: `node` is deliberately NOT among the copied
|
|
2003
|
-
// keys. It names anchor letters that resolve to THIS lane; carrying it
|
|
2004
|
-
// onto the duplicate would give two lanes the same letters and silently
|
|
2005
|
-
// relocate every edge already anchored here onto the copy instead. The
|
|
2006
|
-
// duplicate starts with no anchors of its own.
|
|
2007
|
-
const dup = d.createElement('button');
|
|
2008
|
-
dup.type = 'button';
|
|
2009
|
-
dup.className = 'ed-wave-lane-duplicate';
|
|
2010
|
-
dup.setAttribute('data-focus-key', 'lane-duplicate-' + i);
|
|
2011
|
-
dup.textContent = '複製';
|
|
2012
|
-
dup.title = '複製這條 lane,接在它後面(data 等欄位一起複製;node 錨點不會跟著複製,避免兩條 lane 搶同一個字母)';
|
|
2013
|
-
dup.addEventListener('click', function () {
|
|
2014
|
-
const landing = landingOf(doc, i + 1);
|
|
2015
|
-
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
2016
|
-
// The copy's own name field is the one control a duplicate exists
|
|
2017
|
-
// for the user to reach next — same reasoning as `insertButton`'s
|
|
2018
|
-
// `focusOverride` below.
|
|
2019
|
-
focusOverride = 'lane-name-' + (i + 1);
|
|
2020
|
-
const didDup = commit('duplicate-lane', function (doc2) { return codec.duplicateLane(doc2, i); });
|
|
2021
|
-
if (didDup) say('複製的 lane 落在' + where);
|
|
2022
|
-
});
|
|
2023
|
-
row.appendChild(dup);
|
|
2024
|
-
|
|
2025
|
-
// v3.5.0 Task 13: insert a spacer — WaveDrom's own blank row, `{}` —
|
|
2026
|
-
// right after this lane. There is deliberately no dedicated "remove
|
|
2027
|
-
// spacer" control: a spacer is a lane like any other, so the ✕ button
|
|
2028
|
-
// every row already has removes it exactly as it removes any other
|
|
2029
|
-
// lane. `codec.addLane` already accepts `{}` (`isLane` only requires a
|
|
2030
|
-
// non-array object), so this needs no new codec function of its own —
|
|
2031
|
-
// unlike `duplicateLane`, which needed one because nothing in the
|
|
2032
|
-
// codec already answers "the lane at this position, copied".
|
|
2033
|
-
const spacer = d.createElement('button');
|
|
2034
|
-
spacer.type = 'button';
|
|
2035
|
-
spacer.className = 'ed-wave-lane-spacer';
|
|
2036
|
-
spacer.setAttribute('data-focus-key', 'lane-spacer-' + i);
|
|
2037
|
-
spacer.textContent = '空白列';
|
|
2038
|
-
spacer.title = '在這條後面插入一個空白列(WaveJSON 的 {},用來空出間距;' +
|
|
2039
|
-
'刪除跟刪一般的 lane 一樣,用 ✕)';
|
|
2040
|
-
spacer.addEventListener('click', function () {
|
|
2041
|
-
const landing = landingOf(doc, i + 1);
|
|
2042
|
-
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
2043
|
-
// A spacer has no name to type, so the control the user is most
|
|
2044
|
-
// likely to reach for next is ✕ — on the new row itself, at i + 1.
|
|
2045
|
-
focusOverride = 'lane-remove-' + (i + 1);
|
|
2046
|
-
const didAdd = commit('add-spacer', function (doc2) { return codec.addLane(doc2, i + 1, {}); });
|
|
2047
|
-
if (didAdd) say('空白列落在' + where);
|
|
2048
|
-
});
|
|
2049
|
-
row.appendChild(spacer);
|
|
2050
|
-
|
|
2051
|
-
// v3.5.0 Task 12: 解散, only on the one row `renderLanes` handed a
|
|
2052
|
-
// span for — the group's own direct first lane (see
|
|
2053
|
-
// `directFirstLaneOf`). The lanes it held stay exactly where they
|
|
2054
|
-
// were; only the wrapping array and its title go.
|
|
2055
|
-
if (ungroupSpan !== undefined) {
|
|
2056
|
-
const ungroup = d.createElement('button');
|
|
2057
|
-
ungroup.type = 'button';
|
|
2058
|
-
ungroup.className = 'ed-wave-lane-ungroup';
|
|
2059
|
-
ungroup.setAttribute('data-focus-key', 'lane-ungroup-' + i);
|
|
2060
|
-
ungroup.textContent = '解散群組';
|
|
2061
|
-
ungroup.title = '解散「' + ungroupSpan.title +
|
|
2062
|
-
'」這個群組(裡面的 lane 都留著,只是不再是一組)';
|
|
2063
|
-
ungroup.addEventListener('click', function () {
|
|
2064
|
-
// The lane at `i` survives ungrouping at the same display position
|
|
2065
|
-
// (see `ungroupLanes`'s own comment: the flattened order and count
|
|
2066
|
-
// never change), so its own name field is exactly where the
|
|
2067
|
-
// keyboard should land.
|
|
2068
|
-
focusOverride = 'lane-name-' + i;
|
|
2069
|
-
commit('ungroup', function (doc2) { return codec.ungroupLanes(doc2, i); });
|
|
2070
|
-
});
|
|
2071
|
-
row.appendChild(ungroup);
|
|
2072
|
-
}
|
|
2073
|
-
return row;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
/**
|
|
2077
|
-
* Where a lane inserted at display position `index` would actually land,
|
|
2078
|
-
* asked of the bridge rather than guessed.
|
|
2079
|
-
*
|
|
2080
|
-
* This is constraint 4 made visible. `laneInsertPath` puts an insert at a
|
|
2081
|
-
* group's FIRST lane inside that group, and an insert at the position just
|
|
2082
|
-
* past its LAST lane outside it — a group can be joined at its head and
|
|
2083
|
-
* never at its tail. That asymmetry is not something a label can argue
|
|
2084
|
-
* away, so the label states it: the button says which container it will
|
|
2085
|
-
* land in, before it is pressed, and the status line says it again after.
|
|
2086
|
-
*/
|
|
2087
|
-
function landingOf(doc, index) {
|
|
2088
|
-
const path = codec.laneInsertPath(doc, index);
|
|
2089
|
-
if (path === null) return { path: null, title: null };
|
|
2090
|
-
const container = path.slice(0, path.length - 1);
|
|
2091
|
-
const held = valueAt(doc, container);
|
|
2092
|
-
const title = Array.isArray(held) && typeof held[0] === 'string' ? held[0] : null;
|
|
2093
|
-
return { path: path, title: title };
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
function insertButton(doc, index, layout, isTail) {
|
|
2097
|
-
const b = d.createElement('button');
|
|
2098
|
-
b.type = 'button';
|
|
2099
|
-
b.className = isTail ? 'ed-wave-lane-add ed-wave-lane-add-tail' : 'ed-wave-lane-add';
|
|
2100
|
-
b.textContent = '+';
|
|
2101
|
-
b.setAttribute('data-insert-at', String(index));
|
|
2102
|
-
b.setAttribute('data-focus-key', 'lane-add-' + index);
|
|
2103
|
-
const landing = landingOf(doc, index);
|
|
2104
|
-
const where = landing.title === null ? '群組外' : '群組「' + landing.title + '」裡';
|
|
2105
|
-
b.setAttribute('data-lands-in', landing.title === null ? '' : landing.title);
|
|
2106
|
-
b.title = '在這裡加一條 lane —— 會落在' + where;
|
|
2107
|
-
if (isTail) {
|
|
2108
|
-
b.style.top = (layout.lanes.length * layout.laneHeight) + 'px';
|
|
2109
|
-
}
|
|
2110
|
-
b.addEventListener('click', function () {
|
|
2111
|
-
// Straight into the new lane's name field: it is what the user is going
|
|
2112
|
-
// to type next, and it is the only control the new row introduces.
|
|
2113
|
-
focusOverride = 'lane-name-' + index;
|
|
2114
|
-
const added = commit('add-lane', function (doc2) {
|
|
2115
|
-
return codec.addLane(doc2, index, { name: '', wave: 'x' });
|
|
2116
|
-
});
|
|
2117
|
-
if (added) say('新增的 lane 落在' + where);
|
|
2118
|
-
});
|
|
2119
|
-
return b;
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
/**
|
|
2123
|
-
* The rail's rename field: this dialog's one sub-panel.
|
|
2124
|
-
*
|
|
2125
|
-
* The field is INSERTED beside its tag and the tag is hidden, rather than
|
|
2126
|
-
* the two being swapped, and taking it down again touches only those two
|
|
2127
|
-
* nodes. That is not tidiness. Taking it down used to mean a full
|
|
2128
|
-
* `render()` — a rebuild of the entire rail — and a rebuild that runs
|
|
2129
|
-
* inside a `blur` handler destroys the element the user is in the middle
|
|
2130
|
-
* of pressing. Measured before this: open the field, then press any rail
|
|
2131
|
-
* button; the mousedown blurred the field, the rail was rebuilt, the
|
|
2132
|
-
* mouseup landed on a fresh element so no `click` ever fired, the press
|
|
2133
|
-
* did nothing at all, and the keyboard ended on `document.body` inside a
|
|
2134
|
-
* modal with no visible cursor.
|
|
2135
|
-
*/
|
|
2136
|
-
function renameGroup(span) {
|
|
2137
|
-
const rows = laneCol.querySelector('.ed-wave-lane-rows');
|
|
2138
|
-
const tag = rows === null ? null
|
|
2139
|
-
: rows.querySelector('.ed-wave-group[data-group-from="' + span.from + '"]');
|
|
2140
|
-
if (tag === null) return;
|
|
2141
|
-
|
|
2142
|
-
const input = d.createElement('input');
|
|
2143
|
-
input.type = 'text';
|
|
2144
|
-
input.className = 'ed-wave-group-input';
|
|
2145
|
-
input.setAttribute('data-focus-key', 'group-input-' + span.from);
|
|
2146
|
-
input.value = span.title;
|
|
2147
|
-
input.style.top = tag.style.top;
|
|
2148
|
-
input.style.left = tag.style.left;
|
|
2149
|
-
|
|
2150
|
-
/** The field off, the tag back — and nothing else in the rail touched.
|
|
2151
|
-
* A no-op for both once a repaint has taken them away. */
|
|
2152
|
-
const takeDown = function () {
|
|
2153
|
-
if (input.parentNode !== null) input.parentNode.removeChild(input);
|
|
2154
|
-
if (d.contains(tag)) tag.hidden = false;
|
|
2155
|
-
};
|
|
2156
|
-
/** The tag this field was covering, or — when a repaint has replaced it
|
|
2157
|
-
* and moved it (a group's `from` is its first lane's row, so an insert
|
|
2158
|
-
* above it renumbers the key) — whatever now carries that key, and
|
|
2159
|
-
* failing that the dialog itself. Never nothing: nothing is
|
|
2160
|
-
* `document.body`. */
|
|
2161
|
-
const focusTag = function () {
|
|
2162
|
-
const live = d.contains(tag) ? tag
|
|
2163
|
-
: overlay.querySelector('[data-focus-key="group-' + span.from + '"]');
|
|
2164
|
-
if (live !== null) live.focus();
|
|
2165
|
-
else panel.focus();
|
|
2166
|
-
};
|
|
2167
|
-
/** Whether the keyboard is IN the field right now. False on the `blur`
|
|
2168
|
-
* path — the browser has already moved it by then — which is how the
|
|
2169
|
-
* two are told apart without asking which device did anything. */
|
|
2170
|
-
const holdsKeyboard = function () { return d.activeElement === input; };
|
|
2171
|
-
|
|
2172
|
-
const finish = function () {
|
|
2173
|
-
// Once. `change` and `blur` both fire for a field committed with Enter,
|
|
2174
|
-
// and every teardown below is a blur too. Whoever gets here first owns
|
|
2175
|
-
// the gesture; the latch is also how the other two say "not you".
|
|
2176
|
-
if (groupRename === null) return;
|
|
2177
|
-
groupRename = null;
|
|
2178
|
-
const hadKeyboard = holdsKeyboard();
|
|
2179
|
-
const value = input.value;
|
|
2180
|
-
takeDown();
|
|
2181
|
-
if (value === span.title) {
|
|
2182
|
-
// Nothing to write, so nothing to repaint — the tag is already back.
|
|
2183
|
-
// The keyboard is only moved when it was in the field: on the mouse
|
|
2184
|
-
// path the browser is already carrying it to the control that was
|
|
2185
|
-
// pressed, and that control is still there to receive it now that
|
|
2186
|
-
// this path no longer rebuilds the rail.
|
|
2187
|
-
if (hadKeyboard) focusTag();
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
// A real commit, so a real repaint — and the keyboard has to be named
|
|
2191
|
-
// if it would otherwise be lost. `focusOverride` is left alone when a
|
|
2192
|
-
// caller has already made a more specific promise.
|
|
2193
|
-
if (focusOverride === null &&
|
|
2194
|
-
(hadKeyboard || !overlay.contains(d.activeElement))) {
|
|
2195
|
-
focusOverride = 'group-' + span.from;
|
|
2196
|
-
}
|
|
2197
|
-
commit('rename-group', function (doc2) {
|
|
2198
|
-
return replaceAt(doc2, span.path.concat([0]), value);
|
|
2199
|
-
});
|
|
2200
|
-
};
|
|
2201
|
-
// The rail is about to be rebuilt underneath this field by somebody
|
|
2202
|
-
// else's repaint — an undo, a redo, any gesture that reaches `render()`
|
|
2203
|
-
// while it is open. Both nodes go with that rebuild, so there is nothing
|
|
2204
|
-
// to take down; what matters is dropping the latch and naming where the
|
|
2205
|
-
// keyboard lands, because `group-input-<n>` will not exist afterwards and
|
|
2206
|
-
// focus would be left on a node that is no longer in the document — which
|
|
2207
|
-
// is the wedge that lost every subsequent keystroke.
|
|
2208
|
-
const retire = function () {
|
|
2209
|
-
if (groupRename === null) return;
|
|
2210
|
-
groupRename = null;
|
|
2211
|
-
if (focusOverride === null && holdsKeyboard()) {
|
|
2212
|
-
focusOverride = 'group-' + span.from;
|
|
2213
|
-
}
|
|
2214
|
-
};
|
|
2215
|
-
// Escape's half of the layering: the field goes, the name does not
|
|
2216
|
-
// change, the session stands, and the keyboard goes back to the tag the
|
|
2217
|
-
// field was covering — never to `document.body`, which is the state the
|
|
2218
|
-
// whole of this task exists to make unreachable. No repaint at all: the
|
|
2219
|
-
// tag was never destroyed.
|
|
2220
|
-
const cancel = function () {
|
|
2221
|
-
if (groupRename === null) return;
|
|
2222
|
-
groupRename = null;
|
|
2223
|
-
takeDown();
|
|
2224
|
-
focusTag();
|
|
2225
|
-
};
|
|
2226
|
-
|
|
2227
|
-
input.addEventListener('change', finish);
|
|
2228
|
-
input.addEventListener('blur', finish);
|
|
2229
|
-
tag.hidden = true;
|
|
2230
|
-
rows.insertBefore(input, tag);
|
|
2231
|
-
// Three facts, and only the three that are read: that a rename is
|
|
2232
|
-
// standing (this being non-null), how to take it down on purpose, and how
|
|
2233
|
-
// to let go of it when a repaint takes it down for us. A copy of the span
|
|
2234
|
-
// or of the field itself would be a second record of something the
|
|
2235
|
-
// closure above already holds.
|
|
2236
|
-
groupRename = { cancel: cancel, retire: retire };
|
|
2237
|
-
input.focus();
|
|
2238
|
-
input.select();
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
2591
|
/**
|
|
2242
2592
|
* The in-place editor for one bus cell's `data` label — this dialog's
|
|
2243
2593
|
* OTHER sub-panel, same `{cancel, retire}` shape as `renameGroup` just
|
|
@@ -2411,7 +2761,7 @@
|
|
|
2411
2761
|
const slot = codec.dataSlotOf(lane, cycle);
|
|
2412
2762
|
if (slot !== null) return { cycle: cycle, slot: slot };
|
|
2413
2763
|
const levels = codec.levelsOf(typeof lane.wave === 'string' ? lane.wave : '');
|
|
2414
|
-
if (!isBus(brickOf(levels[cycle]))) return null;
|
|
2764
|
+
if (!drawer.isBus(drawer.brickOf(levels[cycle]))) return null;
|
|
2415
2765
|
for (let i = cycle - 1; i >= 0 && levels[i] === levels[cycle]; i--) {
|
|
2416
2766
|
const s = codec.dataSlotOf(lane, i);
|
|
2417
2767
|
if (s !== null) return { cycle: i, slot: s };
|
|
@@ -2419,697 +2769,6 @@
|
|
|
2419
2769
|
return 'blocked';
|
|
2420
2770
|
}
|
|
2421
2771
|
|
|
2422
|
-
/**
|
|
2423
|
-
* A copy of `doc` with one scalar replaced, for the two fields the codec's
|
|
2424
|
-
* lane operations do not cover (a group's own title).
|
|
2425
|
-
*
|
|
2426
|
-
* Structure-sharing everywhere it does not have to copy, so the store's
|
|
2427
|
-
* lane-identity bookkeeping still sees every untouched lane as the same
|
|
2428
|
-
* object — `remapOrigins` answers a content edit by position and a
|
|
2429
|
-
* structural one by identity, and a wholesale clone here would make an
|
|
2430
|
-
* ordinary rename look like a structural change.
|
|
2431
|
-
*/
|
|
2432
|
-
function replaceAt(node, path, value) {
|
|
2433
|
-
if (path.length === 0) return value;
|
|
2434
|
-
const seg = path[0];
|
|
2435
|
-
const child = replaceAt(node[seg], path.slice(1), value);
|
|
2436
|
-
if (Array.isArray(node)) {
|
|
2437
|
-
const out = node.slice();
|
|
2438
|
-
out[seg] = child;
|
|
2439
|
-
return out;
|
|
2440
|
-
}
|
|
2441
|
-
const out = Object.assign({}, node);
|
|
2442
|
-
out[seg] = child;
|
|
2443
|
-
return out;
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
/**
|
|
2447
|
-
* The hand-drawn waveform.
|
|
2448
|
-
*
|
|
2449
|
-
* Every cycle's picture comes from `codec.levelsOf` through `brickOf`, and
|
|
2450
|
-
* from nowhere else — constraint 3. The levels and the bricks are both
|
|
2451
|
-
* published on the element (`data-levels-N`, `data-bricks-N`) so the
|
|
2452
|
-
* agreement with the preview beside it is something a test can measure
|
|
2453
|
-
* rather than something this comment can promise: the preview's own SVG
|
|
2454
|
-
* names its bricks in its `<use xlink:href="#…">` list.
|
|
2455
|
-
*/
|
|
2456
|
-
function renderCanvas(doc, layout) {
|
|
2457
|
-
// v3.5.0 Task 11: the data-label field's node sits beside the canvas
|
|
2458
|
-
// SVG inside `canvasWrap` (see `openDataEdit`), and the wipe two lines
|
|
2459
|
-
// below (`canvasWrap.textContent = ''`) would tear it out from under
|
|
2460
|
-
// the user on the very next cursor move if this did not retire it
|
|
2461
|
-
// first — every path that reaches this function counts, not only
|
|
2462
|
-
// `render()`, which is why this lives HERE and not only next to
|
|
2463
|
-
// `groupRename.retire()` in `render()` itself.
|
|
2464
|
-
if (dataEdit !== null) dataEdit.retire();
|
|
2465
|
-
// Every repaint REPLACES the canvas element, so a canvas that had the
|
|
2466
|
-
// keyboard loses it to `document.body` the instant it is detached —
|
|
2467
|
-
// exactly the state F7 and T6h exist to prevent, and here it would fire
|
|
2468
|
-
// on every arrow key. `render()`'s own `data-focus-key` restore covers
|
|
2469
|
-
// the full-repaint path; this covers the three that call renderCanvas
|
|
2470
|
-
// directly (a drag's two, and the keyboard's own), so the property holds
|
|
2471
|
-
// for the canvas no matter which road the repaint came down.
|
|
2472
|
-
const hadFocus = canvas !== null && d.activeElement === canvas;
|
|
2473
|
-
// The cursor is an index pair, and the document under it can get shorter:
|
|
2474
|
-
// deleting cycles or removing the bottom lane leaves it pointing past the
|
|
2475
|
-
// end. Clamped HERE, where the layout that decides what "past the end"
|
|
2476
|
-
// means is already in hand — so nothing downstream ever has to ask.
|
|
2477
|
-
if (cursor !== null) {
|
|
2478
|
-
if (layout.lanes.length === 0 || layout.cycles === 0) {
|
|
2479
|
-
cursor = null;
|
|
2480
|
-
} else {
|
|
2481
|
-
cursor = {
|
|
2482
|
-
laneIndex: Math.max(0, Math.min(cursor.laneIndex, layout.lanes.length - 1)),
|
|
2483
|
-
cycle: Math.max(0, Math.min(cursor.cycle, layout.cycles - 1)),
|
|
2484
|
-
};
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
// …and `selection` with it, which is the other half of the same fact and
|
|
2488
|
-
// was left out of the first cut. Measured before this: Shift-select four
|
|
2489
|
-
// cycles, press 刪除 cycle, and a one-cycle document was left with the
|
|
2490
|
-
// cursor on cycle 1, no visible box, 複製 reporting「複製了 4 個 cycle」
|
|
2491
|
-
// and 貼上(插入)growing the document back to five. The toolbar was
|
|
2492
|
-
// acting on a range the drawing no longer had anywhere to put.
|
|
2493
|
-
//
|
|
2494
|
-
// The two axes are not treated alike, and that is the point: a CYCLE
|
|
2495
|
-
// range that overshoots is the same range in a shorter document, so it
|
|
2496
|
-
// clamps; a LANE that is gone is not a lane the user can have meant, so
|
|
2497
|
-
// the selection goes rather than silently moving to whichever lane
|
|
2498
|
-
// inherited the index. (`lane ✕` already nulls it on its own path; this
|
|
2499
|
-
// catches the undo/redo of that.)
|
|
2500
|
-
//
|
|
2501
|
-
// It does NOT catch every route, and the earlier wording here said it
|
|
2502
|
-
// did. MEASURED: `+` at 0, drag-select cycles 1-3 on the new lane 0,
|
|
2503
|
-
// then Ctrl+Z — the selection box and the rail highlight both stay on
|
|
2504
|
-
// row 0, which is now `clk`. The trace-derived marks above fix the
|
|
2505
|
-
// CURSOR for that case; the selection is still carried by row number,
|
|
2506
|
-
// so it lands on whoever inherited the row. The cost is confined to the
|
|
2507
|
-
// highlight and to where the next Shift+arrow anchors, because every
|
|
2508
|
-
// cycle operation is document-wide — but it is a real gap, not a
|
|
2509
|
-
// covered one.
|
|
2510
|
-
if (selection !== null) {
|
|
2511
|
-
if (layout.lanes.length === 0 || layout.cycles === 0 ||
|
|
2512
|
-
selection.laneIndex < 0 || selection.laneIndex >= layout.lanes.length) {
|
|
2513
|
-
selection = null;
|
|
2514
|
-
} else {
|
|
2515
|
-
selection = {
|
|
2516
|
-
laneIndex: selection.laneIndex,
|
|
2517
|
-
from: Math.max(0, Math.min(selection.from, layout.cycles - 1)),
|
|
2518
|
-
to: Math.max(0, Math.min(selection.to, layout.cycles - 1)),
|
|
2519
|
-
};
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
canvasWrap.textContent = '';
|
|
2523
|
-
canvas = d.createElementNS(SVGNS, 'svg');
|
|
2524
|
-
canvas.setAttribute('class', 'ed-wave-canvas');
|
|
2525
|
-
// In the Tab cycle, and therefore in the focus trap, like every other
|
|
2526
|
-
// control in here: the drawing is this dialog's main surface and was the
|
|
2527
|
-
// one thing in it a keyboard could not reach.
|
|
2528
|
-
canvas.setAttribute('tabindex', '0');
|
|
2529
|
-
canvas.setAttribute('data-focus-key', 'canvas');
|
|
2530
|
-
canvas.setAttribute('aria-label',
|
|
2531
|
-
'波形繪圖區:方向鍵移動游標,Shift+左右鍵選一段 cycle,' +
|
|
2532
|
-
'按電位字元(' + BRUSHES.join(' ') + ')塗上去');
|
|
2533
|
-
const width = Math.max(layout.width, SIZES.cycleWidth);
|
|
2534
|
-
canvas.setAttribute('width', String(width));
|
|
2535
|
-
canvas.setAttribute('height', String(Math.max(layout.height, layout.laneHeight)));
|
|
2536
|
-
canvas.setAttribute('viewBox', '0 0 ' + width + ' ' +
|
|
2537
|
-
Math.max(layout.height, layout.laneHeight));
|
|
2538
|
-
canvas.setAttribute('data-lane-count', String(layout.lanes.length));
|
|
2539
|
-
canvas.setAttribute('data-cycle-count', String(layout.cycles));
|
|
2540
|
-
|
|
2541
|
-
// cycle grid
|
|
2542
|
-
for (let c = 0; c <= layout.cycles; c++) {
|
|
2543
|
-
const g = d.createElementNS(SVGNS, 'line');
|
|
2544
|
-
g.setAttribute('class', 'ed-wave-grid');
|
|
2545
|
-
g.setAttribute('x1', String(c * layout.cycleWidth));
|
|
2546
|
-
g.setAttribute('x2', String(c * layout.cycleWidth));
|
|
2547
|
-
g.setAttribute('y1', '0');
|
|
2548
|
-
g.setAttribute('y2', String(layout.height));
|
|
2549
|
-
canvas.appendChild(g);
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
for (let i = 0; i < layout.lanes.length; i++) {
|
|
2553
|
-
drawLane(canvas, layout, i);
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
const edges = renderEdges(canvas, doc, layout);
|
|
2557
|
-
|
|
2558
|
-
// v3.5.0 Task 8: the in-flight endpoint drag's own preview — UI state
|
|
2559
|
-
// only, never a document read (rule 3: nothing is written until
|
|
2560
|
-
// `finishEndpointDrag` commits it, and Escape mid-drag never reaches
|
|
2561
|
-
// that far). `renderEdges` above already faded the dragged edge's OWN,
|
|
2562
|
-
// still-current path (`.is-drag-source`, read from this same
|
|
2563
|
-
// `endpointDrag`); this draws the other half — a dashed line from the
|
|
2564
|
-
// edge's UNMOVED end to wherever the drag is currently over. Reuses
|
|
2565
|
-
// `.ed-wave-edge-pending`, the exact dashed style Task 7's own
|
|
2566
|
-
// not-yet-written preview line already uses, for the identical
|
|
2567
|
-
// reason: this line is not `doc.edge` either, until the drop.
|
|
2568
|
-
//
|
|
2569
|
-
// Looked up in `edges` — the SAME array `renderEdges` just built this
|
|
2570
|
-
// pass, not a second call to `geometry.edgeLayout` — so the two can
|
|
2571
|
-
// never draw from different snapshots of the layout. `render()`'s own
|
|
2572
|
-
// clamp guarantees `endpointDrag.index === selectedEdge` whenever
|
|
2573
|
-
// `endpointDrag` is non-null, so a lookup miss here would mean that
|
|
2574
|
-
// invariant broke, not that the edge is merely unselected — the `!==
|
|
2575
|
-
// undefined` check is defensive rather than expected to matter.
|
|
2576
|
-
let endpointDragMark = '';
|
|
2577
|
-
if (endpointDrag !== null) {
|
|
2578
|
-
const dragged = edges.filter(function (e) { return e.index === endpointDrag.index; })[0];
|
|
2579
|
-
if (dragged !== undefined) {
|
|
2580
|
-
const otherEnd = endpointDrag.end === 'from' ? 'to' : 'from';
|
|
2581
|
-
const from = dragged[otherEnd];
|
|
2582
|
-
// Falls back to the dragged end's OWN current position — the
|
|
2583
|
-
// degenerate "no visible change yet" line — until a `pointermove`
|
|
2584
|
-
// has actually landed on a real cell (`endpointDrag.at !== null`).
|
|
2585
|
-
let to = dragged[endpointDrag.end];
|
|
2586
|
-
if (endpointDrag.at !== null) {
|
|
2587
|
-
const cell = geometry.cellRect(layout, endpointDrag.at, endpointDrag.cell);
|
|
2588
|
-
if (cell !== null) {
|
|
2589
|
-
to = { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 };
|
|
2590
|
-
endpointDragMark = endpointDrag.index + ':' + endpointDrag.end + '@' +
|
|
2591
|
-
endpointDrag.at + ',' + endpointDrag.cell;
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
const line = d.createElementNS(SVGNS, 'line');
|
|
2595
|
-
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
2596
|
-
line.setAttribute('x1', String(from.x));
|
|
2597
|
-
line.setAttribute('y1', String(from.y));
|
|
2598
|
-
line.setAttribute('x2', String(to.x));
|
|
2599
|
-
line.setAttribute('y2', String(to.y));
|
|
2600
|
-
canvas.appendChild(line);
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
overlay.setAttribute('data-wave-endpoint-drag', endpointDragMark);
|
|
2604
|
-
|
|
2605
|
-
if (selection !== null) {
|
|
2606
|
-
const from = Math.min(selection.from, selection.to);
|
|
2607
|
-
const to = Math.max(selection.from, selection.to);
|
|
2608
|
-
const box = d.createElementNS(SVGNS, 'rect');
|
|
2609
|
-
box.setAttribute('class', 'ed-wave-selection');
|
|
2610
|
-
box.setAttribute('x', String(from * layout.cycleWidth));
|
|
2611
|
-
box.setAttribute('y', String(selection.laneIndex * layout.laneHeight));
|
|
2612
|
-
box.setAttribute('width', String((to - from + 1) * layout.cycleWidth));
|
|
2613
|
-
box.setAttribute('height', String(layout.laneHeight));
|
|
2614
|
-
canvas.appendChild(box);
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
// v3.5.0 Task 7: the pending edge — `pendingFrom` to wherever `cursor`
|
|
2618
|
-
// is now — drawn from UI state alone, never from the document (rule 2:
|
|
2619
|
-
// nothing about the doc changes until the drop). `cursor` is shared
|
|
2620
|
-
// with the keyboard cell below it in paint order for the same reason
|
|
2621
|
-
// the two are drawn from the same variable: a mouse drag and the
|
|
2622
|
-
// keyboard's two-Enter version are one gesture wearing two input
|
|
2623
|
-
// devices, so there is exactly one line to preview, however it is
|
|
2624
|
-
// being aimed at this instant. Skipped once `pendingFrom` is null,
|
|
2625
|
-
// which is every state except "armed with a marked start" and
|
|
2626
|
-
// "dragging" — see `setEdgeMode`.
|
|
2627
|
-
let pendingCell = '';
|
|
2628
|
-
if (pendingFrom !== null && cursor !== null) {
|
|
2629
|
-
const from = geometry.cellRect(layout, pendingFrom.at, pendingFrom.cell);
|
|
2630
|
-
const to = geometry.cellRect(layout, cursor.laneIndex, cursor.cycle);
|
|
2631
|
-
if (from !== null && to !== null) {
|
|
2632
|
-
pendingCell = pendingFrom.at + ',' + pendingFrom.cell;
|
|
2633
|
-
const line = d.createElementNS(SVGNS, 'line');
|
|
2634
|
-
line.setAttribute('class', 'ed-wave-edge-pending');
|
|
2635
|
-
line.setAttribute('x1', String(from.x + from.width / 2));
|
|
2636
|
-
line.setAttribute('y1', String(from.y + from.height / 2));
|
|
2637
|
-
line.setAttribute('x2', String(to.x + to.width / 2));
|
|
2638
|
-
line.setAttribute('y2', String(to.y + to.height / 2));
|
|
2639
|
-
canvas.appendChild(line);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
overlay.setAttribute('data-wave-edge-pending', pendingCell);
|
|
2643
|
-
|
|
2644
|
-
// The keyboard's cell, drawn last so it is over the selection tint and
|
|
2645
|
-
// over the trace. Its geometry comes from `wave-geometry.cellRect` — the
|
|
2646
|
-
// inverse of the hit test the pointer uses — so the cell the cursor is
|
|
2647
|
-
// DRAWN on and the cell an edit lands in cannot drift apart.
|
|
2648
|
-
let cursorCell = '';
|
|
2649
|
-
if (cursor !== null) {
|
|
2650
|
-
const cell = geometry.cellRect(layout, cursor.laneIndex, cursor.cycle);
|
|
2651
|
-
if (cell !== null) {
|
|
2652
|
-
cursorCell = cursor.laneIndex + ',' + cursor.cycle;
|
|
2653
|
-
const cur = d.createElementNS(SVGNS, 'rect');
|
|
2654
|
-
cur.setAttribute('class', 'ed-wave-cursor');
|
|
2655
|
-
cur.setAttribute('data-ed-wave-cursor', '');
|
|
2656
|
-
cur.setAttribute('data-cell', cursorCell);
|
|
2657
|
-
cur.setAttribute('x', String(cell.x));
|
|
2658
|
-
cur.setAttribute('y', String(cell.y));
|
|
2659
|
-
cur.setAttribute('width', String(cell.width));
|
|
2660
|
-
cur.setAttribute('height', String(cell.height));
|
|
2661
|
-
canvas.appendChild(cur);
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
// The same value on the overlay: what a scenario reads when the rect is
|
|
2665
|
-
// not the thing under test. It is assigned INSIDE the branch that draws
|
|
2666
|
-
// the rect and defaults to empty, so "there is no rect" and "the overlay
|
|
2667
|
-
// says none" are one decision rather than two that happen to agree —
|
|
2668
|
-
// they did not, in the first cut: this line read `cursor` directly, so a
|
|
2669
|
-
// cursor the clamp had not caught published a cell that `cellRect`
|
|
2670
|
-
// refused to give a rectangle to, and a build with the clamp removed
|
|
2671
|
-
// reported `{cell: null, published: "5,4"}`.
|
|
2672
|
-
overlay.setAttribute('data-wave-cursor', cursorCell);
|
|
2673
|
-
|
|
2674
|
-
canvas.addEventListener('mousedown', onCanvasDown);
|
|
2675
|
-
// Tabbing onto the drawing is an entry like any other, so it puts the
|
|
2676
|
-
// cursor on screen rather than leaving a focused surface with no visible
|
|
2677
|
-
// cursor in it. Re-entrant only once: `enterDrawing()` repaints, the
|
|
2678
|
-
// repaint re-focuses the new canvas and fires this again, and by then
|
|
2679
|
-
// `cursor` is set.
|
|
2680
|
-
canvas.addEventListener('focus', function () {
|
|
2681
|
-
if (cursor === null) enterDrawing();
|
|
2682
|
-
});
|
|
2683
|
-
canvasWrap.appendChild(canvas);
|
|
2684
|
-
if (hadFocus) canvas.focus();
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
function drawLane(svg, layout, i) {
|
|
2688
|
-
const row = layout.lanes[i];
|
|
2689
|
-
const lane = row.lane;
|
|
2690
|
-
// An ABSENT `wave` and an EMPTY one are two different pictures, and
|
|
2691
|
-
// collapsing them into one was the regression the previous round shipped
|
|
2692
|
-
// on the very axis it was fixing. MEASURED against the pinned engine,
|
|
2693
|
-
// second half-brick per cycle:
|
|
2694
|
-
//
|
|
2695
|
-
// {name:'a', wave:''} ["xxx","xxx"] ONE x cycle
|
|
2696
|
-
// {name:'a'} [] NOTHING
|
|
2697
|
-
// {} (spacer) [] NOTHING
|
|
2698
|
-
// {name:'a', wave:1234} [] NOTHING
|
|
2699
|
-
// {name:'a', wave:null} [] NOTHING
|
|
2700
|
-
//
|
|
2701
|
-
// and in a mixed diagram the engine simply emits no bricks for that row:
|
|
2702
|
-
// `{signal:[{a,'0101'},{},{b,'1010'}]}` is 16 uses, i.e. 2 lanes × 4 × 2.
|
|
2703
|
-
// So the rule is exactly "a lane draws cycles only when `wave` is a
|
|
2704
|
-
// string"; `''` is a string, which is why it gets its one cycle, and
|
|
2705
|
-
// `{}` — the canonical WaveDrom blank-row spacer — gets none.
|
|
2706
|
-
//
|
|
2707
|
-
// The empty-string case is not a corner either: `deleteCycles` covering
|
|
2708
|
-
// every cycle sets every lane's wave to `''`, two clicks away.
|
|
2709
|
-
const hasWave = typeof lane.wave === 'string';
|
|
2710
|
-
const wave = hasWave ? lane.wave : '';
|
|
2711
|
-
const cells = hasWave ? codec.expandWave(wave) : [];
|
|
2712
|
-
const levels = !hasWave ? [] : (wave === '' ? ['x'] : codec.levelsOf(cells));
|
|
2713
|
-
const bricks = levels.map(brickOf);
|
|
2714
|
-
// Which cycles carry the engine's discontinuity marker. `levelsOf`
|
|
2715
|
-
// deliberately erases `|` — it answers what a cycle SHOWS, and a gap
|
|
2716
|
-
// shows whatever is in front of it — so the marker has to be read off the
|
|
2717
|
-
// cells. Measured: `01|10` renders one `<use xlink:href="#gap">` at
|
|
2718
|
-
// translate(100), which is the centre of cycle 2, the `|` cycle.
|
|
2719
|
-
const gaps = [];
|
|
2720
|
-
for (let c = 0; c < cells.length; c++) {
|
|
2721
|
-
if (cells[c].held !== true && cells[c].ch === '|') gaps.push(c);
|
|
2722
|
-
}
|
|
2723
|
-
// v3.5.0 Task 10 fix round 2: which cycles carry the P/N explicit-edge
|
|
2724
|
-
// arrow, and which one. Read off `levels` directly rather than off
|
|
2725
|
-
// `bricks`, for the same reason `gaps` above reads off `cells` rather
|
|
2726
|
-
// than off `bricks`: `brickOf` collapses `P` into the SAME brick id as
|
|
2727
|
-
// `p` (that is what makes the arrow the one thing `brickOf`'s own
|
|
2728
|
-
// model cannot carry — see its comment, and the measurement above the
|
|
2729
|
-
// arrow's own drawing code below), so asking `bricks` here would
|
|
2730
|
-
// always come back empty. One entry per cycle that has an arrow,
|
|
2731
|
-
// `"<cycle>:<P|N>"`, in the same index-not-one-slot-per-cycle shape
|
|
2732
|
-
// `data-gaps-<i>` already uses — an empty lane (no `P`/`N` anywhere,
|
|
2733
|
-
// e.g. every existing fixture's plain `p`/`n` clocks) reads as `''`,
|
|
2734
|
-
// which is the ABSENCE half of the contract this attribute exists to
|
|
2735
|
-
// let a test pin: a `P` lane must show an entry here, a `p` lane must
|
|
2736
|
-
// not, and drawing the arrow on every clock (the regression this
|
|
2737
|
-
// attribute is for) would make the second half false.
|
|
2738
|
-
const clockEdges = [];
|
|
2739
|
-
for (let c = 0; c < levels.length; c++) {
|
|
2740
|
-
if (levels[c] === 'P' || levels[c] === 'N') clockEdges.push(c + ':' + levels[c]);
|
|
2741
|
-
}
|
|
2742
|
-
// `data-wave-<i>` cannot carry the difference between "no `wave` key" and
|
|
2743
|
-
// `wave: ''` — both are the empty string — and that difference is exactly
|
|
2744
|
-
// what the engine draws differently, so it gets its own attribute.
|
|
2745
|
-
svg.setAttribute('data-haswave-' + i, hasWave ? '1' : '0');
|
|
2746
|
-
svg.setAttribute('data-wave-' + i, wave);
|
|
2747
|
-
svg.setAttribute('data-levels-' + i, levels.join(''));
|
|
2748
|
-
svg.setAttribute('data-bricks-' + i, bricks.join(' '));
|
|
2749
|
-
svg.setAttribute('data-gaps-' + i, gaps.join(' '));
|
|
2750
|
-
svg.setAttribute('data-clock-edge-' + i, clockEdges.join(' '));
|
|
2751
|
-
|
|
2752
|
-
const hi = row.y + 7;
|
|
2753
|
-
const lo = row.y + row.height - 9;
|
|
2754
|
-
const mid = (hi + lo) / 2;
|
|
2755
|
-
const cw = layout.cycleWidth;
|
|
2756
|
-
const labels = labelsOf(lane);
|
|
2757
|
-
// Which cycles carry a `data` label: the explicit value cycles, in order.
|
|
2758
|
-
// `levelsOf` repeats a bus's level through its continuation cycles, so the
|
|
2759
|
-
// label belongs to the cycle that STARTS the run, which is what the run
|
|
2760
|
-
// grouping below already knows.
|
|
2761
|
-
let labelSlot = 0;
|
|
2762
|
-
|
|
2763
|
-
// Runs of one picture. Clock bricks never merge — each `p` cycle is a
|
|
2764
|
-
// whole clock period, and a merged run would draw one long one.
|
|
2765
|
-
const runs = [];
|
|
2766
|
-
for (let c = 0; c < bricks.length; c++) {
|
|
2767
|
-
const last = runs.length === 0 ? null : runs[runs.length - 1];
|
|
2768
|
-
if (last !== null && last.brick === bricks[c] && !isClock(bricks[c])) last.to = c;
|
|
2769
|
-
else runs.push({ brick: bricks[c], from: c, to: c });
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
function bandOf(brick) {
|
|
2773
|
-
if (brick === '111' || brick === 'uuu') return [hi, hi];
|
|
2774
|
-
if (brick === '000' || brick === 'ddd') return [lo, lo];
|
|
2775
|
-
if (brick === 'zzz') return [mid, mid];
|
|
2776
|
-
return [hi, lo];
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
for (let r = 0; r < runs.length; r++) {
|
|
2780
|
-
const run = runs[r];
|
|
2781
|
-
const x0 = run.from * cw;
|
|
2782
|
-
const x1 = (run.to + 1) * cw;
|
|
2783
|
-
const brick = run.brick;
|
|
2784
|
-
if (isBus(brick)) {
|
|
2785
|
-
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
2786
|
-
const s = Math.min(6, cw / 4);
|
|
2787
|
-
poly.setAttribute('class', 'ed-wave-bus ed-wave-bus-' + brick.slice(4));
|
|
2788
|
-
poly.setAttribute('points',
|
|
2789
|
-
(x0 + s) + ',' + hi + ' ' + (x1 - s) + ',' + hi + ' ' +
|
|
2790
|
-
x1 + ',' + mid + ' ' + (x1 - s) + ',' + lo + ' ' +
|
|
2791
|
-
(x0 + s) + ',' + lo + ' ' + x0 + ',' + mid);
|
|
2792
|
-
svg.appendChild(poly);
|
|
2793
|
-
const text = labels[labelSlot];
|
|
2794
|
-
if (typeof text === 'string' && text !== '') {
|
|
2795
|
-
const t = d.createElementNS(SVGNS, 'text');
|
|
2796
|
-
t.setAttribute('class', 'ed-wave-buslabel');
|
|
2797
|
-
t.setAttribute('x', String((x0 + x1) / 2));
|
|
2798
|
-
t.setAttribute('y', String(mid + 4));
|
|
2799
|
-
t.setAttribute('text-anchor', 'middle');
|
|
2800
|
-
t.textContent = text;
|
|
2801
|
-
svg.appendChild(t);
|
|
2802
|
-
} else {
|
|
2803
|
-
// v3.5.0 Task 11 fix round 3: an UNLABELLED bus run has no
|
|
2804
|
-
// `<text>` for a press to land on — fix round 2's own rule
|
|
2805
|
-
// ("open the field only on a press that hits the label text")
|
|
2806
|
-
// otherwise leaves the single most ordinary flow in this editor
|
|
2807
|
-
// (paint a `=`, then name it) mouse-unreachable. This dot is
|
|
2808
|
-
// that run's hit target when there is no label yet: same
|
|
2809
|
-
// class as the text (`ed-wave-buslabel`, so `onCanvasDown`'s
|
|
2810
|
-
// existing hit test needs no change at all — it already asks
|
|
2811
|
-
// for that one class, not for an element type) plus
|
|
2812
|
-
// `ed-wave-buslabel-empty` for the dim, small styling
|
|
2813
|
-
// (`lib/md2doc.js`) that keeps a diagram full of unlabelled
|
|
2814
|
-
// buses from turning into a field of marks. Radius bounded by
|
|
2815
|
-
// the run's own box on both axes, not a bare constant, so the
|
|
2816
|
-
// box stays MOSTLY paintable even at the narrowest cycle width
|
|
2817
|
-
// this editor draws, and a press anywhere else in the box still
|
|
2818
|
-
// falls through to the ordinary paint dispatch exactly as
|
|
2819
|
-
// round 2 established.
|
|
2820
|
-
const r = Math.min(3.5, (x1 - x0) / 6, (lo - hi) / 6);
|
|
2821
|
-
const dot = d.createElementNS(SVGNS, 'circle');
|
|
2822
|
-
dot.setAttribute('class', 'ed-wave-buslabel ed-wave-buslabel-empty');
|
|
2823
|
-
dot.setAttribute('cx', String((x0 + x1) / 2));
|
|
2824
|
-
dot.setAttribute('cy', String(mid));
|
|
2825
|
-
dot.setAttribute('r', String(r));
|
|
2826
|
-
svg.appendChild(dot);
|
|
2827
|
-
}
|
|
2828
|
-
labelSlot++;
|
|
2829
|
-
} else if (brick === 'xxx') {
|
|
2830
|
-
const rect = d.createElementNS(SVGNS, 'rect');
|
|
2831
|
-
rect.setAttribute('class', 'ed-wave-x');
|
|
2832
|
-
rect.setAttribute('x', String(x0));
|
|
2833
|
-
rect.setAttribute('y', String(hi));
|
|
2834
|
-
rect.setAttribute('width', String(x1 - x0));
|
|
2835
|
-
rect.setAttribute('height', String(lo - hi));
|
|
2836
|
-
svg.appendChild(rect);
|
|
2837
|
-
} else if (isClock(brick)) {
|
|
2838
|
-
const xm = (x0 + x1) / 2;
|
|
2839
|
-
const p = d.createElementNS(SVGNS, 'path');
|
|
2840
|
-
p.setAttribute('class', 'ed-wave-line ed-wave-clock');
|
|
2841
|
-
p.setAttribute('d', brick === 'nclk'
|
|
2842
|
-
? 'M' + x0 + ',' + lo + ' L' + x0 + ',' + hi + ' L' + xm + ',' + hi +
|
|
2843
|
-
' L' + xm + ',' + lo + ' L' + x1 + ',' + lo
|
|
2844
|
-
: 'M' + x0 + ',' + hi + ' L' + x0 + ',' + lo + ' L' + xm + ',' + lo +
|
|
2845
|
-
' L' + xm + ',' + hi + ' L' + x1 + ',' + hi);
|
|
2846
|
-
svg.appendChild(p);
|
|
2847
|
-
|
|
2848
|
-
// v3.5.0 Task 10: `P`/`N` are explicit-edge clocks. `brickOf` maps
|
|
2849
|
-
// both to the SAME brick id as `p`/`n` (`nclk`/`pclk` — see its own
|
|
2850
|
-
// comment), which is correct for the level the engine SHOWS but
|
|
2851
|
-
// erases the one thing that makes `P`/`N` look different: an
|
|
2852
|
-
// arrowhead. MEASURED against the pinned wavedrom 3.5.0 — rendered
|
|
2853
|
-
// `{signal:[{name:'a',wave:'zpp'}]}` next to `{wave:'zPP'}` via
|
|
2854
|
-
// `wd.renderAny(0, doc, wd.waveSkin)` and diffed the `<use
|
|
2855
|
-
// xlink:href>` list:
|
|
2856
|
-
//
|
|
2857
|
-
// p: #zzz #zzz #pclk #nclk #pclk #nclk
|
|
2858
|
-
// P: #zzz #zzz #Pclk #nclk #Pclk #nclk
|
|
2859
|
-
// n: #zzz #zzz #nclk #pclk #nclk #pclk
|
|
2860
|
-
// N: #zzz #zzz #Nclk #pclk #Nclk #pclk
|
|
2861
|
-
//
|
|
2862
|
-
// — same SECOND half-brick every time (that is what `brickOf`'s own
|
|
2863
|
-
// measurement already pins), but the FIRST half-brick gets its own
|
|
2864
|
-
// symbol. Read straight out of `node_modules/wavedrom/skins/default.js`:
|
|
2865
|
-
//
|
|
2866
|
-
// pclk 'M0,20 0,0 20,0' (just the edge)
|
|
2867
|
-
// Pclk same edge PLUS 'M-3,12 0,3 3,12 C 1,11 -1,11 -3,12 z' (filled, class s6)
|
|
2868
|
-
// nclk 'm0,0 0,20 20,0'
|
|
2869
|
-
// Nclk same edge PLUS 'M-3,8 0,17 3,8 C 1,9 -1,9 -3,8 z' (filled, class s6)
|
|
2870
|
-
//
|
|
2871
|
-
// i.e. in the engine's own 20-unit symbol space (y=0 top, y=20
|
|
2872
|
-
// bottom, matching this file's hi/lo), a small filled triangle
|
|
2873
|
-
// straddling local x=0 — `P`'s apex at local y=3 pointing UP
|
|
2874
|
-
// (toward hi), `N`'s apex at local y=17 pointing DOWN (toward lo).
|
|
2875
|
-
// Both sit on the FIRST half-brick, which in this drawing's
|
|
2876
|
-
// one-brick-per-period model (a whole `p`/`P` cycle is ONE `nclk`
|
|
2877
|
-
// path, not two half-bricks — see `isClock`'s own comment) is
|
|
2878
|
-
// always the vertical stroke at x0, the edge INTO this cycle. So
|
|
2879
|
-
// the level char (not the brick id, which is identical for
|
|
2880
|
-
// `p`/`P`) is what has to be read to know an arrow is owed, and
|
|
2881
|
-
// `levels[run.from]` is it — clock bricks never merge (the `!
|
|
2882
|
-
// isClock` guard below), so `run.from === run.to` and this is
|
|
2883
|
-
// exactly the cycle being drawn. `data-clock-edge-<i>`, set above
|
|
2884
|
-
// (this function's own top, next to `data-gaps-<i>`) from the same
|
|
2885
|
-
// `levels` array, is the one place this fact is exposed for a test
|
|
2886
|
-
// to pin — nothing here duplicates it onto the shape itself, so
|
|
2887
|
-
// there is one belief about which cycles have an arrow, not two.
|
|
2888
|
-
//
|
|
2889
|
-
// Reproduced as a plain filled triangle, not the engine's
|
|
2890
|
-
// bezier-curved one: every other brick this file draws is already
|
|
2891
|
-
// a simplified redraw (the bus is a hexagon, the gap is a pair of
|
|
2892
|
-
// chevrons), not a pixel copy of wavedrom's own glyphs. Scaled out
|
|
2893
|
-
// of the engine's 20-unit symbol into this cell's own geometry —
|
|
2894
|
-
// local x by (xm - x0) / 20, local y by (lo - hi) / 20, both
|
|
2895
|
-
// anchored at (x0, hi) — rather than hand-picked pixel offsets, so
|
|
2896
|
-
// it tracks `SIZES.cycleWidth`/`laneHeight` if those ever change.
|
|
2897
|
-
const edgeLevel = levels[run.from];
|
|
2898
|
-
if (edgeLevel === 'P' || edgeLevel === 'N') {
|
|
2899
|
-
const sx = (xm - x0) / 20;
|
|
2900
|
-
const sy = (lo - hi) / 20;
|
|
2901
|
-
const apexY = edgeLevel === 'P' ? hi + 3 * sy : hi + 17 * sy;
|
|
2902
|
-
const baseY = edgeLevel === 'P' ? hi + 12 * sy : hi + 8 * sy;
|
|
2903
|
-
const arrow = d.createElementNS(SVGNS, 'path');
|
|
2904
|
-
arrow.setAttribute('class', 'ed-wave-clock-arrow');
|
|
2905
|
-
arrow.setAttribute('d',
|
|
2906
|
-
'M' + (x0 - 3 * sx) + ',' + baseY +
|
|
2907
|
-
' L' + x0 + ',' + apexY +
|
|
2908
|
-
' L' + (x0 + 3 * sx) + ',' + baseY + ' Z');
|
|
2909
|
-
svg.appendChild(arrow);
|
|
2910
|
-
}
|
|
2911
|
-
} else {
|
|
2912
|
-
const y = bandOf(brick)[0];
|
|
2913
|
-
const line = d.createElementNS(SVGNS, 'line');
|
|
2914
|
-
line.setAttribute('class', brick === 'uuu' || brick === 'ddd'
|
|
2915
|
-
? 'ed-wave-line ed-wave-weak' : 'ed-wave-line');
|
|
2916
|
-
line.setAttribute('x1', String(x0));
|
|
2917
|
-
line.setAttribute('x2', String(x1));
|
|
2918
|
-
line.setAttribute('y1', String(y));
|
|
2919
|
-
line.setAttribute('y2', String(y));
|
|
2920
|
-
svg.appendChild(line);
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
|
-
// The edge into this run. Clock bricks draw their own.
|
|
2924
|
-
if (r > 0 && !isClock(brick) && !isClock(runs[r - 1].brick)) {
|
|
2925
|
-
const a = bandOf(runs[r - 1].brick);
|
|
2926
|
-
const b = bandOf(brick);
|
|
2927
|
-
const top = Math.min(a[0], a[1], b[0], b[1]);
|
|
2928
|
-
const bot = Math.max(a[0], a[1], b[0], b[1]);
|
|
2929
|
-
if (bot > top) {
|
|
2930
|
-
const edge = d.createElementNS(SVGNS, 'line');
|
|
2931
|
-
edge.setAttribute('class', 'ed-wave-edge');
|
|
2932
|
-
edge.setAttribute('x1', String(x0));
|
|
2933
|
-
edge.setAttribute('x2', String(x0));
|
|
2934
|
-
edge.setAttribute('y1', String(top));
|
|
2935
|
-
edge.setAttribute('y2', String(bot));
|
|
2936
|
-
svg.appendChild(edge);
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
// The gap marker, last so it sits over the trace it interrupts. The
|
|
2942
|
-
// engine draws one per `|`; drawing none made the two pictures disagree
|
|
2943
|
-
// on a lane whose levels agreed exactly, which is the quietest form of
|
|
2944
|
-
// the disagreement this overlay exists to surface.
|
|
2945
|
-
for (const c of gaps) {
|
|
2946
|
-
const xm = (c + 0.5) * cw;
|
|
2947
|
-
const mark = d.createElementNS(SVGNS, 'path');
|
|
2948
|
-
mark.setAttribute('class', 'ed-wave-gap');
|
|
2949
|
-
mark.setAttribute('d',
|
|
2950
|
-
'M' + (xm - 5) + ',' + (lo + 3) + ' L' + (xm + 1) + ',' + (mid) +
|
|
2951
|
-
' L' + (xm - 5) + ',' + (hi - 3) +
|
|
2952
|
-
' M' + (xm - 1) + ',' + (lo + 3) + ' L' + (xm + 5) + ',' + (mid) +
|
|
2953
|
-
' L' + (xm - 1) + ',' + (hi - 3));
|
|
2954
|
-
svg.appendChild(mark);
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
/**
|
|
2959
|
-
* Every `edge` entry, drawn after every lane: one `<path>` (`d` from
|
|
2960
|
-
* `geometry.edgeLayout`), a label `<text>` when the entry has one, and —
|
|
2961
|
-
* for `selectedEdge` only — two endpoint handles from
|
|
2962
|
-
* `geometry.edgeHandleRect`, exactly the rectangles `onCanvasDown` below
|
|
2963
|
-
* hit-tests against, so the drawn handle and the clickable one cannot
|
|
2964
|
-
* drift apart.
|
|
2965
|
-
*
|
|
2966
|
-
* A second, invisible, wide-stroke `<path>` (`class="ed-wave-edge-hit"`)
|
|
2967
|
-
* is layered over each visible one purely so it can be clicked: the
|
|
2968
|
-
* visible path is 1px wide, and `document.elementFromPoint` (what
|
|
2969
|
-
* `onCanvasDown` reads to answer "did this press land on a line") would
|
|
2970
|
-
* otherwise miss any press that is not on that exact pixel.
|
|
2971
|
-
* `lib/md2doc.js`'s `.ed-wave-edge-hit` rule sets `pointer-events: stroke`
|
|
2972
|
-
* — what makes a `stroke: transparent` path hit-testable at all, without
|
|
2973
|
-
* which a transparent stroke paints nothing and "nothing painted" is what
|
|
2974
|
-
* `visiblePainted` (the SVG default) refuses to hit.
|
|
2975
|
-
*
|
|
2976
|
-
* Arrow markers are picked from the shape string, MEASURED against
|
|
2977
|
-
* `node_modules/wavedrom/lib/arc-shape.js`'s own `style` table (every
|
|
2978
|
-
* `case` there that sets a `style` string): a shape containing `<` sets
|
|
2979
|
-
* `marker-start:url(#arrowtail)`, one containing `>` sets
|
|
2980
|
-
* `marker-end:url(#arrowhead)`, and `+` is the one shape that sets BOTH
|
|
2981
|
-
* markers, to `url(#tee)` — the seven plain shapes (`-`, `~`, `-~`, `~-`,
|
|
2982
|
-
* `-|`, `|-`, `-|-`) set no `style` at all and draw no arrowhead.
|
|
2983
|
-
* (`applyEdgeMarkers` below is unaffected by the exact count either way —
|
|
2984
|
-
* it tests `indexOf('<')` / `indexOf('>')` rather than enumerating a
|
|
2985
|
-
* list — but this comment's own count should still match the pinned
|
|
2986
|
-
* engine.) `edgeArrowMarker` below reproduces the head/tail split with
|
|
2987
|
-
* ONE shared marker definition rather than two: `orient="auto-start-reverse"`
|
|
2988
|
-
* is what flips a `marker-start` 180° from a `marker-end` automatically,
|
|
2989
|
-
* so the single triangle already points the right way at both ends.
|
|
2990
|
-
*/
|
|
2991
|
-
function renderEdges(svg, doc, layout) {
|
|
2992
|
-
const edges = geometry.edgeLayout(doc, layout);
|
|
2993
|
-
overlay.setAttribute('data-wave-edge-count', String(edges.length));
|
|
2994
|
-
if (edges.length === 0) return edges;
|
|
2995
|
-
|
|
2996
|
-
const defs = d.createElementNS(SVGNS, 'defs');
|
|
2997
|
-
defs.appendChild(edgeArrowMarker());
|
|
2998
|
-
defs.appendChild(edgeTeeMarker());
|
|
2999
|
-
svg.appendChild(defs);
|
|
3000
|
-
|
|
3001
|
-
for (const e of edges) {
|
|
3002
|
-
const isSelected = e.index === selectedEdge;
|
|
3003
|
-
// v3.5.0 Task 8: the edge an in-flight endpoint drag is dragging
|
|
3004
|
-
// keeps drawing at its ACTUAL (still-committed) position — the
|
|
3005
|
-
// document is untouched until the drop (rule 3) — but is faded so
|
|
3006
|
-
// it reads as "not where this will end up" once the dashed preview
|
|
3007
|
-
// (`renderCanvas`, below `renderEdges`'s own call) is standing next
|
|
3008
|
-
// to it. `endpointDrag` can only ever name `selectedEdge` itself
|
|
3009
|
-
// (`render()`'s own clamp), so `isSelected` is already true here too
|
|
3010
|
-
// and this only ever adds a class, never substitutes for `.is-on`.
|
|
3011
|
-
const isDragSource = endpointDrag !== null && e.index === endpointDrag.index;
|
|
3012
|
-
const path = d.createElementNS(SVGNS, 'path');
|
|
3013
|
-
// Every visual property (stroke, fill, width) lives in
|
|
3014
|
-
// `lib/md2doc.js`'s `.ed-wave-edge-path` / `.is-on` CSS, the same
|
|
3015
|
-
// mechanism the rest of this drawing's colours already go through
|
|
3016
|
-
// (`.ed-wave-line`, `.ed-wave-selection`, `.ed-wave-cursor`, …) —
|
|
3017
|
-
// one place to repaint the editor, not two. Only genuinely geometric
|
|
3018
|
-
// attributes (`d`, the marker refs) stay here as attributes.
|
|
3019
|
-
path.setAttribute('class', 'ed-wave-edge-path' + (isSelected ? ' is-on' : '') +
|
|
3020
|
-
(isDragSource ? ' is-drag-source' : ''));
|
|
3021
|
-
path.setAttribute('data-edge-index', String(e.index));
|
|
3022
|
-
path.setAttribute('d', e.d);
|
|
3023
|
-
applyEdgeMarkers(path, e.edge.shape);
|
|
3024
|
-
svg.appendChild(path);
|
|
3025
|
-
|
|
3026
|
-
const hit = d.createElementNS(SVGNS, 'path');
|
|
3027
|
-
hit.setAttribute('class', 'ed-wave-edge-hit');
|
|
3028
|
-
hit.setAttribute('data-edge-index', String(e.index));
|
|
3029
|
-
hit.setAttribute('d', e.d);
|
|
3030
|
-
svg.appendChild(hit);
|
|
3031
|
-
|
|
3032
|
-
if (e.edge.label !== '') {
|
|
3033
|
-
const t = d.createElementNS(SVGNS, 'text');
|
|
3034
|
-
t.setAttribute('class', 'ed-wave-edge-label');
|
|
3035
|
-
t.setAttribute('x', String(e.labelPos.x));
|
|
3036
|
-
t.setAttribute('y', String(e.labelPos.y - 4));
|
|
3037
|
-
// `text-anchor` positions the glyphs against (x, y) — geometry, not
|
|
3038
|
-
// colour — so it stays an attribute, exactly as the existing
|
|
3039
|
-
// `.ed-wave-buslabel` text already does a few lines up.
|
|
3040
|
-
t.setAttribute('text-anchor', 'middle');
|
|
3041
|
-
t.textContent = e.edge.label;
|
|
3042
|
-
svg.appendChild(t);
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
if (isSelected) {
|
|
3046
|
-
for (const end of ['from', 'to']) {
|
|
3047
|
-
const r = geometry.edgeHandleRect(e[end]);
|
|
3048
|
-
const h = d.createElementNS(SVGNS, 'rect');
|
|
3049
|
-
h.setAttribute('class', 'ed-wave-edge-handle');
|
|
3050
|
-
h.setAttribute('data-edge-handle', end);
|
|
3051
|
-
h.setAttribute('data-edge-index', String(e.index));
|
|
3052
|
-
h.setAttribute('x', String(r.x));
|
|
3053
|
-
h.setAttribute('y', String(r.y));
|
|
3054
|
-
h.setAttribute('width', String(r.width));
|
|
3055
|
-
h.setAttribute('height', String(r.height));
|
|
3056
|
-
svg.appendChild(h);
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
return edges;
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
/** `marker-start` / `marker-end` for one shape — see `renderEdges`'s own
|
|
3064
|
-
* comment for the measurement behind this split. */
|
|
3065
|
-
function applyEdgeMarkers(path, shape) {
|
|
3066
|
-
if (shape === '+') {
|
|
3067
|
-
path.setAttribute('marker-start', 'url(#ed-wave-tee)');
|
|
3068
|
-
path.setAttribute('marker-end', 'url(#ed-wave-tee)');
|
|
3069
|
-
return;
|
|
3070
|
-
}
|
|
3071
|
-
if (shape.indexOf('<') !== -1) path.setAttribute('marker-start', 'url(#ed-wave-arrowhead)');
|
|
3072
|
-
if (shape.indexOf('>') !== -1) path.setAttribute('marker-end', 'url(#ed-wave-arrowhead)');
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
/** One triangle, reused for both ends via `auto-start-reverse`. */
|
|
3076
|
-
function edgeArrowMarker() {
|
|
3077
|
-
const m = d.createElementNS(SVGNS, 'marker');
|
|
3078
|
-
m.setAttribute('id', 'ed-wave-arrowhead');
|
|
3079
|
-
m.setAttribute('viewBox', '0 0 8 8');
|
|
3080
|
-
m.setAttribute('markerWidth', '8');
|
|
3081
|
-
m.setAttribute('markerHeight', '8');
|
|
3082
|
-
m.setAttribute('refX', '7');
|
|
3083
|
-
m.setAttribute('refY', '4');
|
|
3084
|
-
m.setAttribute('orient', 'auto-start-reverse');
|
|
3085
|
-
const poly = d.createElementNS(SVGNS, 'polygon');
|
|
3086
|
-
poly.setAttribute('points', '0,0 8,4 0,8');
|
|
3087
|
-
poly.setAttribute('class', 'ed-wave-edge-arrow');
|
|
3088
|
-
m.appendChild(poly);
|
|
3089
|
-
return m;
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
/** The `+` shape's tee: a crossbar perpendicular to the path at either
|
|
3093
|
-
* end, symmetric under a 180° flip so `orient="auto"` needs no reverse. */
|
|
3094
|
-
function edgeTeeMarker() {
|
|
3095
|
-
const m = d.createElementNS(SVGNS, 'marker');
|
|
3096
|
-
m.setAttribute('id', 'ed-wave-tee');
|
|
3097
|
-
m.setAttribute('viewBox', '0 0 8 8');
|
|
3098
|
-
m.setAttribute('markerWidth', '8');
|
|
3099
|
-
m.setAttribute('markerHeight', '8');
|
|
3100
|
-
m.setAttribute('refX', '4');
|
|
3101
|
-
m.setAttribute('refY', '4');
|
|
3102
|
-
m.setAttribute('orient', 'auto');
|
|
3103
|
-
const line = d.createElementNS(SVGNS, 'line');
|
|
3104
|
-
line.setAttribute('x1', '4');
|
|
3105
|
-
line.setAttribute('y1', '0');
|
|
3106
|
-
line.setAttribute('x2', '4');
|
|
3107
|
-
line.setAttribute('y2', '8');
|
|
3108
|
-
line.setAttribute('class', 'ed-wave-edge-tee');
|
|
3109
|
-
m.appendChild(line);
|
|
3110
|
-
return m;
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
2772
|
// ── the keyboard on the drawing ───────────────────────────────────────
|
|
3114
2773
|
/**
|
|
3115
2774
|
* Every gesture the drawing has, without a pointer.
|
|
@@ -3131,6 +2790,17 @@
|
|
|
3131
2790
|
* brush is on. Everything else the dialog can do is a real `<button>` or
|
|
3132
2791
|
* `<input>` that Tab already reaches.
|
|
3133
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
|
+
*
|
|
3134
2804
|
* Two targets are accepted, and only two:
|
|
3135
2805
|
* the canvas — the surface itself, `tabindex="0"`, so it is in the Tab
|
|
3136
2806
|
* cycle and in the focus trap like every other control.
|
|
@@ -3156,6 +2826,28 @@
|
|
|
3156
2826
|
enterDrawing();
|
|
3157
2827
|
return true;
|
|
3158
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.
|
|
3159
2851
|
if (nav) {
|
|
3160
2852
|
ev.preventDefault();
|
|
3161
2853
|
ev.stopPropagation();
|
|
@@ -3175,7 +2867,7 @@
|
|
|
3175
2867
|
ev.stopPropagation();
|
|
3176
2868
|
if (pendingFrom === null) {
|
|
3177
2869
|
pendingFrom = { at: cursor.laneIndex, cell: cursor.cycle };
|
|
3178
|
-
say('起點已標記,移到終點再按一次 Enter');
|
|
2870
|
+
panels.say('起點已標記,移到終點再按一次 Enter');
|
|
3179
2871
|
render();
|
|
3180
2872
|
} else {
|
|
3181
2873
|
finishEdgeDrag(cursor.laneIndex, cursor.cycle);
|
|
@@ -3205,7 +2897,7 @@
|
|
|
3205
2897
|
if (dlTarget === 'blocked') {
|
|
3206
2898
|
ev.preventDefault();
|
|
3207
2899
|
ev.stopPropagation();
|
|
3208
|
-
say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
2900
|
+
panels.say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3209
2901
|
return true;
|
|
3210
2902
|
}
|
|
3211
2903
|
if (dlTarget !== null) {
|
|
@@ -3269,9 +2961,16 @@
|
|
|
3269
2961
|
* ONE lane, a run of cycles — is exactly what a cursor plus Shift can
|
|
3270
2962
|
* describe. A pointer press already collapses it to the pressed cell; a
|
|
3271
2963
|
* plain arrow does the same, and Shift+←/→ keeps the anchor it was made
|
|
3272
|
-
* with. Shift+↑/↓ collapses
|
|
3273
|
-
*
|
|
3274
|
-
*
|
|
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.
|
|
3275
2974
|
*/
|
|
3276
2975
|
function moveCursor(key, extend) {
|
|
3277
2976
|
const layout = geometry.layoutOf(store.doc, SIZES);
|
|
@@ -3290,10 +2989,98 @@
|
|
|
3290
2989
|
const sameLane = lane === cursor.laneIndex;
|
|
3291
2990
|
cursor = { laneIndex: lane, cycle: cyc };
|
|
3292
2991
|
if (extend && sameLane && selection !== null && selection.laneIndex === lane) {
|
|
3293
|
-
selection = { laneIndex: lane, from: selection.from, to: cyc };
|
|
2992
|
+
selection = { laneIndex: lane, laneTo: lane, from: selection.from, to: cyc };
|
|
3294
2993
|
} else {
|
|
3295
|
-
selection = { laneIndex: lane, from: cyc, to: cyc };
|
|
2994
|
+
selection = { laneIndex: lane, laneTo: lane, from: cyc, to: cyc };
|
|
2995
|
+
}
|
|
2996
|
+
repaintDrawing(layout);
|
|
2997
|
+
scrollCursorIntoView(layout);
|
|
2998
|
+
sayCursor(layout);
|
|
2999
|
+
}
|
|
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;
|
|
3296
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 };
|
|
3297
3084
|
repaintDrawing(layout);
|
|
3298
3085
|
scrollCursorIntoView(layout);
|
|
3299
3086
|
sayCursor(layout);
|
|
@@ -3340,7 +3127,7 @@
|
|
|
3340
3127
|
const to = selection === null ? cursor.cycle : Math.max(selection.from, selection.to);
|
|
3341
3128
|
const span = (selection !== null && selection.laneIndex === cursor.laneIndex && to > from)
|
|
3342
3129
|
? ',選了 cycle ' + (from + 1) + '–' + (to + 1) : '';
|
|
3343
|
-
say('游標:' + name + ' cycle ' + (cursor.cycle + 1) + span);
|
|
3130
|
+
panels.say('游標:' + name + ' cycle ' + (cursor.cycle + 1) + span);
|
|
3344
3131
|
}
|
|
3345
3132
|
|
|
3346
3133
|
/** The lane list and the drawing, repainted together and without touching
|
|
@@ -3351,8 +3138,29 @@
|
|
|
3351
3138
|
function repaintDrawing(layout) {
|
|
3352
3139
|
const l = layout === undefined || layout === null
|
|
3353
3140
|
? geometry.layoutOf(store.doc, SIZES) : layout;
|
|
3354
|
-
renderLanes(store.doc, l);
|
|
3355
|
-
|
|
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);
|
|
3356
3164
|
}
|
|
3357
3165
|
|
|
3358
3166
|
/**
|
|
@@ -3461,7 +3269,7 @@
|
|
|
3461
3269
|
// was never reachable at all.
|
|
3462
3270
|
const dragged = edges.filter(function (e) { return e.index === handle.index; })[0];
|
|
3463
3271
|
if (dragged !== undefined && dragged.edge.from === dragged.edge.to) {
|
|
3464
|
-
say('這是一條 self-loop(起點跟終點在同一格),兩端的把手疊在同一個位置:' +
|
|
3272
|
+
panels.say('這是一條 self-loop(起點跟終點在同一格),兩端的把手疊在同一個位置:' +
|
|
3465
3273
|
'只有拖得到的這一端可以搬,另一端目前搬不到。');
|
|
3466
3274
|
}
|
|
3467
3275
|
endpointDrag = { index: handle.index, end: handle.end, at: null, cell: null };
|
|
@@ -3480,7 +3288,26 @@
|
|
|
3480
3288
|
// again would be the same needless cost the existing
|
|
3481
3289
|
// `hadSelectedEdge`-gated branch further down in this function
|
|
3482
3290
|
// already avoids.
|
|
3483
|
-
|
|
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);
|
|
3484
3311
|
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3485
3312
|
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3486
3313
|
return; // 不塗、不選格
|
|
@@ -3557,7 +3384,7 @@
|
|
|
3557
3384
|
// reasoning as the `'blocked'` branch just below it.
|
|
3558
3385
|
if (busTarget === 'blocked') {
|
|
3559
3386
|
ev.preventDefault();
|
|
3560
|
-
say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3387
|
+
panels.say('這一格接在某個 bus 值後面,但看不出屬於哪一個,沒有欄位可以編輯。');
|
|
3561
3388
|
return;
|
|
3562
3389
|
}
|
|
3563
3390
|
if (busTarget !== null) {
|
|
@@ -3565,10 +3392,34 @@
|
|
|
3565
3392
|
const busHadSelectedEdge = selectedEdge !== null;
|
|
3566
3393
|
selectedEdge = null;
|
|
3567
3394
|
cursor = { laneIndex: busAt.laneIndex, cycle: busTarget.cycle };
|
|
3568
|
-
selection = {
|
|
3395
|
+
selection = {
|
|
3396
|
+
laneIndex: busAt.laneIndex, laneTo: busAt.laneIndex,
|
|
3397
|
+
from: busTarget.cycle, to: busTarget.cycle,
|
|
3398
|
+
};
|
|
3569
3399
|
canvas.focus();
|
|
3570
3400
|
if (busHadSelectedEdge) render();
|
|
3571
|
-
else
|
|
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
|
+
}
|
|
3572
3423
|
openDataEdit(busAt.laneIndex, busTarget.cycle);
|
|
3573
3424
|
return;
|
|
3574
3425
|
}
|
|
@@ -3609,7 +3460,10 @@
|
|
|
3609
3460
|
}
|
|
3610
3461
|
ev.preventDefault();
|
|
3611
3462
|
drag = { laneIndex: at.laneIndex, from: at.cycle, to: at.cycle };
|
|
3612
|
-
selection = {
|
|
3463
|
+
selection = {
|
|
3464
|
+
laneIndex: at.laneIndex, laneTo: at.laneIndex,
|
|
3465
|
+
from: at.cycle, to: at.cycle,
|
|
3466
|
+
};
|
|
3613
3467
|
// The keyboard follows the pointer, for two reasons that are really one:
|
|
3614
3468
|
// the two cursors may not disagree about which cell is current, and
|
|
3615
3469
|
// `preventDefault()` above suppresses the focus a press would otherwise
|
|
@@ -3623,7 +3477,28 @@
|
|
|
3623
3477
|
// inspector row, which lives in the toolbar `renderCanvas` never
|
|
3624
3478
|
// touches.
|
|
3625
3479
|
if (hadSelectedEdge) render();
|
|
3626
|
-
else
|
|
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
|
+
}
|
|
3627
3502
|
d.addEventListener('mousemove', onCanvasMove, true);
|
|
3628
3503
|
d.addEventListener('mouseup', onCanvasUp, true);
|
|
3629
3504
|
}
|
|
@@ -3644,9 +3519,37 @@
|
|
|
3644
3519
|
endpointDrag = { index: endpointDrag.index, end: endpointDrag.end,
|
|
3645
3520
|
at: at.laneIndex, cell: at.cycle };
|
|
3646
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.
|
|
3647
3531
|
// The document is untouched during the drag (rule 3): this repaints
|
|
3648
3532
|
// the preview only, never a `commit`.
|
|
3649
|
-
|
|
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);
|
|
3650
3553
|
return;
|
|
3651
3554
|
}
|
|
3652
3555
|
if (edgeMode === 'dragging') {
|
|
@@ -3659,10 +3562,38 @@
|
|
|
3659
3562
|
// preview line — exactly where it last was, rather than snapping the
|
|
3660
3563
|
// line away: the drop target the user is currently over is still the
|
|
3661
3564
|
// last cell the pointer was actually on top of.
|
|
3662
|
-
if (at !== null)
|
|
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
|
+
}
|
|
3663
3575
|
// The document is untouched during the drag (rule 2): this repaints
|
|
3664
3576
|
// the preview line only, never a `commit`.
|
|
3665
|
-
|
|
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);
|
|
3666
3597
|
return;
|
|
3667
3598
|
}
|
|
3668
3599
|
if (drag === null) return;
|
|
@@ -3679,11 +3610,33 @@
|
|
|
3679
3610
|
// The lane is fixed by the press: a drag that strays onto a neighbour is
|
|
3680
3611
|
// still painting the lane it started on.
|
|
3681
3612
|
drag.to = at.cycle;
|
|
3682
|
-
selection = {
|
|
3613
|
+
selection = {
|
|
3614
|
+
laneIndex: drag.laneIndex, laneTo: drag.laneIndex,
|
|
3615
|
+
from: drag.from, to: drag.to,
|
|
3616
|
+
};
|
|
3683
3617
|
// The drag's live end is where the cursor is, so a paint continued with
|
|
3684
3618
|
// Shift+←/→ after the button comes up carries on from the same cell.
|
|
3685
3619
|
cursor = { laneIndex: drag.laneIndex, cycle: drag.to };
|
|
3686
|
-
|
|
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);
|
|
3687
3640
|
}
|
|
3688
3641
|
|
|
3689
3642
|
function onCanvasUp() {
|
|
@@ -3837,11 +3790,85 @@
|
|
|
3837
3790
|
render();
|
|
3838
3791
|
}
|
|
3839
3792
|
|
|
3840
|
-
/**
|
|
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
|
+
*/
|
|
3841
3812
|
function cellFromEvent(ev, layout) {
|
|
3842
3813
|
if (canvas === null) return null;
|
|
3843
3814
|
const box = canvas.getBoundingClientRect();
|
|
3844
|
-
|
|
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);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
/**
|
|
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.
|
|
3828
|
+
*
|
|
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.
|
|
3841
|
+
*/
|
|
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);
|
|
3845
3872
|
}
|
|
3846
3873
|
|
|
3847
3874
|
/**
|
|
@@ -3854,9 +3881,15 @@
|
|
|
3854
3881
|
*
|
|
3855
3882
|
* 1. `geometry.edgeHandleAt` — a handle is a small rect centred on an
|
|
3856
3883
|
* endpoint, asked in the SAME layout-local space `cellFromEvent`
|
|
3857
|
-
* uses (the canvas's own `getBoundingClientRect()
|
|
3858
|
-
* is
|
|
3859
|
-
*
|
|
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
|
|
3860
3893
|
* edge that is actually SELECTED — `renderEdges` only DRAWS a
|
|
3861
3894
|
* handle on the selected edge, so its answer for any OTHER edge is a
|
|
3862
3895
|
* rectangle with nothing on screen, and letting that phantom
|
|
@@ -3975,48 +4008,6 @@
|
|
|
3975
4008
|
return isLabel(ev.target) ? ev.target : null;
|
|
3976
4009
|
}
|
|
3977
4010
|
|
|
3978
|
-
// ── the preview ───────────────────────────────────────────────────────
|
|
3979
|
-
/**
|
|
3980
|
-
* The engine's own rendering of the very same document.
|
|
3981
|
-
*
|
|
3982
|
-
* This is the check on the drawing, which is the only reason it is on
|
|
3983
|
-
* screen. It is rendered from a plain copy because wavedrom writes into the
|
|
3984
|
-
* source object it is handed and the store's documents are frozen.
|
|
3985
|
-
*/
|
|
3986
|
-
function renderPreview(doc) {
|
|
3987
|
-
const engine = opts.wavedrom;
|
|
3988
|
-
if (engine === null || engine === undefined ||
|
|
3989
|
-
typeof engine.RenderWaveForm !== 'function') {
|
|
3990
|
-
previewHost.textContent = '(這一頁沒有載入 WaveDrom,預覽從缺)';
|
|
3991
|
-
previewWrap.setAttribute('data-wave-preview', 'absent');
|
|
3992
|
-
return;
|
|
3993
|
-
}
|
|
3994
|
-
try {
|
|
3995
|
-
// `notFirstSignal` means "the skin is already on this page, do not
|
|
3996
|
-
// emit it again". True whenever a WaveDrom render has already run here,
|
|
3997
|
-
// which is always in the flow that reaches this editor — the entry
|
|
3998
|
-
// affordance only appears over a RENDERED diagram. The fallback to
|
|
3999
|
-
// `false` is for the page where it somehow has not: emitting the skin
|
|
4000
|
-
// is only wasteful, while omitting one that is not there leaves every
|
|
4001
|
-
// `<use>` unresolvable and the preview blank, which is the one state
|
|
4002
|
-
// this panel may never be in.
|
|
4003
|
-
// `socket` is a bare, global, un-namespaced id that the engine's own
|
|
4004
|
-
// skin `<defs>` defines — so ask for it INSIDE a WaveDrom-rendered SVG's
|
|
4005
|
-
// defs rather than anywhere on the page. A document carrying inline
|
|
4006
|
-
// HTML with `id="socket"` would otherwise convince this that the skin is
|
|
4007
|
-
// present on a page where no diagram had rendered, and the preview would
|
|
4008
|
-
// come up blank: the one state this panel may never be in.
|
|
4009
|
-
const skinOnPage = d.querySelector('svg.WaveDrom defs #socket') !== null;
|
|
4010
|
-
engine.RenderWaveForm(PREVIEW_INDEX, plainCopy(doc, 0),
|
|
4011
|
-
PREVIEW_ID_PREFIX, skinOnPage);
|
|
4012
|
-
previewWrap.setAttribute('data-wave-preview', 'ok');
|
|
4013
|
-
previewWrap.setAttribute('data-wave-preview-skin', skinOnPage ? 'shared' : 'own');
|
|
4014
|
-
} catch (e) {
|
|
4015
|
-
previewHost.textContent = 'WaveDrom 畫不出這份文件:' + String(e && e.message);
|
|
4016
|
-
previewWrap.setAttribute('data-wave-preview', 'failed');
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
4011
|
// Mounted BEFORE the first paint, not after: the preview is rendered by the
|
|
4021
4012
|
// engine's own `RenderWaveForm`, which resolves its target with
|
|
4022
4013
|
// `getElementById` — an element that is not in the document yet is not
|
|
@@ -4029,8 +4020,24 @@
|
|
|
4029
4020
|
render();
|
|
4030
4021
|
// The opening line described a pointer and only a pointer, which is how a
|
|
4031
4022
|
// surface ends up with no keyboard: the instructions never mentioned one.
|
|
4032
|
-
|
|
4033
|
-
|
|
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+左右鍵可以在轉態點之間跳。');
|
|
4034
4041
|
|
|
4035
4042
|
return {
|
|
4036
4043
|
el: overlay,
|
|
@@ -4043,8 +4050,6 @@
|
|
|
4043
4050
|
|
|
4044
4051
|
return {
|
|
4045
4052
|
createWaveEditor: createWaveEditor,
|
|
4046
|
-
brickOf: brickOf,
|
|
4047
|
-
groupSpansOf: groupSpansOf,
|
|
4048
4053
|
mountedInsideContent: mountedInsideContent,
|
|
4049
4054
|
BRUSHES: BRUSHES,
|
|
4050
4055
|
isBrushKey: isBrushKey,
|