@grida/svg-editor 1.0.0-alpha.1 → 1.0.0-alpha.12
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/README.md +184 -185
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/dom-BlMk07oX.mjs +3515 -0
- package/dist/dom-Cvm9Towu.js +3545 -0
- package/dist/dom-DCX-a8Kr.d.ts +57 -0
- package/dist/dom-DgB4f-TE.d.mts +59 -0
- package/dist/dom.d.mts +3 -16
- package/dist/dom.d.ts +3 -16
- package/dist/dom.js +5 -1
- package/dist/dom.mjs +2 -2
- package/dist/editor-BH03X8cX.d.mts +1139 -0
- package/dist/editor-Bd4-VCEJ.d.ts +1139 -0
- package/dist/{editor-DQWUWrVZ.js → editor-CdyC3uAe.js} +1205 -388
- package/dist/{editor-B5z-gTML.mjs → editor-DtuRIs-Q.mjs} +1195 -372
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/index.mjs +3 -2
- package/dist/insertions-BJ-6o6o5.js +2399 -0
- package/dist/insertions-Okcuo-Ck.mjs +2176 -0
- package/dist/presets.d.mts +61 -0
- package/dist/presets.d.ts +61 -0
- package/dist/presets.js +61 -0
- package/dist/presets.mjs +55 -0
- package/dist/react.d.mts +94 -9
- package/dist/react.d.ts +94 -9
- package/dist/react.js +157 -19
- package/dist/react.mjs +147 -21
- package/package.json +11 -6
- package/dist/dom-CfP_ZURh.js +0 -963
- package/dist/dom-kA8NDuVh.mjs +0 -929
- package/dist/editor-CTtU2gu4.d.ts +0 -607
- package/dist/editor-JY7AQrR1.d.mts +0 -607
- package/dist/paint-DHq_3iwU.js +0 -509
- package/dist/paint-DuCg6Y-K.mjs +0 -461
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
require("./
|
|
2
|
-
const require_paint = require("./paint-DHq_3iwU.js");
|
|
1
|
+
const require_insertions = require("./insertions-BJ-6o6o5.js");
|
|
3
2
|
let _grida_history = require("@grida/history");
|
|
4
3
|
let _grida_keybinding = require("@grida/keybinding");
|
|
4
|
+
let _grida_cmath = require("@grida/cmath");
|
|
5
|
+
_grida_cmath = require_insertions.__toESM(_grida_cmath);
|
|
6
|
+
let _grida_svg_parser = require("@grida/svg/parser");
|
|
5
7
|
//#region src/commands/registry.ts
|
|
6
8
|
var CommandRegistry = class {
|
|
7
9
|
constructor() {
|
|
@@ -37,6 +39,25 @@ var CommandRegistry = class {
|
|
|
37
39
|
};
|
|
38
40
|
//#endregion
|
|
39
41
|
//#region src/commands/defaults.ts
|
|
42
|
+
/** Command id for `tool.set`. Bound to V/R/O/L in `keymap/defaults.ts`. */
|
|
43
|
+
const TOOL_SET = "tool.set";
|
|
44
|
+
/**
|
|
45
|
+
* The headless default `transform.nudge` handler. Exported so a host
|
|
46
|
+
* surface that overrides nudge (e.g. for faux-snap UX) can restore the
|
|
47
|
+
* default on teardown — the registry doesn't stack handlers, so a plain
|
|
48
|
+
* unregister leaves the slot empty.
|
|
49
|
+
*/
|
|
50
|
+
function default_nudge_handler(editor) {
|
|
51
|
+
return (args) => {
|
|
52
|
+
if (editor.state.selection.length === 0) return false;
|
|
53
|
+
const { dx, dy } = args;
|
|
54
|
+
editor.commands.nudge({
|
|
55
|
+
dx,
|
|
56
|
+
dy
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
40
61
|
function registerDefaultCommands(reg, editor) {
|
|
41
62
|
reg.register("history.undo", () => {
|
|
42
63
|
if (!editor.state.can_undo) return false;
|
|
@@ -58,6 +79,46 @@ function registerDefaultCommands(reg, editor) {
|
|
|
58
79
|
editor.commands.remove();
|
|
59
80
|
return true;
|
|
60
81
|
});
|
|
82
|
+
reg.register("selection.group", () => {
|
|
83
|
+
if (editor.state.mode !== "select") return false;
|
|
84
|
+
if (editor.state.selection.length === 0) return false;
|
|
85
|
+
return editor.commands.group();
|
|
86
|
+
});
|
|
87
|
+
reg.register("selection.resize_to", (args) => {
|
|
88
|
+
if (editor.state.mode !== "select") return false;
|
|
89
|
+
if (editor.state.selection.length === 0) return false;
|
|
90
|
+
const target = args;
|
|
91
|
+
return editor.commands.resize_to(target);
|
|
92
|
+
});
|
|
93
|
+
reg.register("selection.rotate", (args) => {
|
|
94
|
+
if (editor.state.mode !== "select") return false;
|
|
95
|
+
if (editor.state.selection.length === 0) return false;
|
|
96
|
+
const a = args;
|
|
97
|
+
return editor.commands.rotate(a.angle, { pivot: a.pivot });
|
|
98
|
+
});
|
|
99
|
+
reg.register("selection.rotate_to", (args) => {
|
|
100
|
+
if (editor.state.mode !== "select") return false;
|
|
101
|
+
if (editor.state.selection.length === 0) return false;
|
|
102
|
+
const a = args;
|
|
103
|
+
return editor.commands.rotate_to(a.angle, { pivot: a.pivot });
|
|
104
|
+
});
|
|
105
|
+
reg.register("selection.flatten_transform", () => {
|
|
106
|
+
if (editor.state.mode !== "select") return false;
|
|
107
|
+
if (editor.state.selection.length === 0) return false;
|
|
108
|
+
return editor.commands.flatten_transform();
|
|
109
|
+
});
|
|
110
|
+
reg.register("selection.all", () => {
|
|
111
|
+
if (editor.state.mode !== "select") return false;
|
|
112
|
+
return editor.commands.select_all();
|
|
113
|
+
});
|
|
114
|
+
reg.register("selection.sibling", (args) => {
|
|
115
|
+
if (editor.state.mode !== "select") return false;
|
|
116
|
+
return editor.commands.select_sibling(args);
|
|
117
|
+
});
|
|
118
|
+
reg.register("selection.align", (args) => {
|
|
119
|
+
if (editor.state.mode !== "select") return false;
|
|
120
|
+
return editor.commands.align(args);
|
|
121
|
+
});
|
|
61
122
|
reg.register("hierarchy.enter", () => {
|
|
62
123
|
if (editor.state.selection.length !== 1) return false;
|
|
63
124
|
const id = editor.state.selection[0];
|
|
@@ -75,20 +136,17 @@ function registerDefaultCommands(reg, editor) {
|
|
|
75
136
|
editor.commands.select(node.parent);
|
|
76
137
|
return true;
|
|
77
138
|
});
|
|
78
|
-
reg.register("transform.nudge", (
|
|
79
|
-
if (editor.state.selection.length === 0) return false;
|
|
80
|
-
const { dx, dy } = args;
|
|
81
|
-
editor.commands.translate({
|
|
82
|
-
dx,
|
|
83
|
-
dy
|
|
84
|
-
});
|
|
85
|
-
return true;
|
|
86
|
-
});
|
|
139
|
+
reg.register("transform.nudge", default_nudge_handler(editor));
|
|
87
140
|
reg.register("reorder", (args) => {
|
|
88
141
|
if (editor.state.selection.length !== 1) return false;
|
|
89
142
|
editor.commands.reorder(args);
|
|
90
143
|
return true;
|
|
91
144
|
});
|
|
145
|
+
reg.register(TOOL_SET, (args) => {
|
|
146
|
+
if (editor.state.mode !== "select") return false;
|
|
147
|
+
editor.set_tool(args);
|
|
148
|
+
return true;
|
|
149
|
+
});
|
|
92
150
|
}
|
|
93
151
|
//#endregion
|
|
94
152
|
//#region src/keymap/keymap.ts
|
|
@@ -112,15 +170,6 @@ const TEXT_INPUT_SAFE_MODS = new Set([
|
|
|
112
170
|
_grida_keybinding.KeyCode.Ctrl,
|
|
113
171
|
_grida_keybinding.KeyCode.Alt
|
|
114
172
|
]);
|
|
115
|
-
function is_text_input_focused() {
|
|
116
|
-
if (typeof document === "undefined") return false;
|
|
117
|
-
const el = document.activeElement;
|
|
118
|
-
if (!el) return false;
|
|
119
|
-
const tag = el.tagName;
|
|
120
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return true;
|
|
121
|
-
if (el.isContentEditable) return true;
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
173
|
var Keymap = class {
|
|
125
174
|
constructor(commands, platformGetter = _grida_keybinding.getKeyboardOS) {
|
|
126
175
|
this.commands = commands;
|
|
@@ -178,10 +227,35 @@ var Keymap = class {
|
|
|
178
227
|
return Array.from(seen);
|
|
179
228
|
}
|
|
180
229
|
/**
|
|
230
|
+
* Does the keymap have a binding that matches this event's chord —
|
|
231
|
+
* regardless of whether any handler would consume it? Hosts use this
|
|
232
|
+
* to decide whether to swallow the platform's default action (e.g.
|
|
233
|
+
* `event.preventDefault()` in the browser), so that an advertised
|
|
234
|
+
* shortcut like `Cmd+G` doesn't fall through to the browser's find
|
|
235
|
+
* bar even when the binding's handler rejects.
|
|
236
|
+
*
|
|
237
|
+
* Pure read; runs no handlers, no side effects. Honors the same
|
|
238
|
+
* text-input-focused guard `dispatch` uses, so a typing user's
|
|
239
|
+
* keystroke isn't "claimed" by an unrelated unmodified key.
|
|
240
|
+
*/
|
|
241
|
+
claims(event) {
|
|
242
|
+
const chunk = (0, _grida_keybinding.eventToChunk)(event);
|
|
243
|
+
if (chunk.keys.length === 0) return false;
|
|
244
|
+
const list = this.buckets.get((0, _grida_keybinding.chunkKey)(chunk));
|
|
245
|
+
if (!list || list.length === 0) return false;
|
|
246
|
+
if (require_insertions.is_text_input_focused() && !this.has_safe_mod(chunk.mods)) return false;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
181
250
|
* Match the event against bound chunks, then run candidates in chain
|
|
182
|
-
* order. Returns `true`
|
|
183
|
-
*
|
|
184
|
-
*
|
|
251
|
+
* order. Returns `true` on the first handler that consumes; returns
|
|
252
|
+
* `false` if nothing matched or all matches fell through.
|
|
253
|
+
*
|
|
254
|
+
* `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
|
|
255
|
+
* or touch the event in any way. The host decides what to do with the
|
|
256
|
+
* platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
|
|
257
|
+
* which prevents the platform default for advertised shortcuts even
|
|
258
|
+
* when the chain rejects. See README → `editor.keymap`.
|
|
185
259
|
*/
|
|
186
260
|
dispatch(event) {
|
|
187
261
|
const chunk = (0, _grida_keybinding.eventToChunk)(event);
|
|
@@ -189,13 +263,10 @@ var Keymap = class {
|
|
|
189
263
|
const hash = (0, _grida_keybinding.chunkKey)(chunk);
|
|
190
264
|
const list = this.buckets.get(hash);
|
|
191
265
|
if (!list || list.length === 0) return false;
|
|
192
|
-
const text_focused = is_text_input_focused();
|
|
266
|
+
const text_focused = require_insertions.is_text_input_focused();
|
|
193
267
|
for (const { binding } of list) {
|
|
194
268
|
if (text_focused && !this.has_safe_mod(chunk.mods)) continue;
|
|
195
|
-
if (this.commands.invoke(binding.command, binding.args))
|
|
196
|
-
event.preventDefault();
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
269
|
+
if (this.commands.invoke(binding.command, binding.args)) return true;
|
|
199
270
|
}
|
|
200
271
|
return false;
|
|
201
272
|
}
|
|
@@ -238,6 +309,7 @@ function compareEntries(a, b) {
|
|
|
238
309
|
* Same key, multiple meanings? Add multiple rows. The chain semantics
|
|
239
310
|
* (handler returns `false` when not applicable) handle the rest.
|
|
240
311
|
*/
|
|
312
|
+
const NUDGE_MEANINGFUL = _grida_keybinding.M.Shift;
|
|
241
313
|
const DEFAULT_BINDINGS = [
|
|
242
314
|
{
|
|
243
315
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyZ, _grida_keybinding.M.CtrlCmd),
|
|
@@ -263,6 +335,54 @@ const DEFAULT_BINDINGS = [
|
|
|
263
335
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Delete),
|
|
264
336
|
command: "selection.remove"
|
|
265
337
|
},
|
|
338
|
+
{
|
|
339
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyG, _grida_keybinding.M.CtrlCmd),
|
|
340
|
+
command: "selection.group"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyA, _grida_keybinding.M.CtrlCmd),
|
|
344
|
+
command: "selection.all"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Tab),
|
|
348
|
+
command: "selection.sibling",
|
|
349
|
+
args: "next"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Tab, _grida_keybinding.M.Shift),
|
|
353
|
+
command: "selection.sibling",
|
|
354
|
+
args: "prev"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyA, _grida_keybinding.M.Alt),
|
|
358
|
+
command: "selection.align",
|
|
359
|
+
args: "left"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyD, _grida_keybinding.M.Alt),
|
|
363
|
+
command: "selection.align",
|
|
364
|
+
args: "right"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyW, _grida_keybinding.M.Alt),
|
|
368
|
+
command: "selection.align",
|
|
369
|
+
args: "top"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyS, _grida_keybinding.M.Alt),
|
|
373
|
+
command: "selection.align",
|
|
374
|
+
args: "bottom"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyH, _grida_keybinding.M.Alt),
|
|
378
|
+
command: "selection.align",
|
|
379
|
+
args: "horizontal_centers"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyV, _grida_keybinding.M.Alt),
|
|
383
|
+
command: "selection.align",
|
|
384
|
+
args: "vertical_centers"
|
|
385
|
+
},
|
|
266
386
|
{
|
|
267
387
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Enter),
|
|
268
388
|
command: "hierarchy.enter"
|
|
@@ -272,7 +392,7 @@ const DEFAULT_BINDINGS = [
|
|
|
272
392
|
command: "hierarchy.exit"
|
|
273
393
|
},
|
|
274
394
|
{
|
|
275
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.LeftArrow),
|
|
395
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.LeftArrow, 0, NUDGE_MEANINGFUL),
|
|
276
396
|
command: "transform.nudge",
|
|
277
397
|
args: {
|
|
278
398
|
dx: -1,
|
|
@@ -280,7 +400,7 @@ const DEFAULT_BINDINGS = [
|
|
|
280
400
|
}
|
|
281
401
|
},
|
|
282
402
|
{
|
|
283
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.RightArrow),
|
|
403
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.RightArrow, 0, NUDGE_MEANINGFUL),
|
|
284
404
|
command: "transform.nudge",
|
|
285
405
|
args: {
|
|
286
406
|
dx: 1,
|
|
@@ -288,7 +408,7 @@ const DEFAULT_BINDINGS = [
|
|
|
288
408
|
}
|
|
289
409
|
},
|
|
290
410
|
{
|
|
291
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.UpArrow),
|
|
411
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.UpArrow, 0, NUDGE_MEANINGFUL),
|
|
292
412
|
command: "transform.nudge",
|
|
293
413
|
args: {
|
|
294
414
|
dx: 0,
|
|
@@ -296,7 +416,7 @@ const DEFAULT_BINDINGS = [
|
|
|
296
416
|
}
|
|
297
417
|
},
|
|
298
418
|
{
|
|
299
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.DownArrow),
|
|
419
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.DownArrow, 0, NUDGE_MEANINGFUL),
|
|
300
420
|
command: "transform.nudge",
|
|
301
421
|
args: {
|
|
302
422
|
dx: 0,
|
|
@@ -304,7 +424,7 @@ const DEFAULT_BINDINGS = [
|
|
|
304
424
|
}
|
|
305
425
|
},
|
|
306
426
|
{
|
|
307
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.LeftArrow, _grida_keybinding.M.Shift),
|
|
427
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.LeftArrow, _grida_keybinding.M.Shift, NUDGE_MEANINGFUL),
|
|
308
428
|
command: "transform.nudge",
|
|
309
429
|
args: {
|
|
310
430
|
dx: -10,
|
|
@@ -312,7 +432,7 @@ const DEFAULT_BINDINGS = [
|
|
|
312
432
|
}
|
|
313
433
|
},
|
|
314
434
|
{
|
|
315
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.RightArrow, _grida_keybinding.M.Shift),
|
|
435
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.RightArrow, _grida_keybinding.M.Shift, NUDGE_MEANINGFUL),
|
|
316
436
|
command: "transform.nudge",
|
|
317
437
|
args: {
|
|
318
438
|
dx: 10,
|
|
@@ -320,7 +440,7 @@ const DEFAULT_BINDINGS = [
|
|
|
320
440
|
}
|
|
321
441
|
},
|
|
322
442
|
{
|
|
323
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.UpArrow, _grida_keybinding.M.Shift),
|
|
443
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.UpArrow, _grida_keybinding.M.Shift, NUDGE_MEANINGFUL),
|
|
324
444
|
command: "transform.nudge",
|
|
325
445
|
args: {
|
|
326
446
|
dx: 0,
|
|
@@ -328,13 +448,42 @@ const DEFAULT_BINDINGS = [
|
|
|
328
448
|
}
|
|
329
449
|
},
|
|
330
450
|
{
|
|
331
|
-
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.DownArrow, _grida_keybinding.M.Shift),
|
|
451
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.DownArrow, _grida_keybinding.M.Shift, NUDGE_MEANINGFUL),
|
|
332
452
|
command: "transform.nudge",
|
|
333
453
|
args: {
|
|
334
454
|
dx: 0,
|
|
335
455
|
dy: 10
|
|
336
456
|
}
|
|
337
457
|
},
|
|
458
|
+
{
|
|
459
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyV),
|
|
460
|
+
command: TOOL_SET,
|
|
461
|
+
args: { type: "cursor" }
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyR),
|
|
465
|
+
command: TOOL_SET,
|
|
466
|
+
args: {
|
|
467
|
+
type: "insert",
|
|
468
|
+
tag: "rect"
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyO),
|
|
473
|
+
command: TOOL_SET,
|
|
474
|
+
args: {
|
|
475
|
+
type: "insert",
|
|
476
|
+
tag: "ellipse"
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyL),
|
|
481
|
+
command: TOOL_SET,
|
|
482
|
+
args: {
|
|
483
|
+
type: "insert",
|
|
484
|
+
tag: "line"
|
|
485
|
+
}
|
|
486
|
+
},
|
|
338
487
|
{
|
|
339
488
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.BracketRight),
|
|
340
489
|
command: "reorder",
|
|
@@ -361,16 +510,33 @@ function applyDefaultBindings(keymap) {
|
|
|
361
510
|
for (const b of DEFAULT_BINDINGS) keymap.bind(b);
|
|
362
511
|
}
|
|
363
512
|
//#endregion
|
|
513
|
+
//#region src/util/equal.ts
|
|
514
|
+
function array_shallow_equal(a, b) {
|
|
515
|
+
if (a === b) return true;
|
|
516
|
+
if (a.length !== b.length) return false;
|
|
517
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
364
521
|
//#region src/core/defs.ts
|
|
365
522
|
var GradientsRegistry = class {
|
|
366
523
|
constructor(doc) {
|
|
367
524
|
this.doc = doc;
|
|
368
525
|
this.listeners = /* @__PURE__ */ new Set();
|
|
369
526
|
this.counter = 0;
|
|
370
|
-
|
|
527
|
+
this._cached = null;
|
|
528
|
+
this._cached_by_id = /* @__PURE__ */ new Map();
|
|
529
|
+
this._dirty = true;
|
|
530
|
+
doc.on_change(() => {
|
|
531
|
+
this._dirty = true;
|
|
532
|
+
this.emit();
|
|
533
|
+
});
|
|
371
534
|
}
|
|
372
535
|
list() {
|
|
536
|
+
if (!this._dirty && this._cached) return this._cached;
|
|
373
537
|
const out = [];
|
|
538
|
+
let any_change = !this._cached;
|
|
539
|
+
const seen = /* @__PURE__ */ new Set();
|
|
374
540
|
const defs = this.find_defs_elements();
|
|
375
541
|
for (const def_id of defs) for (const child of this.doc.element_children_of(def_id)) {
|
|
376
542
|
const tag = this.doc.tag_of(child);
|
|
@@ -379,14 +545,34 @@ var GradientsRegistry = class {
|
|
|
379
545
|
if (!id) continue;
|
|
380
546
|
const definition = this.read_gradient(child, tag);
|
|
381
547
|
if (!definition) continue;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
548
|
+
const ref_count = this.count_refs(id);
|
|
549
|
+
const prev = this._cached_by_id.get(id);
|
|
550
|
+
if (prev && prev.ref_count === ref_count && gradient_definition_equals(prev.definition, definition)) out.push(prev);
|
|
551
|
+
else {
|
|
552
|
+
const entry = {
|
|
553
|
+
id,
|
|
554
|
+
definition,
|
|
555
|
+
ref_count
|
|
556
|
+
};
|
|
557
|
+
this._cached_by_id.set(id, entry);
|
|
558
|
+
out.push(entry);
|
|
559
|
+
any_change = true;
|
|
560
|
+
}
|
|
561
|
+
seen.add(id);
|
|
387
562
|
}
|
|
388
563
|
}
|
|
389
|
-
|
|
564
|
+
for (const id of this._cached_by_id.keys()) if (!seen.has(id)) {
|
|
565
|
+
this._cached_by_id.delete(id);
|
|
566
|
+
any_change = true;
|
|
567
|
+
}
|
|
568
|
+
if (!any_change && this._cached && array_shallow_equal(this._cached, out)) {
|
|
569
|
+
this._dirty = false;
|
|
570
|
+
return this._cached;
|
|
571
|
+
}
|
|
572
|
+
const frozen = Object.freeze(out);
|
|
573
|
+
this._cached = frozen;
|
|
574
|
+
this._dirty = false;
|
|
575
|
+
return frozen;
|
|
390
576
|
}
|
|
391
577
|
get(id) {
|
|
392
578
|
return this.list().find((g) => g.id === id) ?? null;
|
|
@@ -499,7 +685,7 @@ var GradientsRegistry = class {
|
|
|
499
685
|
};
|
|
500
686
|
}
|
|
501
687
|
write_gradient(node, def) {
|
|
502
|
-
for (const c of
|
|
688
|
+
for (const c of this.doc.children_of(node).slice()) this.doc.remove(c);
|
|
503
689
|
const set_num = (name, v) => {
|
|
504
690
|
this.doc.set_attr(node, name, v === void 0 ? null : String(v));
|
|
505
691
|
};
|
|
@@ -546,292 +732,73 @@ var GradientsRegistry = class {
|
|
|
546
732
|
function escape_regex(s) {
|
|
547
733
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
548
734
|
}
|
|
735
|
+
function gradient_definition_equals(a, b) {
|
|
736
|
+
if (a === b) return true;
|
|
737
|
+
if (a.kind !== b.kind) return false;
|
|
738
|
+
if (a.stops.length !== b.stops.length) return false;
|
|
739
|
+
for (let i = 0; i < a.stops.length; i++) {
|
|
740
|
+
const sa = a.stops[i];
|
|
741
|
+
const sb = b.stops[i];
|
|
742
|
+
if (sa.offset !== sb.offset || sa.color !== sb.color || sa.opacity !== sb.opacity) return false;
|
|
743
|
+
}
|
|
744
|
+
if (a.kind === "linear" && b.kind === "linear") return a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2 && a.gradient_units === b.gradient_units && a.spread_method === b.spread_method;
|
|
745
|
+
if (a.kind === "radial" && b.kind === "radial") return a.cx === b.cx && a.cy === b.cy && a.r === b.r && a.fx === b.fx && a.fy === b.fy && a.gradient_units === b.gradient_units && a.spread_method === b.spread_method;
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
549
748
|
function create_defs(doc) {
|
|
550
749
|
return { gradients: new GradientsRegistry(doc) };
|
|
551
750
|
}
|
|
552
|
-
const XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
553
|
-
const XMLNS_NS = "http://www.w3.org/2000/xmlns/";
|
|
554
|
-
let id_counter = 0;
|
|
555
|
-
function fresh_id() {
|
|
556
|
-
return `n${id_counter++}`;
|
|
557
|
-
}
|
|
558
|
-
function reset_id_counter() {
|
|
559
|
-
id_counter = 0;
|
|
560
|
-
}
|
|
561
|
-
function parse_svg(src) {
|
|
562
|
-
reset_id_counter();
|
|
563
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
564
|
-
const prolog = [];
|
|
565
|
-
const epilog = [];
|
|
566
|
-
let i = 0;
|
|
567
|
-
const n = src.length;
|
|
568
|
-
let root = null;
|
|
569
|
-
const open_stack = [];
|
|
570
|
-
/** ns prefix → uri, per ancestor scope (top of stack). */
|
|
571
|
-
const ns_stack = [new Map([["xml", XML_NS], ["xmlns", XMLNS_NS]])];
|
|
572
|
-
/** default ns per ancestor scope (top of stack). */
|
|
573
|
-
const default_ns_stack = [null];
|
|
574
|
-
function push_to_parent(node) {
|
|
575
|
-
nodes.set(node.id, node);
|
|
576
|
-
if (open_stack.length === 0) {
|
|
577
|
-
if (node.kind === "element" && root === null) return;
|
|
578
|
-
if (root === null) prolog.push(node);
|
|
579
|
-
else epilog.push(node);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
const parent = open_stack[open_stack.length - 1];
|
|
583
|
-
node.parent = parent.id;
|
|
584
|
-
parent.children.push(node.id);
|
|
585
|
-
}
|
|
586
|
-
while (i < n) {
|
|
587
|
-
if (src[i] === "<") {
|
|
588
|
-
if (src.startsWith("<!--", i)) {
|
|
589
|
-
const end = src.indexOf("-->", i + 4);
|
|
590
|
-
if (end === -1) throw new Error("unterminated comment");
|
|
591
|
-
const value = src.slice(i + 4, end);
|
|
592
|
-
push_to_parent({
|
|
593
|
-
kind: "comment",
|
|
594
|
-
id: fresh_id(),
|
|
595
|
-
parent: null,
|
|
596
|
-
value
|
|
597
|
-
});
|
|
598
|
-
i = end + 3;
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (src.startsWith("<![CDATA[", i)) {
|
|
602
|
-
const end = src.indexOf("]]>", i + 9);
|
|
603
|
-
if (end === -1) throw new Error("unterminated CDATA");
|
|
604
|
-
const value = src.slice(i + 9, end);
|
|
605
|
-
push_to_parent({
|
|
606
|
-
kind: "cdata",
|
|
607
|
-
id: fresh_id(),
|
|
608
|
-
parent: null,
|
|
609
|
-
value
|
|
610
|
-
});
|
|
611
|
-
i = end + 3;
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (src.startsWith("<!DOCTYPE", i) || src.startsWith("<!doctype", i)) {
|
|
615
|
-
let depth = 1;
|
|
616
|
-
let j = i + 9;
|
|
617
|
-
while (j < n && depth > 0) {
|
|
618
|
-
const c = src[j];
|
|
619
|
-
if (c === "<") depth++;
|
|
620
|
-
else if (c === ">") depth--;
|
|
621
|
-
if (depth === 0) break;
|
|
622
|
-
j++;
|
|
623
|
-
}
|
|
624
|
-
if (j >= n) throw new Error("unterminated doctype");
|
|
625
|
-
push_to_parent({
|
|
626
|
-
kind: "doctype",
|
|
627
|
-
id: fresh_id(),
|
|
628
|
-
parent: null,
|
|
629
|
-
value: src.slice(i + 9, j)
|
|
630
|
-
});
|
|
631
|
-
i = j + 1;
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
if (src.startsWith("<?", i)) {
|
|
635
|
-
const end = src.indexOf("?>", i + 2);
|
|
636
|
-
if (end === -1) throw new Error("unterminated PI");
|
|
637
|
-
const body = src.slice(i + 2, end);
|
|
638
|
-
const space = body.search(/\s/);
|
|
639
|
-
const target = space === -1 ? body : body.slice(0, space);
|
|
640
|
-
const value = space === -1 ? "" : body.slice(space + 1);
|
|
641
|
-
push_to_parent({
|
|
642
|
-
kind: "pi",
|
|
643
|
-
id: fresh_id(),
|
|
644
|
-
parent: null,
|
|
645
|
-
target,
|
|
646
|
-
value
|
|
647
|
-
});
|
|
648
|
-
i = end + 2;
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
if (src[i + 1] === "/") {
|
|
652
|
-
const end = src.indexOf(">", i + 2);
|
|
653
|
-
if (end === -1) throw new Error("unterminated end tag");
|
|
654
|
-
const open = open_stack.pop();
|
|
655
|
-
if (!open) throw new Error("unexpected end tag at " + i);
|
|
656
|
-
ns_stack.pop();
|
|
657
|
-
default_ns_stack.pop();
|
|
658
|
-
const m = src.slice(i + 2, end).match(/^(\s*)([^\s]+)(\s*)$/);
|
|
659
|
-
if (m) {
|
|
660
|
-
open.close_tag_leading = m[1];
|
|
661
|
-
open.close_tag_trailing = m[3];
|
|
662
|
-
}
|
|
663
|
-
i = end + 1;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
const start = i + 1;
|
|
667
|
-
let j = start;
|
|
668
|
-
while (j < n && !/[\s/>]/.test(src[j])) j++;
|
|
669
|
-
const raw_tag = src.slice(start, j);
|
|
670
|
-
const [prefix, local] = split_qname(raw_tag);
|
|
671
|
-
const { attrs, end_index, self_closing, trailing } = parse_attrs(src, j);
|
|
672
|
-
const new_ns_map = new Map(ns_stack[ns_stack.length - 1]);
|
|
673
|
-
let new_default_ns = default_ns_stack[default_ns_stack.length - 1];
|
|
674
|
-
for (const a of attrs) if (a.prefix === "xmlns") new_ns_map.set(a.local, a.value);
|
|
675
|
-
else if (a.prefix === null && a.local === "xmlns") new_default_ns = a.value;
|
|
676
|
-
for (const a of attrs) if (a.prefix === "xmlns" || a.prefix === null && a.local === "xmlns") a.ns = XMLNS_NS;
|
|
677
|
-
else if (a.prefix) a.ns = new_ns_map.get(a.prefix) ?? null;
|
|
678
|
-
else a.ns = null;
|
|
679
|
-
const element_ns = prefix ? new_ns_map.get(prefix) ?? null : new_default_ns;
|
|
680
|
-
const elem = {
|
|
681
|
-
kind: "element",
|
|
682
|
-
id: fresh_id(),
|
|
683
|
-
parent: null,
|
|
684
|
-
raw_tag,
|
|
685
|
-
prefix,
|
|
686
|
-
local,
|
|
687
|
-
ns: element_ns,
|
|
688
|
-
attrs,
|
|
689
|
-
children: [],
|
|
690
|
-
self_closing,
|
|
691
|
-
open_tag_trailing: trailing,
|
|
692
|
-
close_tag_leading: "",
|
|
693
|
-
close_tag_trailing: ""
|
|
694
|
-
};
|
|
695
|
-
push_to_parent(elem);
|
|
696
|
-
if (root === null) root = elem.id;
|
|
697
|
-
if (!self_closing) {
|
|
698
|
-
open_stack.push(elem);
|
|
699
|
-
ns_stack.push(new_ns_map);
|
|
700
|
-
default_ns_stack.push(new_default_ns);
|
|
701
|
-
}
|
|
702
|
-
i = end_index;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
const next = src.indexOf("<", i);
|
|
706
|
-
const end = next === -1 ? n : next;
|
|
707
|
-
const value = decode_entities(src.slice(i, end));
|
|
708
|
-
push_to_parent({
|
|
709
|
-
kind: "text",
|
|
710
|
-
id: fresh_id(),
|
|
711
|
-
parent: null,
|
|
712
|
-
value
|
|
713
|
-
});
|
|
714
|
-
i = end;
|
|
715
|
-
}
|
|
716
|
-
if (open_stack.length > 0) throw new Error(`unclosed element <${open_stack[open_stack.length - 1].raw_tag}>`);
|
|
717
|
-
if (root === null) throw new Error("no root element");
|
|
718
|
-
return {
|
|
719
|
-
prolog,
|
|
720
|
-
root,
|
|
721
|
-
epilog,
|
|
722
|
-
nodes
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
function split_qname(qname) {
|
|
726
|
-
const idx = qname.indexOf(":");
|
|
727
|
-
if (idx === -1) return [null, qname];
|
|
728
|
-
return [qname.slice(0, idx), qname.slice(idx + 1)];
|
|
729
|
-
}
|
|
730
|
-
function parse_attrs(src, from) {
|
|
731
|
-
const attrs = [];
|
|
732
|
-
let i = from;
|
|
733
|
-
let pre = "";
|
|
734
|
-
const n = src.length;
|
|
735
|
-
while (i < n) {
|
|
736
|
-
const ws_start = i;
|
|
737
|
-
while (i < n && /\s/.test(src[i])) i++;
|
|
738
|
-
pre += src.slice(ws_start, i);
|
|
739
|
-
if (i >= n) throw new Error("unterminated start tag");
|
|
740
|
-
const c = src[i];
|
|
741
|
-
if (c === "/") {
|
|
742
|
-
if (src[i + 1] !== ">") throw new Error("expected '/>' at " + i);
|
|
743
|
-
pre + "";
|
|
744
|
-
return {
|
|
745
|
-
attrs,
|
|
746
|
-
end_index: i + 2,
|
|
747
|
-
self_closing: true,
|
|
748
|
-
trailing: pre
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
if (c === ">") return {
|
|
752
|
-
attrs,
|
|
753
|
-
end_index: i + 1,
|
|
754
|
-
self_closing: false,
|
|
755
|
-
trailing: pre
|
|
756
|
-
};
|
|
757
|
-
const name_start = i;
|
|
758
|
-
while (i < n && !/[\s=/>]/.test(src[i])) i++;
|
|
759
|
-
const raw_name = src.slice(name_start, i);
|
|
760
|
-
let eq_trivia = "";
|
|
761
|
-
while (i < n && /\s/.test(src[i])) {
|
|
762
|
-
eq_trivia += src[i];
|
|
763
|
-
i++;
|
|
764
|
-
}
|
|
765
|
-
if (src[i] !== "=") {
|
|
766
|
-
const [prefix, local] = split_qname(raw_name);
|
|
767
|
-
attrs.push({
|
|
768
|
-
raw_name,
|
|
769
|
-
prefix,
|
|
770
|
-
local,
|
|
771
|
-
ns: null,
|
|
772
|
-
value: "",
|
|
773
|
-
pre,
|
|
774
|
-
eq_trivia,
|
|
775
|
-
quote: "\""
|
|
776
|
-
});
|
|
777
|
-
pre = "";
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
i++;
|
|
781
|
-
while (i < n && /\s/.test(src[i])) i++;
|
|
782
|
-
const quote = src[i];
|
|
783
|
-
if (quote !== "\"" && quote !== "'") throw new Error("expected attribute quote at " + i);
|
|
784
|
-
i++;
|
|
785
|
-
const val_start = i;
|
|
786
|
-
while (i < n && src[i] !== quote) i++;
|
|
787
|
-
if (i >= n) throw new Error("unterminated attribute value");
|
|
788
|
-
const raw_value = src.slice(val_start, i);
|
|
789
|
-
i++;
|
|
790
|
-
const [prefix, local] = split_qname(raw_name);
|
|
791
|
-
attrs.push({
|
|
792
|
-
raw_name,
|
|
793
|
-
prefix,
|
|
794
|
-
local,
|
|
795
|
-
ns: null,
|
|
796
|
-
value: decode_entities(raw_value),
|
|
797
|
-
pre,
|
|
798
|
-
eq_trivia,
|
|
799
|
-
quote
|
|
800
|
-
});
|
|
801
|
-
pre = "";
|
|
802
|
-
}
|
|
803
|
-
throw new Error("unterminated start tag");
|
|
804
|
-
}
|
|
805
|
-
const NAMED_ENTITIES = {
|
|
806
|
-
amp: "&",
|
|
807
|
-
lt: "<",
|
|
808
|
-
gt: ">",
|
|
809
|
-
quot: "\"",
|
|
810
|
-
apos: "'"
|
|
811
|
-
};
|
|
812
|
-
function decode_entities(s) {
|
|
813
|
-
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]*);/g, (_, ent) => {
|
|
814
|
-
if (ent.startsWith("#x") || ent.startsWith("#X")) return String.fromCodePoint(parseInt(ent.slice(2), 16));
|
|
815
|
-
if (ent.startsWith("#")) return String.fromCodePoint(parseInt(ent.slice(1), 10));
|
|
816
|
-
return NAMED_ENTITIES[ent] ?? `&${ent};`;
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
function encode_attr_value(value, quote) {
|
|
820
|
-
let out = value.replace(/&/g, "&").replace(/</g, "<");
|
|
821
|
-
out = quote === "\"" ? out.replace(/"/g, """) : out.replace(/'/g, "'");
|
|
822
|
-
return out;
|
|
823
|
-
}
|
|
824
|
-
function encode_text(value) {
|
|
825
|
-
return value.replace(/&/g, "&").replace(/</g, "<");
|
|
826
|
-
}
|
|
827
751
|
//#endregion
|
|
828
752
|
//#region src/core/document.ts
|
|
753
|
+
/**
|
|
754
|
+
* Attribute names whose writes can shift a node's rendered bounds.
|
|
755
|
+
* Membership drives `_geometry_version` bumps in `set_attr`. Only
|
|
756
|
+
* non-namespaced attribute names — namespaced writes (xlink:href, etc.)
|
|
757
|
+
* never bump because they're references, not geometry.
|
|
758
|
+
*
|
|
759
|
+
* Includes text-shaping attributes (font-*) because they re-shape glyph
|
|
760
|
+
* runs and change `<text>` bbox.
|
|
761
|
+
*/
|
|
762
|
+
const GEOMETRY_ATTRS = new Set([
|
|
763
|
+
"x",
|
|
764
|
+
"y",
|
|
765
|
+
"x1",
|
|
766
|
+
"y1",
|
|
767
|
+
"x2",
|
|
768
|
+
"y2",
|
|
769
|
+
"cx",
|
|
770
|
+
"cy",
|
|
771
|
+
"width",
|
|
772
|
+
"height",
|
|
773
|
+
"r",
|
|
774
|
+
"rx",
|
|
775
|
+
"ry",
|
|
776
|
+
"points",
|
|
777
|
+
"d",
|
|
778
|
+
"transform",
|
|
779
|
+
"viewBox",
|
|
780
|
+
"font-size",
|
|
781
|
+
"font-family",
|
|
782
|
+
"font-weight",
|
|
783
|
+
"font-style",
|
|
784
|
+
"text-anchor",
|
|
785
|
+
"dx",
|
|
786
|
+
"dy",
|
|
787
|
+
"rotate",
|
|
788
|
+
"textLength",
|
|
789
|
+
"lengthAdjust",
|
|
790
|
+
"pathLength",
|
|
791
|
+
"marker-start",
|
|
792
|
+
"marker-mid",
|
|
793
|
+
"marker-end"
|
|
794
|
+
]);
|
|
829
795
|
var SvgDocument = class SvgDocument {
|
|
830
796
|
constructor(svg) {
|
|
831
797
|
this.listeners = /* @__PURE__ */ new Set();
|
|
832
798
|
this._structure_version = 0;
|
|
799
|
+
this._geometry_version = 0;
|
|
833
800
|
this.source = svg;
|
|
834
|
-
const parsed = parse_svg(svg);
|
|
801
|
+
const parsed = (0, _grida_svg_parser.parse_svg)(svg);
|
|
835
802
|
this.original = parsed;
|
|
836
803
|
this.nodes = parsed.nodes;
|
|
837
804
|
this.prolog = parsed.prolog;
|
|
@@ -843,25 +810,27 @@ var SvgDocument = class SvgDocument {
|
|
|
843
810
|
}
|
|
844
811
|
/** Reload from the original parse, discarding all edits. */
|
|
845
812
|
reset_to_original() {
|
|
846
|
-
const parsed = parse_svg(this.source);
|
|
813
|
+
const parsed = (0, _grida_svg_parser.parse_svg)(this.source);
|
|
847
814
|
this.original = parsed;
|
|
848
815
|
this.nodes = parsed.nodes;
|
|
849
816
|
this.prolog = parsed.prolog;
|
|
850
817
|
this.epilog = parsed.epilog;
|
|
851
818
|
this.root = parsed.root;
|
|
852
819
|
this._structure_version++;
|
|
820
|
+
this._geometry_version++;
|
|
853
821
|
this.emit();
|
|
854
822
|
}
|
|
855
823
|
/** Replace document with new svg source (clears edits + history-owned state). */
|
|
856
824
|
load(svg) {
|
|
857
825
|
this.source = svg;
|
|
858
|
-
const parsed = parse_svg(svg);
|
|
826
|
+
const parsed = (0, _grida_svg_parser.parse_svg)(svg);
|
|
859
827
|
this.original = parsed;
|
|
860
828
|
this.nodes = parsed.nodes;
|
|
861
829
|
this.prolog = parsed.prolog;
|
|
862
830
|
this.epilog = parsed.epilog;
|
|
863
831
|
this.root = parsed.root;
|
|
864
832
|
this._structure_version++;
|
|
833
|
+
this._geometry_version++;
|
|
865
834
|
this.emit();
|
|
866
835
|
}
|
|
867
836
|
on_change(fn) {
|
|
@@ -872,6 +841,10 @@ var SvgDocument = class SvgDocument {
|
|
|
872
841
|
get structure_version() {
|
|
873
842
|
return this._structure_version;
|
|
874
843
|
}
|
|
844
|
+
/** See `_geometry_version` for what this counter signals. */
|
|
845
|
+
get geometry_version() {
|
|
846
|
+
return this._geometry_version;
|
|
847
|
+
}
|
|
875
848
|
emit() {
|
|
876
849
|
for (const fn of this.listeners) fn();
|
|
877
850
|
}
|
|
@@ -924,6 +897,48 @@ var SvgDocument = class SvgDocument {
|
|
|
924
897
|
}
|
|
925
898
|
return false;
|
|
926
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* Filter a selection down to its **subtree roots** — drop any id whose
|
|
902
|
+
* ancestor is also in the input set.
|
|
903
|
+
*
|
|
904
|
+
* Mirrors `pruneNestedNodes` in the main canvas editor's query module
|
|
905
|
+
* ([editor/grida-canvas/query/index.ts:138](../../../../editor/grida-canvas/query/index.ts)) and shares its UX motivation:
|
|
906
|
+
* when a parent and a descendant are both selected, only the parent
|
|
907
|
+
* should drive multi-node mutations — otherwise the descendant
|
|
908
|
+
* accumulates the transform twice (once via the parent's `transform`,
|
|
909
|
+
* once via its own attribute write). Required for `commands.remove`
|
|
910
|
+
* (avoids re-attaching detached descendants on undo) and any multi-
|
|
911
|
+
* member translate path (avoids 2× drift for the Bar-chart marquee
|
|
912
|
+
* case).
|
|
913
|
+
*
|
|
914
|
+
* Order: preserves the input order for retained ids. Duplicates in
|
|
915
|
+
* the input are not deduplicated — callers are responsible (the
|
|
916
|
+
* editor's `commands.select` already dedupes).
|
|
917
|
+
*
|
|
918
|
+
* Performance: `O(n × depth)`. Builds a `Set` over the input once,
|
|
919
|
+
* then walks each id's ancestor chain at most once. The main editor's
|
|
920
|
+
* version is `O(n² × depth)` (per-pair `isAncestor`) — fine at typical
|
|
921
|
+
* selection sizes (a few dozen), worth winning here for free since
|
|
922
|
+
* `parent_of` is `O(1)` on our parent-map.
|
|
923
|
+
*/
|
|
924
|
+
prune_nested_nodes(ids) {
|
|
925
|
+
if (ids.length <= 1) return [...ids];
|
|
926
|
+
const set = new Set(ids);
|
|
927
|
+
const out = [];
|
|
928
|
+
for (const id of ids) {
|
|
929
|
+
let nested = false;
|
|
930
|
+
let cur = this.parent_of(id);
|
|
931
|
+
while (cur !== null) {
|
|
932
|
+
if (set.has(cur)) {
|
|
933
|
+
nested = true;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
cur = this.parent_of(cur);
|
|
937
|
+
}
|
|
938
|
+
if (!nested) out.push(id);
|
|
939
|
+
}
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
927
942
|
all_nodes() {
|
|
928
943
|
const out = [];
|
|
929
944
|
const walk = (id) => {
|
|
@@ -961,18 +976,20 @@ var SvgDocument = class SvgDocument {
|
|
|
961
976
|
const n = this.nodes.get(id);
|
|
962
977
|
if (!n || n.kind !== "element") return;
|
|
963
978
|
const structural = name === "id";
|
|
979
|
+
const geometry = ns === null && GEOMETRY_ATTRS.has(name);
|
|
964
980
|
for (let i = 0; i < n.attrs.length; i++) {
|
|
965
981
|
const a = n.attrs[i];
|
|
966
982
|
if (a.local === name && (ns === null || a.ns === ns)) {
|
|
967
983
|
if (value === null) n.attrs.splice(i, 1);
|
|
968
984
|
else a.value = value;
|
|
969
985
|
if (structural) this._structure_version++;
|
|
986
|
+
if (geometry) this._geometry_version++;
|
|
970
987
|
this.emit();
|
|
971
988
|
return;
|
|
972
989
|
}
|
|
973
990
|
}
|
|
974
991
|
if (value !== null) {
|
|
975
|
-
const prefix = ns ===
|
|
992
|
+
const prefix = ns === _grida_svg_parser.XLINK_NS ? "xlink" : null;
|
|
976
993
|
n.attrs.push({
|
|
977
994
|
raw_name: prefix ? `${prefix}:${name}` : name,
|
|
978
995
|
prefix,
|
|
@@ -984,6 +1001,7 @@ var SvgDocument = class SvgDocument {
|
|
|
984
1001
|
quote: "\""
|
|
985
1002
|
});
|
|
986
1003
|
if (structural) this._structure_version++;
|
|
1004
|
+
if (geometry) this._geometry_version++;
|
|
987
1005
|
this.emit();
|
|
988
1006
|
}
|
|
989
1007
|
}
|
|
@@ -1022,6 +1040,25 @@ var SvgDocument = class SvgDocument {
|
|
|
1022
1040
|
if (!style) return [];
|
|
1023
1041
|
return parse_inline_style(style);
|
|
1024
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
1045
|
+
*
|
|
1046
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
1047
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
1048
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
1049
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
1050
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
1051
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
1052
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
1053
|
+
* or its parent text.
|
|
1054
|
+
*/
|
|
1055
|
+
is_text_edit_target(id) {
|
|
1056
|
+
const n = this.nodes.get(id);
|
|
1057
|
+
if (!n || n.kind !== "element") return false;
|
|
1058
|
+
if (n.local !== "text" && n.local !== "tspan") return false;
|
|
1059
|
+
for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1025
1062
|
text_of(id) {
|
|
1026
1063
|
const n = this.nodes.get(id);
|
|
1027
1064
|
if (!n || n.kind !== "element") return "";
|
|
@@ -1049,6 +1086,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1049
1086
|
n.children.push(text_id);
|
|
1050
1087
|
}
|
|
1051
1088
|
this._structure_version++;
|
|
1089
|
+
this._geometry_version++;
|
|
1052
1090
|
this.emit();
|
|
1053
1091
|
}
|
|
1054
1092
|
insert(id, parent, before) {
|
|
@@ -1067,6 +1105,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1067
1105
|
else parent_node.children.splice(ix, 0, id);
|
|
1068
1106
|
node.parent = parent;
|
|
1069
1107
|
this._structure_version++;
|
|
1108
|
+
this._geometry_version++;
|
|
1070
1109
|
this.emit();
|
|
1071
1110
|
}
|
|
1072
1111
|
remove(id) {
|
|
@@ -1078,6 +1117,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1078
1117
|
if (i >= 0) parent.children.splice(i, 1);
|
|
1079
1118
|
n.parent = null;
|
|
1080
1119
|
this._structure_version++;
|
|
1120
|
+
this._geometry_version++;
|
|
1081
1121
|
this.emit();
|
|
1082
1122
|
}
|
|
1083
1123
|
/** Create a new element node and register it (not yet inserted). */
|
|
@@ -1112,7 +1152,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1112
1152
|
}
|
|
1113
1153
|
emit_node(n) {
|
|
1114
1154
|
switch (n.kind) {
|
|
1115
|
-
case "text": return encode_text(n.value);
|
|
1155
|
+
case "text": return (0, _grida_svg_parser.encode_text)(n.value);
|
|
1116
1156
|
case "comment": return `<!--${n.value}-->`;
|
|
1117
1157
|
case "cdata": return `<![CDATA[${n.value}]]>`;
|
|
1118
1158
|
case "pi": {
|
|
@@ -1139,7 +1179,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1139
1179
|
}
|
|
1140
1180
|
}
|
|
1141
1181
|
emit_attr(a) {
|
|
1142
|
-
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.quote}${encode_attr_value(a.value, a.quote)}${a.quote}`;
|
|
1182
|
+
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.quote}${(0, _grida_svg_parser.encode_attr_value)(a.value, a.quote)}${a.quote}`;
|
|
1143
1183
|
}
|
|
1144
1184
|
};
|
|
1145
1185
|
function parse_inline_style(s) {
|
|
@@ -1158,6 +1198,56 @@ function parse_inline_style(s) {
|
|
|
1158
1198
|
return out;
|
|
1159
1199
|
}
|
|
1160
1200
|
//#endregion
|
|
1201
|
+
//#region src/core/align.ts
|
|
1202
|
+
/**
|
|
1203
|
+
* Compute per-member translation deltas to align `members` against `target`.
|
|
1204
|
+
*
|
|
1205
|
+
* Convention (matches Figma menu labels and `cmath.rect.alignA`):
|
|
1206
|
+
* - `left` / `right` → shift X so each left/right edge matches target's
|
|
1207
|
+
* - `top` / `bottom` → shift Y so each top/bottom edge matches target's
|
|
1208
|
+
* - `horizontal_centers` → shift X so each center-X matches target's center-X
|
|
1209
|
+
* - `vertical_centers` → shift Y so each center-Y matches target's center-Y
|
|
1210
|
+
*
|
|
1211
|
+
* Members already at the target position are omitted from the returned map —
|
|
1212
|
+
* callers iterate non-zero deltas only.
|
|
1213
|
+
*/
|
|
1214
|
+
function compute_align_deltas(members, target, direction) {
|
|
1215
|
+
const out = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const m of members) {
|
|
1217
|
+
const d = delta_for(m.bbox, target, direction);
|
|
1218
|
+
if (d.x !== 0 || d.y !== 0) out.set(m.id, d);
|
|
1219
|
+
}
|
|
1220
|
+
return out;
|
|
1221
|
+
}
|
|
1222
|
+
function delta_for(bbox, target, direction) {
|
|
1223
|
+
switch (direction) {
|
|
1224
|
+
case "left": return {
|
|
1225
|
+
x: target.x - bbox.x,
|
|
1226
|
+
y: 0
|
|
1227
|
+
};
|
|
1228
|
+
case "right": return {
|
|
1229
|
+
x: target.x + target.width - (bbox.x + bbox.width),
|
|
1230
|
+
y: 0
|
|
1231
|
+
};
|
|
1232
|
+
case "top": return {
|
|
1233
|
+
x: 0,
|
|
1234
|
+
y: target.y - bbox.y
|
|
1235
|
+
};
|
|
1236
|
+
case "bottom": return {
|
|
1237
|
+
x: 0,
|
|
1238
|
+
y: target.y + target.height - (bbox.y + bbox.height)
|
|
1239
|
+
};
|
|
1240
|
+
case "horizontal_centers": return {
|
|
1241
|
+
x: target.x + target.width / 2 - (bbox.x + bbox.width / 2),
|
|
1242
|
+
y: 0
|
|
1243
|
+
};
|
|
1244
|
+
case "vertical_centers": return {
|
|
1245
|
+
x: 0,
|
|
1246
|
+
y: target.y + target.height / 2 - (bbox.y + bbox.height / 2)
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
//#endregion
|
|
1161
1251
|
//#region src/core/properties.ts
|
|
1162
1252
|
/** SVG properties that inherit per SVG 2 §6 (subset; the common ones). */
|
|
1163
1253
|
const INHERITED = new Set([
|
|
@@ -1301,17 +1391,6 @@ function choose_write_carrier(doc, id, property) {
|
|
|
1301
1391
|
return "presentation_attribute";
|
|
1302
1392
|
}
|
|
1303
1393
|
//#endregion
|
|
1304
|
-
//#region src/types.ts
|
|
1305
|
-
const DEFAULT_STYLE = {
|
|
1306
|
-
chrome_color: "#2563eb",
|
|
1307
|
-
handle_size: 8,
|
|
1308
|
-
handle_fill: "#ffffff",
|
|
1309
|
-
handle_stroke: "#2563eb",
|
|
1310
|
-
endpoint_dot_radius: 5,
|
|
1311
|
-
selection_outline_width: 2,
|
|
1312
|
-
measurement_color: "#ff3a30"
|
|
1313
|
-
};
|
|
1314
|
-
//#endregion
|
|
1315
1394
|
//#region src/core/editor.ts
|
|
1316
1395
|
const PROVIDER_ID = "svg-editor";
|
|
1317
1396
|
/** Max characters in a synthesized display label before truncation. */
|
|
@@ -1323,29 +1402,46 @@ function createSvgEditor(opts) {
|
|
|
1323
1402
|
let selection = [];
|
|
1324
1403
|
let scope = null;
|
|
1325
1404
|
let mode = "select";
|
|
1405
|
+
let tool = require_insertions.TOOL_CURSOR;
|
|
1326
1406
|
let version = 0;
|
|
1327
1407
|
/** Document-edit counter — only bumps on actual mutations, not selection. */
|
|
1328
1408
|
let doc_version = 0;
|
|
1329
1409
|
/** doc_version at the last load()/serialize(); compared to derive `dirty`. */
|
|
1330
1410
|
let baseline_doc_version = 0;
|
|
1411
|
+
/**
|
|
1412
|
+
* Bumps once per `editor.load(svg)` call. The constructor's initial parse
|
|
1413
|
+
* does NOT count — it's the "factory" state. Hosts subscribe via
|
|
1414
|
+
* `subscribe_with_selector(s => s.load_version, ...)` to react to fresh
|
|
1415
|
+
* document loads without firing on every edit.
|
|
1416
|
+
*/
|
|
1417
|
+
let load_version = 0;
|
|
1331
1418
|
let style = {
|
|
1332
|
-
...DEFAULT_STYLE,
|
|
1333
|
-
...opts.style
|
|
1419
|
+
...require_insertions.DEFAULT_STYLE,
|
|
1420
|
+
...opts.style
|
|
1334
1421
|
};
|
|
1335
1422
|
const providers = opts.providers ?? {};
|
|
1336
1423
|
const listeners = /* @__PURE__ */ new Set();
|
|
1337
1424
|
let attached_surface = null;
|
|
1425
|
+
/**
|
|
1426
|
+
* World-space geometry query provider. Set by the DOM surface on
|
|
1427
|
+
* attach (`editor._internal.set_geometry`); cleared on detach. Null
|
|
1428
|
+
* means no renderer is attached — bounds queries cannot be answered.
|
|
1429
|
+
*/
|
|
1430
|
+
let geometry_provider = null;
|
|
1338
1431
|
const modes = ["select", "edit-content"];
|
|
1339
1432
|
function snapshot() {
|
|
1340
1433
|
return Object.freeze({
|
|
1341
1434
|
selection,
|
|
1342
1435
|
scope,
|
|
1343
1436
|
mode,
|
|
1437
|
+
tool,
|
|
1344
1438
|
dirty: doc_version !== baseline_doc_version,
|
|
1345
1439
|
can_undo: history.stack.canUndo,
|
|
1346
1440
|
can_redo: history.stack.canRedo,
|
|
1347
1441
|
version,
|
|
1348
|
-
structure_version: doc.structure_version
|
|
1442
|
+
structure_version: doc.structure_version,
|
|
1443
|
+
geometry_version: doc.geometry_version,
|
|
1444
|
+
load_version
|
|
1349
1445
|
});
|
|
1350
1446
|
}
|
|
1351
1447
|
function emit() {
|
|
@@ -1356,8 +1452,18 @@ function createSvgEditor(opts) {
|
|
|
1356
1452
|
history.on("onChange", () => emit());
|
|
1357
1453
|
history.on("onUndo", () => emit());
|
|
1358
1454
|
history.on("onRedo", () => emit());
|
|
1455
|
+
let last_emitted_geometry_version = doc.geometry_version;
|
|
1456
|
+
const geometry_listeners = /* @__PURE__ */ new Set();
|
|
1457
|
+
const translate_commit_listeners = /* @__PURE__ */ new Set();
|
|
1458
|
+
const notify_translate_commit = () => {
|
|
1459
|
+
for (const cb of translate_commit_listeners) cb();
|
|
1460
|
+
};
|
|
1359
1461
|
doc.on_change(() => {
|
|
1360
1462
|
doc_version++;
|
|
1463
|
+
if (doc.geometry_version !== last_emitted_geometry_version) {
|
|
1464
|
+
last_emitted_geometry_version = doc.geometry_version;
|
|
1465
|
+
for (const cb of geometry_listeners) cb();
|
|
1466
|
+
}
|
|
1361
1467
|
});
|
|
1362
1468
|
function subscribe(fn) {
|
|
1363
1469
|
listeners.add(fn);
|
|
@@ -1377,16 +1483,29 @@ function createSvgEditor(opts) {
|
|
|
1377
1483
|
});
|
|
1378
1484
|
}
|
|
1379
1485
|
function set_selection(next) {
|
|
1486
|
+
if (next.length === selection.length) {
|
|
1487
|
+
let same = true;
|
|
1488
|
+
for (let i = 0; i < next.length; i++) if (next[i] !== selection[i]) {
|
|
1489
|
+
same = false;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
if (same) return;
|
|
1493
|
+
}
|
|
1380
1494
|
selection = Object.freeze([...next]);
|
|
1381
1495
|
emit();
|
|
1382
1496
|
}
|
|
1383
1497
|
function select(target, opts) {
|
|
1384
1498
|
const ids = typeof target === "string" ? [target] : [...target];
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1499
|
+
const mode = opts?.mode ?? "replace";
|
|
1500
|
+
if (mode === "replace") {
|
|
1501
|
+
set_selection(ids);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const next = new Set(selection);
|
|
1505
|
+
if (mode === "add") for (const id of ids) next.add(id);
|
|
1506
|
+
else for (const id of ids) if (next.has(id)) next.delete(id);
|
|
1507
|
+
else next.add(id);
|
|
1508
|
+
set_selection([...next]);
|
|
1390
1509
|
}
|
|
1391
1510
|
function deselect() {
|
|
1392
1511
|
set_selection([]);
|
|
@@ -1406,18 +1525,121 @@ function createSvgEditor(opts) {
|
|
|
1406
1525
|
mode = next;
|
|
1407
1526
|
emit();
|
|
1408
1527
|
}
|
|
1528
|
+
function tools_equal(a, b) {
|
|
1529
|
+
if (a.type !== b.type) return false;
|
|
1530
|
+
if (a.type === "cursor") return true;
|
|
1531
|
+
return b.type === "insert" && a.tag === b.tag;
|
|
1532
|
+
}
|
|
1533
|
+
function set_tool(next) {
|
|
1534
|
+
if (tools_equal(tool, next)) return;
|
|
1535
|
+
tool = next;
|
|
1536
|
+
emit();
|
|
1537
|
+
}
|
|
1538
|
+
const paint_cache = /* @__PURE__ */ new Map();
|
|
1539
|
+
const property_cache = /* @__PURE__ */ new Map();
|
|
1540
|
+
const properties_cache = /* @__PURE__ */ new Map();
|
|
1541
|
+
let tree_cache = null;
|
|
1542
|
+
const tree_node_pool = /* @__PURE__ */ new Map();
|
|
1543
|
+
function tree_snapshot() {
|
|
1544
|
+
const sv = doc.structure_version;
|
|
1545
|
+
if (tree_cache && tree_cache.structure_version === sv) return tree_cache.value;
|
|
1546
|
+
const map = /* @__PURE__ */ new Map();
|
|
1547
|
+
let any_change = !tree_cache;
|
|
1548
|
+
for (const id of doc.all_elements()) {
|
|
1549
|
+
const tag = doc.tag_of(id);
|
|
1550
|
+
const name = doc.get_attr(id, "id") ?? void 0;
|
|
1551
|
+
const parent = doc.parent_of(id);
|
|
1552
|
+
const children = doc.element_children_of(id);
|
|
1553
|
+
const pooled = tree_node_pool.get(id);
|
|
1554
|
+
if (pooled && pooled.tag === tag && pooled.name === name && pooled.parent === parent && array_shallow_equal(pooled.children, children)) {
|
|
1555
|
+
map.set(id, pooled);
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const node = {
|
|
1559
|
+
id,
|
|
1560
|
+
tag,
|
|
1561
|
+
name,
|
|
1562
|
+
parent,
|
|
1563
|
+
children
|
|
1564
|
+
};
|
|
1565
|
+
tree_node_pool.set(id, node);
|
|
1566
|
+
map.set(id, node);
|
|
1567
|
+
any_change = true;
|
|
1568
|
+
}
|
|
1569
|
+
for (const id of tree_node_pool.keys()) if (!map.has(id)) {
|
|
1570
|
+
tree_node_pool.delete(id);
|
|
1571
|
+
any_change = true;
|
|
1572
|
+
}
|
|
1573
|
+
if (!any_change && tree_cache) {
|
|
1574
|
+
tree_cache.structure_version = sv;
|
|
1575
|
+
return tree_cache.value;
|
|
1576
|
+
}
|
|
1577
|
+
const snap = {
|
|
1578
|
+
root: doc.root,
|
|
1579
|
+
nodes: map
|
|
1580
|
+
};
|
|
1581
|
+
tree_cache = {
|
|
1582
|
+
structure_version: sv,
|
|
1583
|
+
value: snap
|
|
1584
|
+
};
|
|
1585
|
+
return snap;
|
|
1586
|
+
}
|
|
1587
|
+
function node_property_cached(id, name) {
|
|
1588
|
+
const key = `${id}${name}`;
|
|
1589
|
+
const cached = property_cache.get(key);
|
|
1590
|
+
if (cached && cached.doc_version === doc_version) return cached.value;
|
|
1591
|
+
const next = read_property(doc, id, name);
|
|
1592
|
+
if (cached && property_value_equals(cached.value, next)) {
|
|
1593
|
+
cached.doc_version = doc_version;
|
|
1594
|
+
return cached.value;
|
|
1595
|
+
}
|
|
1596
|
+
property_cache.set(key, {
|
|
1597
|
+
doc_version,
|
|
1598
|
+
value: next
|
|
1599
|
+
});
|
|
1600
|
+
return next;
|
|
1601
|
+
}
|
|
1409
1602
|
function node_properties(id, names) {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
return
|
|
1603
|
+
const key = `${id}${names.join("")}`;
|
|
1604
|
+
const cached = properties_cache.get(key);
|
|
1605
|
+
if (cached && cached.doc_version === doc_version) return cached.value;
|
|
1606
|
+
const next = {};
|
|
1607
|
+
let changed = !cached;
|
|
1608
|
+
for (const name of names) {
|
|
1609
|
+
const v = node_property_cached(id, name);
|
|
1610
|
+
next[name] = v;
|
|
1611
|
+
if (cached && cached.value[name] !== v) changed = true;
|
|
1612
|
+
}
|
|
1613
|
+
if (cached && !changed) {
|
|
1614
|
+
cached.doc_version = doc_version;
|
|
1615
|
+
return cached.value;
|
|
1616
|
+
}
|
|
1617
|
+
const frozen = Object.freeze(next);
|
|
1618
|
+
properties_cache.set(key, {
|
|
1619
|
+
doc_version,
|
|
1620
|
+
value: frozen
|
|
1621
|
+
});
|
|
1622
|
+
return frozen;
|
|
1413
1623
|
}
|
|
1414
1624
|
function node_paint(id, channel) {
|
|
1625
|
+
const key = `${id}${channel}`;
|
|
1626
|
+
const cached = paint_cache.get(key);
|
|
1627
|
+
if (cached && cached.doc_version === doc_version) return cached.value;
|
|
1415
1628
|
const { declared, provenance } = resolve_declared(doc, id, channel);
|
|
1416
|
-
|
|
1629
|
+
const next = {
|
|
1417
1630
|
declared,
|
|
1418
|
-
computed:
|
|
1631
|
+
computed: require_insertions.parse_paint(declared),
|
|
1419
1632
|
provenance
|
|
1420
1633
|
};
|
|
1634
|
+
if (cached && paint_value_equals(cached.value, next)) {
|
|
1635
|
+
cached.doc_version = doc_version;
|
|
1636
|
+
return cached.value;
|
|
1637
|
+
}
|
|
1638
|
+
paint_cache.set(key, {
|
|
1639
|
+
doc_version,
|
|
1640
|
+
value: next
|
|
1641
|
+
});
|
|
1642
|
+
return next;
|
|
1421
1643
|
}
|
|
1422
1644
|
function write_property(id, name, value) {
|
|
1423
1645
|
if (choose_write_carrier(doc, id, name) === "inline_style") doc.set_style(id, name, value);
|
|
@@ -1485,12 +1707,12 @@ function createSvgEditor(opts) {
|
|
|
1485
1707
|
}
|
|
1486
1708
|
function set_paint(channel, paint) {
|
|
1487
1709
|
if (selection.length === 0) return;
|
|
1488
|
-
set_property(channel,
|
|
1710
|
+
set_property(channel, require_insertions.serialize_paint(paint));
|
|
1489
1711
|
}
|
|
1490
1712
|
function preview_paint(channel) {
|
|
1491
1713
|
const session = preview_property(channel);
|
|
1492
1714
|
return {
|
|
1493
|
-
update: (paint) => session.update(
|
|
1715
|
+
update: (paint) => session.update(require_insertions.serialize_paint(paint)),
|
|
1494
1716
|
commit: () => session.commit(),
|
|
1495
1717
|
discard: () => session.discard()
|
|
1496
1718
|
};
|
|
@@ -1503,29 +1725,427 @@ function createSvgEditor(opts) {
|
|
|
1503
1725
|
});
|
|
1504
1726
|
return { gradient_id };
|
|
1505
1727
|
}
|
|
1728
|
+
/** Shared one-shot translate runner. `stages` selects semantics — see
|
|
1729
|
+
* `core/translate-pipeline/README.md`'s "Stage lists per entry point". */
|
|
1730
|
+
function do_translate_oneshot(delta, stages, label) {
|
|
1731
|
+
if (selection.length === 0) return false;
|
|
1732
|
+
if (delta.dx === 0 && delta.dy === 0) return false;
|
|
1733
|
+
const { apply, revert } = require_insertions.prepare_translate_rpc({
|
|
1734
|
+
doc,
|
|
1735
|
+
ids: selection,
|
|
1736
|
+
delta: {
|
|
1737
|
+
x: delta.dx,
|
|
1738
|
+
y: delta.dy
|
|
1739
|
+
},
|
|
1740
|
+
options: {
|
|
1741
|
+
pixel_grid_quantum: style.snap_to_pixel_grid ? style.pixel_grid_size : null,
|
|
1742
|
+
snap_enabled: style.snap_enabled,
|
|
1743
|
+
snap_threshold_px: style.snap_threshold_px
|
|
1744
|
+
},
|
|
1745
|
+
emit,
|
|
1746
|
+
stages
|
|
1747
|
+
});
|
|
1748
|
+
apply();
|
|
1749
|
+
history.atomic(label, (tx) => {
|
|
1750
|
+
tx.push({
|
|
1751
|
+
providerId: PROVIDER_ID,
|
|
1752
|
+
apply,
|
|
1753
|
+
revert
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1506
1758
|
function translate(delta) {
|
|
1507
|
-
if (
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1759
|
+
if (do_translate_oneshot(delta, void 0, "translate")) notify_translate_commit();
|
|
1760
|
+
}
|
|
1761
|
+
function nudge(delta) {
|
|
1762
|
+
if (do_translate_oneshot(delta, require_insertions.STAGES_NUDGE, "nudge")) notify_translate_commit();
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* One-shot multi-member resize to an explicit target rect. Mirrors a
|
|
1766
|
+
* drag-resize gesture in mechanics — capture per-member baselines,
|
|
1767
|
+
* scale around the union's NW corner, translate the result so the
|
|
1768
|
+
* union NW lands at the requested position — but as a single
|
|
1769
|
+
* atomic step rather than a preview session.
|
|
1770
|
+
*
|
|
1771
|
+
* The function does its own geometry lookup via the
|
|
1772
|
+
* `geometry_provider` registered by the DOM surface. When no surface
|
|
1773
|
+
* is attached, the call is a no-op (returns `false`). Members whose
|
|
1774
|
+
* tag is not resizable are silently filtered.
|
|
1775
|
+
*
|
|
1776
|
+
* Revert restores the captured `transform` attribute and all
|
|
1777
|
+
* geometry attrs the apply step wrote — so a `<rect>` with an
|
|
1778
|
+
* existing `transform` round-trips cleanly. See `apply_translate`'s
|
|
1779
|
+
* `viaTransform` arm for why this matters.
|
|
1780
|
+
*/
|
|
1781
|
+
function resize_to(target, opts) {
|
|
1782
|
+
const ids = opts?.ids ?? selection;
|
|
1783
|
+
if (ids.length === 0) return false;
|
|
1784
|
+
if (!geometry_provider) return false;
|
|
1785
|
+
const members = [];
|
|
1786
|
+
for (const id of ids) {
|
|
1787
|
+
if (!require_insertions.is_resizable(doc.tag_of(id))) continue;
|
|
1788
|
+
const bbox = geometry_provider.bounds_of(id);
|
|
1789
|
+
if (!bbox) continue;
|
|
1790
|
+
members.push({
|
|
1791
|
+
id,
|
|
1792
|
+
rz: require_insertions.capture_resize_baseline(doc, id, bbox),
|
|
1793
|
+
tx_pre: require_insertions.capture_translate_baseline(doc, id),
|
|
1794
|
+
transform_pre: doc.get_attr(id, "transform"),
|
|
1795
|
+
bbox
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
if (members.length === 0) return false;
|
|
1799
|
+
const union = _grida_cmath.default.rect.union(members.map((m) => m.bbox));
|
|
1800
|
+
const sx = union.width === 0 ? 1 : target.width / union.width;
|
|
1801
|
+
const sy = union.height === 0 ? 1 : target.height / union.height;
|
|
1802
|
+
const origin = {
|
|
1803
|
+
x: union.x,
|
|
1804
|
+
y: union.y
|
|
1805
|
+
};
|
|
1806
|
+
const dx = target.x - union.x;
|
|
1807
|
+
const dy = target.y - union.y;
|
|
1808
|
+
const apply = () => {
|
|
1809
|
+
for (const m of members) require_insertions.apply_resize(doc, m.id, m.rz, sx, sy, origin);
|
|
1810
|
+
if (dx !== 0 || dy !== 0) for (const m of members) {
|
|
1811
|
+
const tx_after = require_insertions.capture_translate_baseline(doc, m.id);
|
|
1812
|
+
require_insertions.apply_translate(doc, m.id, tx_after, dx, dy);
|
|
1813
|
+
}
|
|
1814
|
+
emit();
|
|
1815
|
+
};
|
|
1816
|
+
const revert = () => {
|
|
1817
|
+
for (const m of members) {
|
|
1818
|
+
require_insertions.apply_resize(doc, m.id, m.rz, 1, 1, origin);
|
|
1819
|
+
doc.set_attr(m.id, "transform", m.transform_pre);
|
|
1820
|
+
}
|
|
1821
|
+
emit();
|
|
1822
|
+
};
|
|
1823
|
+
apply();
|
|
1824
|
+
history.atomic("resize-to", (tx) => {
|
|
1825
|
+
tx.push({
|
|
1826
|
+
providerId: PROVIDER_ID,
|
|
1827
|
+
apply,
|
|
1828
|
+
revert
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
/** Shared helper: compute a default rotation pivot from the live
|
|
1834
|
+
* geometry_provider when the caller omitted one. Falls back to (0,0)
|
|
1835
|
+
* if no surface is attached. */
|
|
1836
|
+
function default_rotate_pivot(ids) {
|
|
1837
|
+
if (!geometry_provider || ids.length === 0) return {
|
|
1838
|
+
x: 0,
|
|
1839
|
+
y: 0
|
|
1840
|
+
};
|
|
1841
|
+
const rects = [];
|
|
1842
|
+
for (const id of ids) {
|
|
1843
|
+
const b = geometry_provider.bounds_of(id);
|
|
1844
|
+
if (b) rects.push(b);
|
|
1845
|
+
}
|
|
1846
|
+
if (rects.length === 0) return {
|
|
1847
|
+
x: 0,
|
|
1848
|
+
y: 0
|
|
1849
|
+
};
|
|
1850
|
+
const u = _grida_cmath.default.rect.union(rects);
|
|
1851
|
+
return {
|
|
1852
|
+
x: u.x + u.width / 2,
|
|
1853
|
+
y: u.y + u.height / 2
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function rotate(angle, opts) {
|
|
1857
|
+
const ids = opts?.ids ?? selection;
|
|
1858
|
+
if (ids.length === 0) return false;
|
|
1859
|
+
const prepared = require_insertions.prepare_rotate_rpc({
|
|
1860
|
+
doc,
|
|
1861
|
+
ids,
|
|
1862
|
+
pivot: opts?.pivot ?? default_rotate_pivot(ids),
|
|
1863
|
+
angle_radians: angle,
|
|
1864
|
+
options: { angle_snap_step_radians: style.angle_snap_step_radians },
|
|
1865
|
+
emit
|
|
1866
|
+
});
|
|
1867
|
+
for (const v of prepared.verdicts.values()) if (v.kind === "refuse") return false;
|
|
1868
|
+
prepared.apply();
|
|
1869
|
+
history.atomic("rotate", (tx) => {
|
|
1870
|
+
tx.push({
|
|
1871
|
+
providerId: PROVIDER_ID,
|
|
1872
|
+
apply: prepared.apply,
|
|
1873
|
+
revert: prepared.revert
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
function rotate_to(angle, opts) {
|
|
1879
|
+
const ids = opts?.ids ?? selection;
|
|
1880
|
+
if (ids.length === 0) return false;
|
|
1881
|
+
const probe = require_insertions.prepare_rotate_rpc({
|
|
1882
|
+
doc,
|
|
1883
|
+
ids,
|
|
1884
|
+
pivot: opts?.pivot ?? default_rotate_pivot(ids),
|
|
1885
|
+
angle_radians: 0,
|
|
1886
|
+
options: { angle_snap_step_radians: style.angle_snap_step_radians },
|
|
1887
|
+
emit: () => {}
|
|
1888
|
+
});
|
|
1889
|
+
for (const v of probe.verdicts.values()) if (v.kind === "refuse") return false;
|
|
1890
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1513
1891
|
const apply = () => {
|
|
1514
|
-
for (const
|
|
1892
|
+
for (const m of probe.plan.members) {
|
|
1893
|
+
const delta = angle - m.baseline.current_rotation_deg * DEG_TO_RAD;
|
|
1894
|
+
require_insertions.apply_rotate(doc, m.id, m.baseline, delta);
|
|
1895
|
+
}
|
|
1515
1896
|
emit();
|
|
1516
1897
|
};
|
|
1517
1898
|
const revert = () => {
|
|
1518
|
-
for (const
|
|
1899
|
+
for (const m of probe.plan.members) require_insertions.apply_rotate(doc, m.id, m.baseline, 0);
|
|
1519
1900
|
emit();
|
|
1520
1901
|
};
|
|
1521
1902
|
apply();
|
|
1522
|
-
history.atomic("
|
|
1903
|
+
history.atomic("rotate-to", (tx) => {
|
|
1523
1904
|
tx.push({
|
|
1524
1905
|
providerId: PROVIDER_ID,
|
|
1525
1906
|
apply,
|
|
1526
1907
|
revert
|
|
1527
1908
|
});
|
|
1528
1909
|
});
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
function flatten_transform(opts) {
|
|
1913
|
+
const ids = opts?.ids ?? selection;
|
|
1914
|
+
if (ids.length === 0) return false;
|
|
1915
|
+
const members = [];
|
|
1916
|
+
for (const id of ids) {
|
|
1917
|
+
const pre = doc.get_attr(id, "transform");
|
|
1918
|
+
if (pre === null) continue;
|
|
1919
|
+
const ops = require_insertions.parse_transform_list(pre);
|
|
1920
|
+
if (ops === null) continue;
|
|
1921
|
+
if (ops.length === 1 && ops[0].type === "matrix") continue;
|
|
1922
|
+
members.push({
|
|
1923
|
+
id,
|
|
1924
|
+
transform_pre: pre,
|
|
1925
|
+
ops
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
if (members.length === 0) return false;
|
|
1929
|
+
const IDENT = [
|
|
1930
|
+
1,
|
|
1931
|
+
0,
|
|
1932
|
+
0,
|
|
1933
|
+
1,
|
|
1934
|
+
0,
|
|
1935
|
+
0
|
|
1936
|
+
];
|
|
1937
|
+
function mul(m1, m2) {
|
|
1938
|
+
const [a1, b1, c1, d1, e1, f1] = m1;
|
|
1939
|
+
const [a2, b2, c2, d2, e2, f2] = m2;
|
|
1940
|
+
return [
|
|
1941
|
+
a1 * a2 + c1 * b2,
|
|
1942
|
+
b1 * a2 + d1 * b2,
|
|
1943
|
+
a1 * c2 + c1 * d2,
|
|
1944
|
+
b1 * c2 + d1 * d2,
|
|
1945
|
+
a1 * e2 + c1 * f2 + e1,
|
|
1946
|
+
b1 * e2 + d1 * f2 + f1
|
|
1947
|
+
];
|
|
1948
|
+
}
|
|
1949
|
+
function op_to_mat(op) {
|
|
1950
|
+
switch (op.type) {
|
|
1951
|
+
case "matrix": return [
|
|
1952
|
+
op.a,
|
|
1953
|
+
op.b,
|
|
1954
|
+
op.c,
|
|
1955
|
+
op.d,
|
|
1956
|
+
op.e,
|
|
1957
|
+
op.f
|
|
1958
|
+
];
|
|
1959
|
+
case "translate": return [
|
|
1960
|
+
1,
|
|
1961
|
+
0,
|
|
1962
|
+
0,
|
|
1963
|
+
1,
|
|
1964
|
+
op.tx,
|
|
1965
|
+
op.ty
|
|
1966
|
+
];
|
|
1967
|
+
case "rotate": {
|
|
1968
|
+
const rad = op.angle * Math.PI / 180;
|
|
1969
|
+
const c = Math.cos(rad);
|
|
1970
|
+
const s = Math.sin(rad);
|
|
1971
|
+
if (op.cx === 0 && op.cy === 0) return [
|
|
1972
|
+
c,
|
|
1973
|
+
s,
|
|
1974
|
+
-s,
|
|
1975
|
+
c,
|
|
1976
|
+
0,
|
|
1977
|
+
0
|
|
1978
|
+
];
|
|
1979
|
+
const e = op.cx - c * op.cx + s * op.cy;
|
|
1980
|
+
const f = op.cy - s * op.cx - c * op.cy;
|
|
1981
|
+
return [
|
|
1982
|
+
c,
|
|
1983
|
+
s,
|
|
1984
|
+
-s,
|
|
1985
|
+
c,
|
|
1986
|
+
e,
|
|
1987
|
+
f
|
|
1988
|
+
];
|
|
1989
|
+
}
|
|
1990
|
+
case "scale": return [
|
|
1991
|
+
op.sx,
|
|
1992
|
+
0,
|
|
1993
|
+
0,
|
|
1994
|
+
op.sy,
|
|
1995
|
+
0,
|
|
1996
|
+
0
|
|
1997
|
+
];
|
|
1998
|
+
case "skewX": {
|
|
1999
|
+
const rad = op.angle * Math.PI / 180;
|
|
2000
|
+
return [
|
|
2001
|
+
1,
|
|
2002
|
+
0,
|
|
2003
|
+
Math.tan(rad),
|
|
2004
|
+
1,
|
|
2005
|
+
0,
|
|
2006
|
+
0
|
|
2007
|
+
];
|
|
2008
|
+
}
|
|
2009
|
+
case "skewY": {
|
|
2010
|
+
const rad = op.angle * Math.PI / 180;
|
|
2011
|
+
return [
|
|
2012
|
+
1,
|
|
2013
|
+
Math.tan(rad),
|
|
2014
|
+
0,
|
|
2015
|
+
1,
|
|
2016
|
+
0,
|
|
2017
|
+
0
|
|
2018
|
+
];
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
const apply = () => {
|
|
2023
|
+
for (const m of members) {
|
|
2024
|
+
let mat = IDENT;
|
|
2025
|
+
for (const op of m.ops) mat = mul(mat, op_to_mat(op));
|
|
2026
|
+
doc.set_attr(m.id, "transform", require_insertions.emit_transform_list([{
|
|
2027
|
+
type: "matrix",
|
|
2028
|
+
a: mat[0],
|
|
2029
|
+
b: mat[1],
|
|
2030
|
+
c: mat[2],
|
|
2031
|
+
d: mat[3],
|
|
2032
|
+
e: mat[4],
|
|
2033
|
+
f: mat[5]
|
|
2034
|
+
}]));
|
|
2035
|
+
}
|
|
2036
|
+
emit();
|
|
2037
|
+
};
|
|
2038
|
+
const revert = () => {
|
|
2039
|
+
for (const m of members) doc.set_attr(m.id, "transform", m.transform_pre);
|
|
2040
|
+
emit();
|
|
2041
|
+
};
|
|
2042
|
+
apply();
|
|
2043
|
+
history.atomic("flatten-transform", (tx) => {
|
|
2044
|
+
tx.push({
|
|
2045
|
+
providerId: PROVIDER_ID,
|
|
2046
|
+
apply,
|
|
2047
|
+
revert
|
|
2048
|
+
});
|
|
2049
|
+
});
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Translate selected members so they line up along the requested edge or
|
|
2054
|
+
* center of a reference rect. Same mechanics as `resize_to`: per-member
|
|
2055
|
+
* translate baselines (so `<g>`, transformed, and natively-attributed
|
|
2056
|
+
* nodes all write the cleanest in-place representation), one atomic
|
|
2057
|
+
* history step.
|
|
2058
|
+
*
|
|
2059
|
+
* Reference rect is selection-size dependent:
|
|
2060
|
+
* - multi-selection: union of member bboxes
|
|
2061
|
+
* - single selection: the parent's bbox (root → `<svg>` viewport,
|
|
2062
|
+
* inside a `<g>` → that group's bbox). Refuses when the selected
|
|
2063
|
+
* node IS the root (no container to align against).
|
|
2064
|
+
*
|
|
2065
|
+
* Refuses when `geometry_provider` is null (no surface attached) or when
|
|
2066
|
+
* no member has a resolvable bbox.
|
|
2067
|
+
*/
|
|
2068
|
+
function align(direction, opts) {
|
|
2069
|
+
const ids = opts?.ids ?? selection;
|
|
2070
|
+
if (ids.length === 0) return false;
|
|
2071
|
+
if (!geometry_provider) return false;
|
|
2072
|
+
const members = [];
|
|
2073
|
+
for (const id of ids) {
|
|
2074
|
+
const bbox = geometry_provider.bounds_of(id);
|
|
2075
|
+
if (!bbox) continue;
|
|
2076
|
+
const baseline = require_insertions.capture_translate_baseline(doc, id);
|
|
2077
|
+
if (baseline.type === "unsupported") continue;
|
|
2078
|
+
members.push({
|
|
2079
|
+
id,
|
|
2080
|
+
bbox,
|
|
2081
|
+
baseline
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
if (members.length === 0) return false;
|
|
2085
|
+
let target;
|
|
2086
|
+
if (members.length === 1) {
|
|
2087
|
+
const parent_id = doc.parent_of(members[0].id);
|
|
2088
|
+
if (parent_id === null) return false;
|
|
2089
|
+
const parent_bbox = geometry_provider.bounds_of(parent_id);
|
|
2090
|
+
if (!parent_bbox) return false;
|
|
2091
|
+
target = parent_bbox;
|
|
2092
|
+
} else target = _grida_cmath.default.rect.union(members.map((m) => m.bbox));
|
|
2093
|
+
const deltas = compute_align_deltas(members, target, direction);
|
|
2094
|
+
if (deltas.size === 0) return false;
|
|
2095
|
+
const apply = () => {
|
|
2096
|
+
for (const m of members) {
|
|
2097
|
+
const d = deltas.get(m.id);
|
|
2098
|
+
if (d) require_insertions.apply_translate(doc, m.id, m.baseline, d.x, d.y);
|
|
2099
|
+
}
|
|
2100
|
+
emit();
|
|
2101
|
+
};
|
|
2102
|
+
const revert = () => {
|
|
2103
|
+
for (const m of members) if (deltas.has(m.id)) require_insertions.apply_translate(doc, m.id, m.baseline, 0, 0);
|
|
2104
|
+
emit();
|
|
2105
|
+
};
|
|
2106
|
+
apply();
|
|
2107
|
+
history.atomic(`align ${direction}`, (tx) => {
|
|
2108
|
+
tx.push({
|
|
2109
|
+
providerId: PROVIDER_ID,
|
|
2110
|
+
apply,
|
|
2111
|
+
revert
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
function select_all() {
|
|
2117
|
+
const parent = scope ?? doc.root;
|
|
2118
|
+
const children = doc.element_children_of(parent);
|
|
2119
|
+
if (children.length === 0) return false;
|
|
2120
|
+
set_selection(children);
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Cycle the selection to the next / previous sibling. Single-selection
|
|
2125
|
+
* path uses the selected node's parent; empty / multi-selection falls
|
|
2126
|
+
* back to the current scope's first / last child. Wraps at edges.
|
|
2127
|
+
*/
|
|
2128
|
+
function select_sibling(direction) {
|
|
2129
|
+
let parent;
|
|
2130
|
+
let anchor_index;
|
|
2131
|
+
let siblings;
|
|
2132
|
+
if (selection.length === 1) {
|
|
2133
|
+
const current = selection[0];
|
|
2134
|
+
parent = doc.parent_of(current);
|
|
2135
|
+
if (parent === null) return false;
|
|
2136
|
+
siblings = doc.element_children_of(parent);
|
|
2137
|
+
anchor_index = siblings.indexOf(current);
|
|
2138
|
+
if (anchor_index < 0) return false;
|
|
2139
|
+
} else {
|
|
2140
|
+
parent = scope ?? doc.root;
|
|
2141
|
+
siblings = doc.element_children_of(parent);
|
|
2142
|
+
if (siblings.length === 0) return false;
|
|
2143
|
+
anchor_index = direction === "next" ? -1 : siblings.length;
|
|
2144
|
+
}
|
|
2145
|
+
const n = siblings.length;
|
|
2146
|
+
const next = direction === "next" ? (anchor_index + 1) % n : (anchor_index - 1 + n) % n;
|
|
2147
|
+
set_selection([siblings[next]]);
|
|
2148
|
+
return true;
|
|
1529
2149
|
}
|
|
1530
2150
|
function reorder(direction) {
|
|
1531
2151
|
if (selection.length !== 1) return;
|
|
@@ -1573,22 +2193,31 @@ function createSvgEditor(opts) {
|
|
|
1573
2193
|
});
|
|
1574
2194
|
}
|
|
1575
2195
|
function remove() {
|
|
1576
|
-
if (selection.length
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const
|
|
2196
|
+
if (selection.length === 0) return;
|
|
2197
|
+
const filtered = doc.prune_nested_nodes(selection).filter((id) => doc.parent_of(id) !== null);
|
|
2198
|
+
if (filtered.length === 0) return;
|
|
2199
|
+
const doc_order = doc.all_elements();
|
|
2200
|
+
const index_of = /* @__PURE__ */ new Map();
|
|
2201
|
+
for (let i = 0; i < doc_order.length; i++) index_of.set(doc_order[i], i);
|
|
2202
|
+
const captures = [...filtered].sort((a, b) => (index_of.get(a) ?? 0) - (index_of.get(b) ?? 0)).map((id) => ({
|
|
2203
|
+
id,
|
|
2204
|
+
parent: doc.parent_of(id),
|
|
2205
|
+
next_sibling: doc.next_element_sibling_of(id)
|
|
2206
|
+
}));
|
|
1581
2207
|
const old_selection = selection;
|
|
1582
2208
|
const apply = () => {
|
|
1583
|
-
doc.remove(
|
|
2209
|
+
for (const c of captures) doc.remove(c.id);
|
|
1584
2210
|
set_selection([]);
|
|
1585
2211
|
};
|
|
1586
2212
|
const revert = () => {
|
|
1587
|
-
|
|
2213
|
+
for (let i = captures.length - 1; i >= 0; i--) {
|
|
2214
|
+
const c = captures[i];
|
|
2215
|
+
doc.insert(c.id, c.parent, c.next_sibling);
|
|
2216
|
+
}
|
|
1588
2217
|
set_selection(old_selection);
|
|
1589
2218
|
};
|
|
1590
2219
|
apply();
|
|
1591
|
-
history.atomic("remove"
|
|
2220
|
+
history.atomic(captures.length === 1 ? "remove" : `remove ${captures.length}`, (tx) => {
|
|
1592
2221
|
tx.push({
|
|
1593
2222
|
providerId: PROVIDER_ID,
|
|
1594
2223
|
apply,
|
|
@@ -1596,10 +2225,147 @@ function createSvgEditor(opts) {
|
|
|
1596
2225
|
});
|
|
1597
2226
|
});
|
|
1598
2227
|
}
|
|
2228
|
+
function group() {
|
|
2229
|
+
const plan = require_insertions.plan_group(doc, selection);
|
|
2230
|
+
if (!plan) return false;
|
|
2231
|
+
const group_id = doc.create_element("g");
|
|
2232
|
+
const original_selection = selection;
|
|
2233
|
+
const apply = () => {
|
|
2234
|
+
doc.insert(group_id, plan.parent, plan.insert_before);
|
|
2235
|
+
for (const child of plan.children) doc.insert(child, group_id, null);
|
|
2236
|
+
set_selection([group_id]);
|
|
2237
|
+
};
|
|
2238
|
+
const revert = () => {
|
|
2239
|
+
for (let i = plan.children.length - 1; i >= 0; i--) {
|
|
2240
|
+
const child = plan.children[i];
|
|
2241
|
+
doc.insert(child, plan.parent, plan.original_positions.get(child) ?? null);
|
|
2242
|
+
}
|
|
2243
|
+
doc.remove(group_id);
|
|
2244
|
+
set_selection(original_selection);
|
|
2245
|
+
};
|
|
2246
|
+
apply();
|
|
2247
|
+
history.atomic("group", (tx) => {
|
|
2248
|
+
tx.push({
|
|
2249
|
+
providerId: PROVIDER_ID,
|
|
2250
|
+
apply,
|
|
2251
|
+
revert
|
|
2252
|
+
});
|
|
2253
|
+
});
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Atomic one-shot insertion. Used by paste, programmatic RPC, and the
|
|
2258
|
+
* click-no-drag commit path inside the insertion gesture driver. One
|
|
2259
|
+
* undo step. Returns the new node id.
|
|
2260
|
+
*
|
|
2261
|
+
* `attrs` are merged on top of `default_paint_attrs(tag)` — caller attrs
|
|
2262
|
+
* win. `opts.parent` defaults to root; `opts.index` (insert-before
|
|
2263
|
+
* sibling index) defaults to append; `opts.select` defaults to `true`.
|
|
2264
|
+
*/
|
|
2265
|
+
function insert(tag, attrs, opts) {
|
|
2266
|
+
const parent = opts?.parent ?? doc.root;
|
|
2267
|
+
const select_after = opts?.select !== false;
|
|
2268
|
+
let insert_before = null;
|
|
2269
|
+
if (opts?.index !== void 0) insert_before = doc.element_children_of(parent)[opts.index] ?? null;
|
|
2270
|
+
const id = doc.create_element(tag);
|
|
2271
|
+
const merged_attrs = {
|
|
2272
|
+
...default_paint_attrs_for(tag),
|
|
2273
|
+
...attrs
|
|
2274
|
+
};
|
|
2275
|
+
const attr_pairs = Object.entries(merged_attrs);
|
|
2276
|
+
const previous_selection = selection;
|
|
2277
|
+
const apply = () => {
|
|
2278
|
+
for (const [name, value] of attr_pairs) doc.set_attr(id, name, value);
|
|
2279
|
+
doc.insert(id, parent, insert_before);
|
|
2280
|
+
if (select_after) set_selection([id]);
|
|
2281
|
+
};
|
|
2282
|
+
const revert = () => {
|
|
2283
|
+
doc.remove(id);
|
|
2284
|
+
if (select_after) set_selection(previous_selection);
|
|
2285
|
+
};
|
|
2286
|
+
apply();
|
|
2287
|
+
history.atomic(`insert ${tag}`, (tx) => {
|
|
2288
|
+
tx.push({
|
|
2289
|
+
providerId: PROVIDER_ID,
|
|
2290
|
+
apply,
|
|
2291
|
+
revert
|
|
2292
|
+
});
|
|
2293
|
+
});
|
|
2294
|
+
return id;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Preview-bracketed insertion. Used by the pointer-driven drag gesture
|
|
2298
|
+
* in the DOM surface. Per-frame attr writes call `update(attrs)`; one
|
|
2299
|
+
* undo step on `commit()`; clean rollback on `discard()`.
|
|
2300
|
+
*
|
|
2301
|
+
* The node is created and inserted on open so the HUD selection chrome
|
|
2302
|
+
* can render the in-progress shape immediately. On `discard()` the
|
|
2303
|
+
* preview's revert removes the node entirely.
|
|
2304
|
+
*/
|
|
2305
|
+
function insert_preview(tag, initial, opts) {
|
|
2306
|
+
const parent = opts?.parent ?? doc.root;
|
|
2307
|
+
let insert_before = null;
|
|
2308
|
+
if (opts?.index !== void 0) insert_before = doc.element_children_of(parent)[opts.index] ?? null;
|
|
2309
|
+
const id = doc.create_element(tag);
|
|
2310
|
+
const previous_selection = selection;
|
|
2311
|
+
const live_attrs = {
|
|
2312
|
+
...default_paint_attrs_for(tag),
|
|
2313
|
+
...initial
|
|
2314
|
+
};
|
|
2315
|
+
for (const name in live_attrs) doc.set_attr(id, name, live_attrs[name]);
|
|
2316
|
+
doc.insert(id, parent, insert_before);
|
|
2317
|
+
set_selection([id]);
|
|
2318
|
+
const preview = history.preview(`insert ${tag}`);
|
|
2319
|
+
let active = true;
|
|
2320
|
+
const apply = () => {
|
|
2321
|
+
for (const name in live_attrs) doc.set_attr(id, name, live_attrs[name]);
|
|
2322
|
+
if (doc.parent_of(id) === null) doc.insert(id, parent, insert_before);
|
|
2323
|
+
set_selection([id]);
|
|
2324
|
+
};
|
|
2325
|
+
const revert = () => {
|
|
2326
|
+
doc.remove(id);
|
|
2327
|
+
set_selection(previous_selection);
|
|
2328
|
+
};
|
|
2329
|
+
const entry = {
|
|
2330
|
+
providerId: PROVIDER_ID,
|
|
2331
|
+
apply,
|
|
2332
|
+
revert
|
|
2333
|
+
};
|
|
2334
|
+
preview.set(entry);
|
|
2335
|
+
return {
|
|
2336
|
+
id,
|
|
2337
|
+
update(attrs) {
|
|
2338
|
+
if (!active) return;
|
|
2339
|
+
for (const name in attrs) {
|
|
2340
|
+
live_attrs[name] = attrs[name];
|
|
2341
|
+
doc.set_attr(id, name, attrs[name]);
|
|
2342
|
+
}
|
|
2343
|
+
preview.set(entry);
|
|
2344
|
+
},
|
|
2345
|
+
commit() {
|
|
2346
|
+
if (!active) return;
|
|
2347
|
+
active = false;
|
|
2348
|
+
preview.commit();
|
|
2349
|
+
},
|
|
2350
|
+
discard() {
|
|
2351
|
+
if (!active) return;
|
|
2352
|
+
active = false;
|
|
2353
|
+
preview.discard();
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
/** Per-tag default paint attrs. Wrapped so callers don't need to depend
|
|
2358
|
+
* on the InsertableTag type — `insert()` accepts arbitrary string tags
|
|
2359
|
+
* (so `commands.insert("path", ...)` works for paste / RPC) but only
|
|
2360
|
+
* the closed insertable set gets default paint. */
|
|
2361
|
+
function default_paint_attrs_for(tag) {
|
|
2362
|
+
if (tag === "rect" || tag === "ellipse" || tag === "line") return require_insertions.default_paint_attrs(tag);
|
|
2363
|
+
return {};
|
|
2364
|
+
}
|
|
1599
2365
|
function set_text(value) {
|
|
1600
2366
|
if (selection.length !== 1) return;
|
|
1601
2367
|
const target = selection[0];
|
|
1602
|
-
if (doc.
|
|
2368
|
+
if (!doc.is_text_edit_target(target)) return;
|
|
1603
2369
|
const original = doc.text_of(target);
|
|
1604
2370
|
if (original === value) return;
|
|
1605
2371
|
const apply = () => {
|
|
@@ -1642,7 +2408,7 @@ function createSvgEditor(opts) {
|
|
|
1642
2408
|
function enter_content_edit(target) {
|
|
1643
2409
|
const id = target ?? (selection.length === 1 ? selection[0] : null);
|
|
1644
2410
|
if (!id) return false;
|
|
1645
|
-
if (doc.
|
|
2411
|
+
if (!doc.is_text_edit_target(id)) return false;
|
|
1646
2412
|
if (!content_edit_driver) return false;
|
|
1647
2413
|
return content_edit_driver(id);
|
|
1648
2414
|
}
|
|
@@ -1651,8 +2417,10 @@ function createSvgEditor(opts) {
|
|
|
1651
2417
|
selection = [];
|
|
1652
2418
|
scope = null;
|
|
1653
2419
|
mode = "select";
|
|
2420
|
+
tool = require_insertions.TOOL_CURSOR;
|
|
1654
2421
|
history.clear();
|
|
1655
2422
|
baseline_doc_version = doc_version;
|
|
2423
|
+
load_version++;
|
|
1656
2424
|
emit();
|
|
1657
2425
|
}
|
|
1658
2426
|
function serialize_svg() {
|
|
@@ -1669,6 +2437,8 @@ function createSvgEditor(opts) {
|
|
|
1669
2437
|
const commands = {
|
|
1670
2438
|
select,
|
|
1671
2439
|
deselect,
|
|
2440
|
+
select_all,
|
|
2441
|
+
select_sibling,
|
|
1672
2442
|
enter_scope,
|
|
1673
2443
|
exit_scope,
|
|
1674
2444
|
set_mode,
|
|
@@ -1678,8 +2448,17 @@ function createSvgEditor(opts) {
|
|
|
1678
2448
|
preview_paint,
|
|
1679
2449
|
set_paint_from_gradient,
|
|
1680
2450
|
translate,
|
|
2451
|
+
nudge,
|
|
2452
|
+
resize_to,
|
|
2453
|
+
rotate,
|
|
2454
|
+
rotate_to,
|
|
2455
|
+
flatten_transform,
|
|
2456
|
+
align,
|
|
1681
2457
|
reorder,
|
|
1682
2458
|
remove,
|
|
2459
|
+
group,
|
|
2460
|
+
insert,
|
|
2461
|
+
insert_preview,
|
|
1683
2462
|
set_text,
|
|
1684
2463
|
load_svg,
|
|
1685
2464
|
serialize_svg,
|
|
@@ -1701,6 +2480,7 @@ function createSvgEditor(opts) {
|
|
|
1701
2480
|
selection = [];
|
|
1702
2481
|
scope = null;
|
|
1703
2482
|
mode = "select";
|
|
2483
|
+
tool = require_insertions.TOOL_CURSOR;
|
|
1704
2484
|
baseline_doc_version = doc_version;
|
|
1705
2485
|
emit();
|
|
1706
2486
|
}
|
|
@@ -1757,18 +2537,7 @@ function createSvgEditor(opts) {
|
|
|
1757
2537
|
return elem_id && elem_id.length > 0 ? `${head} #${elem_id}` : head;
|
|
1758
2538
|
},
|
|
1759
2539
|
tree() {
|
|
1760
|
-
|
|
1761
|
-
for (const id of doc.all_elements()) map.set(id, {
|
|
1762
|
-
id,
|
|
1763
|
-
tag: doc.tag_of(id),
|
|
1764
|
-
name: doc.get_attr(id, "id") ?? void 0,
|
|
1765
|
-
parent: doc.parent_of(id),
|
|
1766
|
-
children: doc.element_children_of(id)
|
|
1767
|
-
});
|
|
1768
|
-
return {
|
|
1769
|
-
root: doc.root,
|
|
1770
|
-
nodes: map
|
|
1771
|
-
};
|
|
2540
|
+
return tree_snapshot();
|
|
1772
2541
|
},
|
|
1773
2542
|
surface_hover() {
|
|
1774
2543
|
return current_surface_hover;
|
|
@@ -1784,7 +2553,17 @@ function createSvgEditor(opts) {
|
|
|
1784
2553
|
surface_hover_listeners.delete(cb);
|
|
1785
2554
|
};
|
|
1786
2555
|
},
|
|
2556
|
+
subscribe_geometry(cb) {
|
|
2557
|
+
geometry_listeners.add(cb);
|
|
2558
|
+
return () => {
|
|
2559
|
+
geometry_listeners.delete(cb);
|
|
2560
|
+
};
|
|
2561
|
+
},
|
|
2562
|
+
get geometry() {
|
|
2563
|
+
return geometry_provider;
|
|
2564
|
+
},
|
|
1787
2565
|
modes,
|
|
2566
|
+
set_tool,
|
|
1788
2567
|
get style() {
|
|
1789
2568
|
return style;
|
|
1790
2569
|
},
|
|
@@ -1798,6 +2577,15 @@ function createSvgEditor(opts) {
|
|
|
1798
2577
|
providers,
|
|
1799
2578
|
_internal: {
|
|
1800
2579
|
doc,
|
|
2580
|
+
history: { preview: (label) => history.preview(label) },
|
|
2581
|
+
emit,
|
|
2582
|
+
subscribe_translate_commit(cb) {
|
|
2583
|
+
translate_commit_listeners.add(cb);
|
|
2584
|
+
return () => {
|
|
2585
|
+
translate_commit_listeners.delete(cb);
|
|
2586
|
+
};
|
|
2587
|
+
},
|
|
2588
|
+
notify_translate_commit,
|
|
1801
2589
|
set_content_edit_driver(fn) {
|
|
1802
2590
|
content_edit_driver = fn;
|
|
1803
2591
|
},
|
|
@@ -1810,6 +2598,9 @@ function createSvgEditor(opts) {
|
|
|
1810
2598
|
},
|
|
1811
2599
|
set_computed_resolver(fn) {
|
|
1812
2600
|
computed_resolver = fn;
|
|
2601
|
+
},
|
|
2602
|
+
set_geometry(p) {
|
|
2603
|
+
geometry_provider = p;
|
|
1813
2604
|
}
|
|
1814
2605
|
},
|
|
1815
2606
|
keymap
|
|
@@ -1818,13 +2609,39 @@ function createSvgEditor(opts) {
|
|
|
1818
2609
|
applyDefaultBindings(keymap);
|
|
1819
2610
|
return public_editor;
|
|
1820
2611
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2612
|
+
function paint_value_equals(a, b) {
|
|
2613
|
+
if (a === b) return true;
|
|
2614
|
+
if (a.declared !== b.declared) return false;
|
|
2615
|
+
if (a.provenance.carrier !== b.provenance.carrier) return false;
|
|
2616
|
+
if (a.provenance.origin !== b.provenance.origin) return false;
|
|
2617
|
+
return paint_equals(a.computed, b.computed);
|
|
2618
|
+
}
|
|
2619
|
+
function paint_equals(a, b) {
|
|
2620
|
+
if (a === b) return true;
|
|
2621
|
+
if (a == null || b == null) return false;
|
|
2622
|
+
if ("error" in a || "error" in b) return "error" in a && "error" in b && a.error === b.error && a.reason === b.reason;
|
|
2623
|
+
if (a.kind !== b.kind) return false;
|
|
2624
|
+
if (a.kind === "color" && b.kind === "color") {
|
|
2625
|
+
if (a.value.kind !== b.value.kind) return false;
|
|
2626
|
+
if (a.value.kind === "rgb" && b.value.kind === "rgb") return a.value.value === b.value.value;
|
|
2627
|
+
return true;
|
|
1826
2628
|
}
|
|
1827
|
-
|
|
2629
|
+
if (a.kind === "ref" && b.kind === "ref") return a.id === b.id;
|
|
2630
|
+
if (a.kind === "none" && b.kind === "none") return true;
|
|
2631
|
+
if (a.kind === "context_fill" && b.kind === "context_fill") return true;
|
|
2632
|
+
if (a.kind === "context_stroke" && b.kind === "context_stroke") return true;
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
function property_value_equals(a, b) {
|
|
2636
|
+
if (a === b) return true;
|
|
2637
|
+
if (a.declared !== b.declared) return false;
|
|
2638
|
+
if (a.provenance.carrier !== b.provenance.carrier) return false;
|
|
2639
|
+
if (a.provenance.origin !== b.provenance.origin) return false;
|
|
2640
|
+
if (a.computed === b.computed) return true;
|
|
2641
|
+
if (a.computed && b.computed && typeof a.computed === "object" && typeof b.computed === "object" && "error" in a.computed && "error" in b.computed) return a.computed.error === b.computed.error && a.computed.reason === b.computed.reason;
|
|
2642
|
+
return false;
|
|
2643
|
+
}
|
|
2644
|
+
//#endregion
|
|
1828
2645
|
Object.defineProperty(exports, "createSvgEditor", {
|
|
1829
2646
|
enumerable: true,
|
|
1830
2647
|
get: function() {
|