@grida/svg-editor 1.0.0-alpha.13 → 1.0.0-alpha.14
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 +88 -42
- package/dist/{dom-Cvm9Towu.js → dom-BuD8TKmL.js} +1592 -624
- package/dist/dom-D4dy6kq5.d.ts +112 -0
- package/dist/{dom-BlMk07oX.mjs → dom-DSjfCllZ.mjs} +1566 -617
- package/dist/dom-Dz_V6q0Y.d.mts +114 -0
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +4 -1
- package/dist/dom.mjs +2 -2
- package/dist/{editor-DtuRIs-Q.mjs → editor-B6pchGYk.mjs} +519 -321
- package/dist/{editor-CdyC3uAe.js → editor-BHHU_Nvz.js} +527 -329
- package/dist/{editor-Bd4-VCEJ.d.ts → editor-CJ2KuRh5.d.ts} +472 -24
- package/dist/{editor-BH03X8cX.d.mts → editor-YQwdWHBb.d.mts} +472 -24
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.mjs +3 -3
- package/dist/model-DIzZmeyf.mjs +3677 -0
- package/dist/model-DqGqV1H4.js +3823 -0
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +3 -3
- package/dist/presets.mjs +2 -2
- package/dist/react.d.mts +18 -6
- package/dist/react.d.ts +18 -6
- package/dist/react.js +24 -3
- package/dist/react.mjs +24 -3
- package/package.json +9 -8
- package/dist/dom-DCX-a8Kr.d.ts +0 -57
- package/dist/dom-DgB4f-TE.d.mts +0 -59
- package/dist/insertions-BJ-6o6o5.js +0 -2399
- package/dist/insertions-Okcuo-Ck.mjs +0 -2176
- /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const require_model = require("./model-DqGqV1H4.js");
|
|
2
2
|
let _grida_history = require("@grida/history");
|
|
3
3
|
let _grida_keybinding = require("@grida/keybinding");
|
|
4
4
|
let _grida_cmath = require("@grida/cmath");
|
|
5
|
-
_grida_cmath =
|
|
5
|
+
_grida_cmath = require_model.__toESM(_grida_cmath);
|
|
6
6
|
let _grida_svg_parser = require("@grida/svg/parser");
|
|
7
|
+
let _grida_svg_parse = require("@grida/svg/parse");
|
|
7
8
|
//#region src/commands/registry.ts
|
|
8
9
|
var CommandRegistry = class {
|
|
9
10
|
constructor() {
|
|
@@ -119,6 +120,7 @@ function registerDefaultCommands(reg, editor) {
|
|
|
119
120
|
if (editor.state.mode !== "select") return false;
|
|
120
121
|
return editor.commands.align(args);
|
|
121
122
|
});
|
|
123
|
+
reg.register("content.enter", () => editor.enter_content_edit());
|
|
122
124
|
reg.register("hierarchy.enter", () => {
|
|
123
125
|
if (editor.state.selection.length !== 1) return false;
|
|
124
126
|
const id = editor.state.selection[0];
|
|
@@ -143,8 +145,10 @@ function registerDefaultCommands(reg, editor) {
|
|
|
143
145
|
return true;
|
|
144
146
|
});
|
|
145
147
|
reg.register(TOOL_SET, (args) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
const next = args;
|
|
149
|
+
const required_mode = next.type === "lasso" || next.type === "bend" ? "edit-content" : next.type === "insert" || next.type === "insert-text" ? "select" : null;
|
|
150
|
+
if (required_mode !== null && editor.state.mode !== required_mode) return false;
|
|
151
|
+
editor.set_tool(next);
|
|
148
152
|
return true;
|
|
149
153
|
});
|
|
150
154
|
}
|
|
@@ -164,12 +168,6 @@ function registerDefaultCommands(reg, editor) {
|
|
|
164
168
|
* measurement). That stays on the HUD modifiers channel. The keymap
|
|
165
169
|
* only sees Mod+D-shape chords.
|
|
166
170
|
*/
|
|
167
|
-
/** Modifiers that, when held, allow a binding to fire even inside a text input. */
|
|
168
|
-
const TEXT_INPUT_SAFE_MODS = new Set([
|
|
169
|
-
_grida_keybinding.KeyCode.Meta,
|
|
170
|
-
_grida_keybinding.KeyCode.Ctrl,
|
|
171
|
-
_grida_keybinding.KeyCode.Alt
|
|
172
|
-
]);
|
|
173
171
|
var Keymap = class {
|
|
174
172
|
constructor(commands, platformGetter = _grida_keybinding.getKeyboardOS) {
|
|
175
173
|
this.commands = commands;
|
|
@@ -235,15 +233,16 @@ var Keymap = class {
|
|
|
235
233
|
* bar even when the binding's handler rejects.
|
|
236
234
|
*
|
|
237
235
|
* Pure read; runs no handlers, no side effects. Honors the same
|
|
238
|
-
*
|
|
239
|
-
* keystroke isn't "claimed"
|
|
236
|
+
* form-element focus guard `dispatch` uses, so a typing user's
|
|
237
|
+
* keystroke isn't "claimed" — and the browser's native text-editing
|
|
238
|
+
* default (Cmd+A select all, Cmd+Z undo, etc.) wins.
|
|
240
239
|
*/
|
|
241
240
|
claims(event) {
|
|
242
241
|
const chunk = (0, _grida_keybinding.eventToChunk)(event);
|
|
243
242
|
if (chunk.keys.length === 0) return false;
|
|
244
243
|
const list = this.buckets.get((0, _grida_keybinding.chunkKey)(chunk));
|
|
245
244
|
if (!list || list.length === 0) return false;
|
|
246
|
-
if (
|
|
245
|
+
if (require_model.is_text_input_focused()) return list.some(({ binding }) => binding.allowInFormElement === true);
|
|
247
246
|
return true;
|
|
248
247
|
}
|
|
249
248
|
/**
|
|
@@ -251,6 +250,12 @@ var Keymap = class {
|
|
|
251
250
|
* order. Returns `true` on the first handler that consumes; returns
|
|
252
251
|
* `false` if nothing matched or all matches fell through.
|
|
253
252
|
*
|
|
253
|
+
* **Form-element focus guard.** When a text input is focused
|
|
254
|
+
* (`<input>`, `<textarea>`, contentEditable), bindings are suppressed
|
|
255
|
+
* by default so the platform's native shortcuts (Cmd+A, Cmd+Z, Cmd+C,
|
|
256
|
+
* arrow nav, …) are preserved. A binding can opt out of this guard
|
|
257
|
+
* with `allowInFormElement: true` — see `KeymapBinding`.
|
|
258
|
+
*
|
|
254
259
|
* `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
|
|
255
260
|
* or touch the event in any way. The host decides what to do with the
|
|
256
261
|
* platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
|
|
@@ -263,9 +268,9 @@ var Keymap = class {
|
|
|
263
268
|
const hash = (0, _grida_keybinding.chunkKey)(chunk);
|
|
264
269
|
const list = this.buckets.get(hash);
|
|
265
270
|
if (!list || list.length === 0) return false;
|
|
266
|
-
const text_focused =
|
|
271
|
+
const text_focused = require_model.is_text_input_focused();
|
|
267
272
|
for (const { binding } of list) {
|
|
268
|
-
if (text_focused &&
|
|
273
|
+
if (text_focused && binding.allowInFormElement !== true) continue;
|
|
269
274
|
if (this.commands.invoke(binding.command, binding.args)) return true;
|
|
270
275
|
}
|
|
271
276
|
return false;
|
|
@@ -285,10 +290,6 @@ var Keymap = class {
|
|
|
285
290
|
}
|
|
286
291
|
return out;
|
|
287
292
|
}
|
|
288
|
-
has_safe_mod(mods) {
|
|
289
|
-
for (const m of mods) if (TEXT_INPUT_SAFE_MODS.has(m)) return true;
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
293
|
};
|
|
293
294
|
function compareEntries(a, b) {
|
|
294
295
|
const pa = a.binding.priority ?? 0;
|
|
@@ -383,6 +384,10 @@ const DEFAULT_BINDINGS = [
|
|
|
383
384
|
command: "selection.align",
|
|
384
385
|
args: "vertical_centers"
|
|
385
386
|
},
|
|
387
|
+
{
|
|
388
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Enter),
|
|
389
|
+
command: "content.enter"
|
|
390
|
+
},
|
|
386
391
|
{
|
|
387
392
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.Enter),
|
|
388
393
|
command: "hierarchy.enter"
|
|
@@ -484,6 +489,16 @@ const DEFAULT_BINDINGS = [
|
|
|
484
489
|
tag: "line"
|
|
485
490
|
}
|
|
486
491
|
},
|
|
492
|
+
{
|
|
493
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyT),
|
|
494
|
+
command: TOOL_SET,
|
|
495
|
+
args: { type: "insert-text" }
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.KeyQ),
|
|
499
|
+
command: TOOL_SET,
|
|
500
|
+
args: { type: "lasso" }
|
|
501
|
+
},
|
|
487
502
|
{
|
|
488
503
|
keybinding: (0, _grida_keybinding.kb)(_grida_keybinding.KeyCode.BracketRight),
|
|
489
504
|
command: "reorder",
|
|
@@ -510,14 +525,6 @@ function applyDefaultBindings(keymap) {
|
|
|
510
525
|
for (const b of DEFAULT_BINDINGS) keymap.bind(b);
|
|
511
526
|
}
|
|
512
527
|
//#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
|
|
521
528
|
//#region src/core/defs.ts
|
|
522
529
|
var GradientsRegistry = class {
|
|
523
530
|
constructor(doc) {
|
|
@@ -565,7 +572,7 @@ var GradientsRegistry = class {
|
|
|
565
572
|
this._cached_by_id.delete(id);
|
|
566
573
|
any_change = true;
|
|
567
574
|
}
|
|
568
|
-
if (!any_change && this._cached && array_shallow_equal(this._cached, out)) {
|
|
575
|
+
if (!any_change && this._cached && require_model.array_shallow_equal(this._cached, out)) {
|
|
569
576
|
this._dirty = false;
|
|
570
577
|
return this._cached;
|
|
571
578
|
}
|
|
@@ -792,11 +799,14 @@ const GEOMETRY_ATTRS = new Set([
|
|
|
792
799
|
"marker-mid",
|
|
793
800
|
"marker-end"
|
|
794
801
|
]);
|
|
802
|
+
/** `transform:` CSS property at the start of a declaration list or after `;`. */
|
|
803
|
+
const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
|
|
795
804
|
var SvgDocument = class SvgDocument {
|
|
796
805
|
constructor(svg) {
|
|
797
806
|
this.listeners = /* @__PURE__ */ new Set();
|
|
798
807
|
this._structure_version = 0;
|
|
799
808
|
this._geometry_version = 0;
|
|
809
|
+
if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
800
810
|
this.source = svg;
|
|
801
811
|
const parsed = (0, _grida_svg_parser.parse_svg)(svg);
|
|
802
812
|
this.original = parsed;
|
|
@@ -822,6 +832,7 @@ var SvgDocument = class SvgDocument {
|
|
|
822
832
|
}
|
|
823
833
|
/** Replace document with new svg source (clears edits + history-owned state). */
|
|
824
834
|
load(svg) {
|
|
835
|
+
if (typeof svg !== "string") throw new TypeError(`SvgDocument.load(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
825
836
|
this.source = svg;
|
|
826
837
|
const parsed = (0, _grida_svg_parser.parse_svg)(svg);
|
|
827
838
|
this.original = parsed;
|
|
@@ -1059,6 +1070,88 @@ var SvgDocument = class SvgDocument {
|
|
|
1059
1070
|
for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
|
|
1060
1071
|
return true;
|
|
1061
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Returns a tag-discriminated snapshot of the authored geometry attrs
|
|
1075
|
+
* if this node is eligible for vector (vertex) editing — else `null`.
|
|
1076
|
+
*
|
|
1077
|
+
* v1 eligibility:
|
|
1078
|
+
* - `<path>` — requires non-empty `d`.
|
|
1079
|
+
* - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
|
|
1080
|
+
* - `<polygon>` — same as polyline.
|
|
1081
|
+
*
|
|
1082
|
+
* Deliberately rejects `<line>` in v1: the only useful vertex-edit
|
|
1083
|
+
* gestures on a `<line>` are (a) introducing a new vertex (which would
|
|
1084
|
+
* have to promote it to `<polyline>`) and (b) bending it with a tangent
|
|
1085
|
+
* (which would have to promote it to `<path>`). Both promotions are
|
|
1086
|
+
* out of scope for v1, so opening a `<line>` in vector-edit mode would
|
|
1087
|
+
* advertise capabilities that don't work.
|
|
1088
|
+
*
|
|
1089
|
+
* Also rejects `<rect>`, `<circle>`, `<ellipse>`, `<image>`, `<use>` —
|
|
1090
|
+
* those would force the same promotion-to-`<path>` machinery (trivia
|
|
1091
|
+
* transfer, cross-cutting attr carry, DOM-element swap, history-bracket
|
|
1092
|
+
* changes) that v1 keeps out of scope.
|
|
1093
|
+
*/
|
|
1094
|
+
is_vector_edit_target(id) {
|
|
1095
|
+
const n = this.nodes.get(id);
|
|
1096
|
+
if (!n || n.kind !== "element") return null;
|
|
1097
|
+
switch (n.local) {
|
|
1098
|
+
case "path": {
|
|
1099
|
+
const d = this.get_attr(id, "d");
|
|
1100
|
+
if (d === null || d.trim().length === 0) return null;
|
|
1101
|
+
return {
|
|
1102
|
+
kind: "path",
|
|
1103
|
+
d
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
case "polyline":
|
|
1107
|
+
case "polygon": {
|
|
1108
|
+
const raw = this.get_attr(id, "points") ?? "";
|
|
1109
|
+
const parsed = _grida_svg_parse.svg_parse.parse_points(raw);
|
|
1110
|
+
if (parsed.length < 2) return null;
|
|
1111
|
+
const points = parsed.map((p) => [p.x, p.y]);
|
|
1112
|
+
return n.local === "polyline" ? {
|
|
1113
|
+
kind: "polyline",
|
|
1114
|
+
points
|
|
1115
|
+
} : {
|
|
1116
|
+
kind: "polygon",
|
|
1117
|
+
points
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
default: return null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
|
|
1125
|
+
* per-glyph attribute (which conflicts with element-level rotation).
|
|
1126
|
+
*/
|
|
1127
|
+
has_glyph_rotate(id) {
|
|
1128
|
+
const tag = this.tag_of(id);
|
|
1129
|
+
if (tag !== "text" && tag !== "tspan") return false;
|
|
1130
|
+
const value = this.get_attr(id, "rotate");
|
|
1131
|
+
if (value === null) return false;
|
|
1132
|
+
return value.trim() !== "";
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* True iff this element's inline `style=""` declares a `transform:`
|
|
1136
|
+
* CSS property (which would shadow the editor's `transform=` writes).
|
|
1137
|
+
*/
|
|
1138
|
+
has_inline_css_transform(id) {
|
|
1139
|
+
const style = this.get_attr(id, "style");
|
|
1140
|
+
if (!style) return false;
|
|
1141
|
+
return CSS_TRANSFORM_PROPERTY.test(style);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* True iff this element has a direct `<animateTransform>` child
|
|
1145
|
+
* (which produces a time-varying transform invisible to attribute writes).
|
|
1146
|
+
* Only direct children are checked — nested cases attach to the nearer ancestor.
|
|
1147
|
+
*/
|
|
1148
|
+
has_animate_transform_child(id) {
|
|
1149
|
+
for (const c of this.children_of(id)) {
|
|
1150
|
+
const n = this.nodes.get(c);
|
|
1151
|
+
if (n?.kind === "element" && n.local === "animateTransform") return true;
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1062
1155
|
text_of(id) {
|
|
1063
1156
|
const n = this.nodes.get(id);
|
|
1064
1157
|
if (!n || n.kind !== "element") return "";
|
|
@@ -1249,160 +1342,169 @@ function delta_for(bbox, target, direction) {
|
|
|
1249
1342
|
}
|
|
1250
1343
|
//#endregion
|
|
1251
1344
|
//#region src/core/properties.ts
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
* Resolve a property's declared value and its provenance for a single node.
|
|
1301
|
-
*
|
|
1302
|
-
* The cascade engine here covers what the README says is in scope:
|
|
1303
|
-
* presentation attributes + inline style + parent inheritance + initial.
|
|
1304
|
-
* `<style>` block matching is deferred.
|
|
1305
|
-
*/
|
|
1306
|
-
function resolve_declared(doc, id, property) {
|
|
1307
|
-
const inline = doc.get_style(id, property);
|
|
1308
|
-
if (inline !== null && inline !== "") return {
|
|
1309
|
-
declared: inline,
|
|
1310
|
-
provenance: {
|
|
1311
|
-
origin: "author",
|
|
1312
|
-
carrier: "inline_style"
|
|
1313
|
-
}
|
|
1345
|
+
let properties;
|
|
1346
|
+
(function(_properties) {
|
|
1347
|
+
/** SVG properties that inherit per SVG 2 §6 (subset; the common ones). */
|
|
1348
|
+
const INHERITED = new Set([
|
|
1349
|
+
"color",
|
|
1350
|
+
"cursor",
|
|
1351
|
+
"direction",
|
|
1352
|
+
"fill",
|
|
1353
|
+
"fill-opacity",
|
|
1354
|
+
"fill-rule",
|
|
1355
|
+
"font",
|
|
1356
|
+
"font-family",
|
|
1357
|
+
"font-size",
|
|
1358
|
+
"font-style",
|
|
1359
|
+
"font-variant",
|
|
1360
|
+
"font-weight",
|
|
1361
|
+
"letter-spacing",
|
|
1362
|
+
"marker",
|
|
1363
|
+
"marker-end",
|
|
1364
|
+
"marker-mid",
|
|
1365
|
+
"marker-start",
|
|
1366
|
+
"paint-order",
|
|
1367
|
+
"pointer-events",
|
|
1368
|
+
"shape-rendering",
|
|
1369
|
+
"stroke",
|
|
1370
|
+
"stroke-dasharray",
|
|
1371
|
+
"stroke-dashoffset",
|
|
1372
|
+
"stroke-linecap",
|
|
1373
|
+
"stroke-linejoin",
|
|
1374
|
+
"stroke-miterlimit",
|
|
1375
|
+
"stroke-opacity",
|
|
1376
|
+
"stroke-width",
|
|
1377
|
+
"text-anchor",
|
|
1378
|
+
"text-rendering",
|
|
1379
|
+
"visibility",
|
|
1380
|
+
"word-spacing",
|
|
1381
|
+
"writing-mode"
|
|
1382
|
+
]);
|
|
1383
|
+
/** Initial values for known properties (subset). */
|
|
1384
|
+
const INITIAL = {
|
|
1385
|
+
fill: "black",
|
|
1386
|
+
stroke: "none",
|
|
1387
|
+
"fill-opacity": "1",
|
|
1388
|
+
"stroke-opacity": "1",
|
|
1389
|
+
"stroke-width": "1",
|
|
1390
|
+
opacity: "1",
|
|
1391
|
+
visibility: "visible",
|
|
1392
|
+
display: "inline"
|
|
1314
1393
|
};
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const
|
|
1325
|
-
if (
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1394
|
+
function resolve_declared(doc, id, property) {
|
|
1395
|
+
const inline = doc.get_style(id, property);
|
|
1396
|
+
if (inline !== null && inline !== "") return {
|
|
1397
|
+
declared: inline,
|
|
1398
|
+
provenance: {
|
|
1399
|
+
origin: "author",
|
|
1400
|
+
carrier: "inline_style"
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
const attr = doc.get_attr(id, property);
|
|
1404
|
+
if (attr !== null && attr !== "") return {
|
|
1405
|
+
declared: attr,
|
|
1406
|
+
provenance: {
|
|
1407
|
+
origin: "author",
|
|
1408
|
+
carrier: "presentation_attribute"
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
if (INHERITED.has(property)) {
|
|
1412
|
+
const parent = doc.parent_of(id);
|
|
1413
|
+
if (parent !== null && doc.is_element(parent)) {
|
|
1414
|
+
const r = resolve_declared(doc, parent, property);
|
|
1415
|
+
if (r.declared !== null) return {
|
|
1416
|
+
declared: r.declared,
|
|
1417
|
+
provenance: {
|
|
1418
|
+
origin: "author",
|
|
1419
|
+
carrier: "inherited"
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1334
1423
|
}
|
|
1424
|
+
return {
|
|
1425
|
+
declared: INITIAL[property] ?? null,
|
|
1426
|
+
provenance: {
|
|
1427
|
+
origin: "user_agent",
|
|
1428
|
+
carrier: "defaulted"
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1335
1431
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
case "height":
|
|
1365
|
-
case "cx":
|
|
1366
|
-
case "cy":
|
|
1367
|
-
case "r":
|
|
1368
|
-
case "rx":
|
|
1369
|
-
case "ry":
|
|
1370
|
-
case "font-size": {
|
|
1371
|
-
const n = parseFloat(trimmed);
|
|
1372
|
-
return Number.isFinite(n) ? n : trimmed;
|
|
1432
|
+
_properties.resolve_declared = resolve_declared;
|
|
1433
|
+
function compute_known(property, declared) {
|
|
1434
|
+
if (declared === null) return null;
|
|
1435
|
+
const trimmed = declared.trim();
|
|
1436
|
+
if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
|
|
1437
|
+
if (/^var\s*\(/i.test(trimmed)) return {
|
|
1438
|
+
error: "invalid_at_computed_value_time",
|
|
1439
|
+
reason: `var() substitution requires a cascade engine (not implemented)`
|
|
1440
|
+
};
|
|
1441
|
+
switch (property) {
|
|
1442
|
+
case "opacity":
|
|
1443
|
+
case "fill-opacity":
|
|
1444
|
+
case "stroke-opacity":
|
|
1445
|
+
case "stroke-width":
|
|
1446
|
+
case "x":
|
|
1447
|
+
case "y":
|
|
1448
|
+
case "width":
|
|
1449
|
+
case "height":
|
|
1450
|
+
case "cx":
|
|
1451
|
+
case "cy":
|
|
1452
|
+
case "r":
|
|
1453
|
+
case "rx":
|
|
1454
|
+
case "ry":
|
|
1455
|
+
case "font-size": {
|
|
1456
|
+
const n = parseFloat(trimmed);
|
|
1457
|
+
return Number.isFinite(n) ? n : trimmed;
|
|
1458
|
+
}
|
|
1459
|
+
default: return trimmed;
|
|
1373
1460
|
}
|
|
1374
|
-
default: return trimmed;
|
|
1375
1461
|
}
|
|
1376
|
-
|
|
1377
|
-
function
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1462
|
+
_properties.compute_known = compute_known;
|
|
1463
|
+
function read(doc, id, property) {
|
|
1464
|
+
const { declared, provenance } = resolve_declared(doc, id, property);
|
|
1465
|
+
return {
|
|
1466
|
+
declared,
|
|
1467
|
+
computed: compute_known(property, declared),
|
|
1468
|
+
provenance
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
_properties.read = read;
|
|
1472
|
+
function choose_write_carrier(doc, id, property) {
|
|
1473
|
+
const inline = doc.get_style(id, property);
|
|
1474
|
+
if (inline !== null && inline !== "") return "inline_style";
|
|
1475
|
+
return "presentation_attribute";
|
|
1476
|
+
}
|
|
1477
|
+
_properties.choose_write_carrier = choose_write_carrier;
|
|
1478
|
+
function value_equals(a, b) {
|
|
1479
|
+
if (a === b) return true;
|
|
1480
|
+
if (a.declared !== b.declared) return false;
|
|
1481
|
+
if (a.provenance.carrier !== b.provenance.carrier) return false;
|
|
1482
|
+
if (a.provenance.origin !== b.provenance.origin) return false;
|
|
1483
|
+
if (a.computed === b.computed) return true;
|
|
1484
|
+
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;
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
_properties.value_equals = value_equals;
|
|
1488
|
+
})(properties || (properties = {}));
|
|
1393
1489
|
//#endregion
|
|
1394
1490
|
//#region src/core/editor.ts
|
|
1395
1491
|
const PROVIDER_ID = "svg-editor";
|
|
1396
1492
|
/** Max characters in a synthesized display label before truncation. */
|
|
1397
1493
|
const DISPLAY_LABEL_MAX_LEN = 40;
|
|
1398
|
-
|
|
1494
|
+
/**
|
|
1495
|
+
* Wide internal factory — returns the full object including the
|
|
1496
|
+
* `_internal` / `keymap` surfaces in its inferred type. Stays private.
|
|
1497
|
+
* The public `createSvgEditor` below wraps this and narrows the return
|
|
1498
|
+
* to `SvgEditor` so the published `.d.ts` doesn't advertise internals.
|
|
1499
|
+
*/
|
|
1500
|
+
function _create_svg_editor_internal(opts) {
|
|
1399
1501
|
const doc = new SvgDocument(opts.svg);
|
|
1400
1502
|
const history = new _grida_history.HistoryImpl();
|
|
1401
1503
|
const defs = create_defs(doc);
|
|
1402
1504
|
let selection = [];
|
|
1403
1505
|
let scope = null;
|
|
1404
1506
|
let mode = "select";
|
|
1405
|
-
let tool =
|
|
1507
|
+
let tool = require_model.TOOL_CURSOR;
|
|
1406
1508
|
let version = 0;
|
|
1407
1509
|
/** Document-edit counter — only bumps on actual mutations, not selection. */
|
|
1408
1510
|
let doc_version = 0;
|
|
@@ -1416,7 +1518,7 @@ function createSvgEditor(opts) {
|
|
|
1416
1518
|
*/
|
|
1417
1519
|
let load_version = 0;
|
|
1418
1520
|
let style = {
|
|
1419
|
-
...
|
|
1521
|
+
...require_model.DEFAULT_STYLE,
|
|
1420
1522
|
...opts.style
|
|
1421
1523
|
};
|
|
1422
1524
|
const providers = opts.providers ?? {};
|
|
@@ -1439,6 +1541,7 @@ function createSvgEditor(opts) {
|
|
|
1439
1541
|
can_undo: history.stack.canUndo,
|
|
1440
1542
|
can_redo: history.stack.canRedo,
|
|
1441
1543
|
version,
|
|
1544
|
+
content_version: doc_version,
|
|
1442
1545
|
structure_version: doc.structure_version,
|
|
1443
1546
|
geometry_version: doc.geometry_version,
|
|
1444
1547
|
load_version
|
|
@@ -1527,7 +1630,7 @@ function createSvgEditor(opts) {
|
|
|
1527
1630
|
}
|
|
1528
1631
|
function tools_equal(a, b) {
|
|
1529
1632
|
if (a.type !== b.type) return false;
|
|
1530
|
-
if (a.type === "cursor") return true;
|
|
1633
|
+
if (a.type === "cursor" || a.type === "lasso" || a.type === "bend" || a.type === "insert-text") return true;
|
|
1531
1634
|
return b.type === "insert" && a.tag === b.tag;
|
|
1532
1635
|
}
|
|
1533
1636
|
function set_tool(next) {
|
|
@@ -1551,7 +1654,7 @@ function createSvgEditor(opts) {
|
|
|
1551
1654
|
const parent = doc.parent_of(id);
|
|
1552
1655
|
const children = doc.element_children_of(id);
|
|
1553
1656
|
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)) {
|
|
1657
|
+
if (pooled && pooled.tag === tag && pooled.name === name && pooled.parent === parent && require_model.array_shallow_equal(pooled.children, children)) {
|
|
1555
1658
|
map.set(id, pooled);
|
|
1556
1659
|
continue;
|
|
1557
1660
|
}
|
|
@@ -1588,8 +1691,8 @@ function createSvgEditor(opts) {
|
|
|
1588
1691
|
const key = `${id}${name}`;
|
|
1589
1692
|
const cached = property_cache.get(key);
|
|
1590
1693
|
if (cached && cached.doc_version === doc_version) return cached.value;
|
|
1591
|
-
const next =
|
|
1592
|
-
if (cached &&
|
|
1694
|
+
const next = properties.read(doc, id, name);
|
|
1695
|
+
if (cached && properties.value_equals(cached.value, next)) {
|
|
1593
1696
|
cached.doc_version = doc_version;
|
|
1594
1697
|
return cached.value;
|
|
1595
1698
|
}
|
|
@@ -1625,13 +1728,13 @@ function createSvgEditor(opts) {
|
|
|
1625
1728
|
const key = `${id}${channel}`;
|
|
1626
1729
|
const cached = paint_cache.get(key);
|
|
1627
1730
|
if (cached && cached.doc_version === doc_version) return cached.value;
|
|
1628
|
-
const { declared, provenance } = resolve_declared(doc, id, channel);
|
|
1731
|
+
const { declared, provenance } = properties.resolve_declared(doc, id, channel);
|
|
1629
1732
|
const next = {
|
|
1630
1733
|
declared,
|
|
1631
|
-
computed:
|
|
1734
|
+
computed: require_model.paint.parse(declared),
|
|
1632
1735
|
provenance
|
|
1633
1736
|
};
|
|
1634
|
-
if (cached &&
|
|
1737
|
+
if (cached && require_model.paint.value_equals(cached.value, next)) {
|
|
1635
1738
|
cached.doc_version = doc_version;
|
|
1636
1739
|
return cached.value;
|
|
1637
1740
|
}
|
|
@@ -1642,7 +1745,7 @@ function createSvgEditor(opts) {
|
|
|
1642
1745
|
return next;
|
|
1643
1746
|
}
|
|
1644
1747
|
function write_property(id, name, value) {
|
|
1645
|
-
if (choose_write_carrier(doc, id, name) === "inline_style") doc.set_style(id, name, value);
|
|
1748
|
+
if (properties.choose_write_carrier(doc, id, name) === "inline_style") doc.set_style(id, name, value);
|
|
1646
1749
|
else doc.set_attr(id, name, value);
|
|
1647
1750
|
}
|
|
1648
1751
|
function set_property(name, value) {
|
|
@@ -1705,14 +1808,14 @@ function createSvgEditor(opts) {
|
|
|
1705
1808
|
discard: () => preview.discard()
|
|
1706
1809
|
};
|
|
1707
1810
|
}
|
|
1708
|
-
function set_paint(channel,
|
|
1811
|
+
function set_paint(channel, p) {
|
|
1709
1812
|
if (selection.length === 0) return;
|
|
1710
|
-
set_property(channel,
|
|
1813
|
+
set_property(channel, require_model.paint.serialize(p));
|
|
1711
1814
|
}
|
|
1712
1815
|
function preview_paint(channel) {
|
|
1713
1816
|
const session = preview_property(channel);
|
|
1714
1817
|
return {
|
|
1715
|
-
update: (
|
|
1818
|
+
update: (p) => session.update(require_model.paint.serialize(p)),
|
|
1716
1819
|
commit: () => session.commit(),
|
|
1717
1820
|
discard: () => session.discard()
|
|
1718
1821
|
};
|
|
@@ -1730,7 +1833,7 @@ function createSvgEditor(opts) {
|
|
|
1730
1833
|
function do_translate_oneshot(delta, stages, label) {
|
|
1731
1834
|
if (selection.length === 0) return false;
|
|
1732
1835
|
if (delta.dx === 0 && delta.dy === 0) return false;
|
|
1733
|
-
const { apply, revert } =
|
|
1836
|
+
const { apply, revert } = require_model.translate_pipeline.prepare_rpc({
|
|
1734
1837
|
doc,
|
|
1735
1838
|
ids: selection,
|
|
1736
1839
|
delta: {
|
|
@@ -1759,7 +1862,7 @@ function createSvgEditor(opts) {
|
|
|
1759
1862
|
if (do_translate_oneshot(delta, void 0, "translate")) notify_translate_commit();
|
|
1760
1863
|
}
|
|
1761
1864
|
function nudge(delta) {
|
|
1762
|
-
if (do_translate_oneshot(delta,
|
|
1865
|
+
if (do_translate_oneshot(delta, require_model.translate_pipeline.stages.NUDGE, "nudge")) notify_translate_commit();
|
|
1763
1866
|
}
|
|
1764
1867
|
/**
|
|
1765
1868
|
* One-shot multi-member resize to an explicit target rect. Mirrors a
|
|
@@ -1784,13 +1887,13 @@ function createSvgEditor(opts) {
|
|
|
1784
1887
|
if (!geometry_provider) return false;
|
|
1785
1888
|
const members = [];
|
|
1786
1889
|
for (const id of ids) {
|
|
1787
|
-
if (!
|
|
1890
|
+
if (!require_model.resize_pipeline.intent.is_resizable(doc.tag_of(id))) continue;
|
|
1788
1891
|
const bbox = geometry_provider.bounds_of(id);
|
|
1789
1892
|
if (!bbox) continue;
|
|
1790
1893
|
members.push({
|
|
1791
1894
|
id,
|
|
1792
|
-
rz:
|
|
1793
|
-
tx_pre:
|
|
1895
|
+
rz: require_model.resize_pipeline.intent.capture_baseline(doc, id, bbox),
|
|
1896
|
+
tx_pre: require_model.translate_pipeline.intent.capture_baseline(doc, id),
|
|
1794
1897
|
transform_pre: doc.get_attr(id, "transform"),
|
|
1795
1898
|
bbox
|
|
1796
1899
|
});
|
|
@@ -1806,16 +1909,16 @@ function createSvgEditor(opts) {
|
|
|
1806
1909
|
const dx = target.x - union.x;
|
|
1807
1910
|
const dy = target.y - union.y;
|
|
1808
1911
|
const apply = () => {
|
|
1809
|
-
for (const m of members)
|
|
1912
|
+
for (const m of members) require_model.resize_pipeline.intent.apply(doc, m.id, m.rz, sx, sy, origin);
|
|
1810
1913
|
if (dx !== 0 || dy !== 0) for (const m of members) {
|
|
1811
|
-
const tx_after =
|
|
1812
|
-
|
|
1914
|
+
const tx_after = require_model.translate_pipeline.intent.capture_baseline(doc, m.id);
|
|
1915
|
+
require_model.translate_pipeline.intent.apply(doc, m.id, tx_after, dx, dy);
|
|
1813
1916
|
}
|
|
1814
1917
|
emit();
|
|
1815
1918
|
};
|
|
1816
1919
|
const revert = () => {
|
|
1817
1920
|
for (const m of members) {
|
|
1818
|
-
|
|
1921
|
+
require_model.resize_pipeline.intent.apply(doc, m.id, m.rz, 1, 1, origin);
|
|
1819
1922
|
doc.set_attr(m.id, "transform", m.transform_pre);
|
|
1820
1923
|
}
|
|
1821
1924
|
emit();
|
|
@@ -1856,10 +1959,11 @@ function createSvgEditor(opts) {
|
|
|
1856
1959
|
function rotate(angle, opts) {
|
|
1857
1960
|
const ids = opts?.ids ?? selection;
|
|
1858
1961
|
if (ids.length === 0) return false;
|
|
1859
|
-
const
|
|
1962
|
+
const pivot = opts?.pivot ?? default_rotate_pivot(ids);
|
|
1963
|
+
const prepared = require_model.rotate_pipeline.prepare_rpc({
|
|
1860
1964
|
doc,
|
|
1861
1965
|
ids,
|
|
1862
|
-
pivot
|
|
1966
|
+
pivot,
|
|
1863
1967
|
angle_radians: angle,
|
|
1864
1968
|
options: { angle_snap_step_radians: style.angle_snap_step_radians },
|
|
1865
1969
|
emit
|
|
@@ -1878,10 +1982,11 @@ function createSvgEditor(opts) {
|
|
|
1878
1982
|
function rotate_to(angle, opts) {
|
|
1879
1983
|
const ids = opts?.ids ?? selection;
|
|
1880
1984
|
if (ids.length === 0) return false;
|
|
1881
|
-
const
|
|
1985
|
+
const pivot = opts?.pivot ?? default_rotate_pivot(ids);
|
|
1986
|
+
const probe = require_model.rotate_pipeline.prepare_rpc({
|
|
1882
1987
|
doc,
|
|
1883
1988
|
ids,
|
|
1884
|
-
pivot
|
|
1989
|
+
pivot,
|
|
1885
1990
|
angle_radians: 0,
|
|
1886
1991
|
options: { angle_snap_step_radians: style.angle_snap_step_radians },
|
|
1887
1992
|
emit: () => {}
|
|
@@ -1891,12 +1996,12 @@ function createSvgEditor(opts) {
|
|
|
1891
1996
|
const apply = () => {
|
|
1892
1997
|
for (const m of probe.plan.members) {
|
|
1893
1998
|
const delta = angle - m.baseline.current_rotation_deg * DEG_TO_RAD;
|
|
1894
|
-
|
|
1999
|
+
require_model.rotate_pipeline.intent.apply(doc, m.id, m.baseline, delta);
|
|
1895
2000
|
}
|
|
1896
2001
|
emit();
|
|
1897
2002
|
};
|
|
1898
2003
|
const revert = () => {
|
|
1899
|
-
for (const m of probe.plan.members)
|
|
2004
|
+
for (const m of probe.plan.members) require_model.rotate_pipeline.intent.apply(doc, m.id, m.baseline, 0);
|
|
1900
2005
|
emit();
|
|
1901
2006
|
};
|
|
1902
2007
|
apply();
|
|
@@ -1916,7 +2021,7 @@ function createSvgEditor(opts) {
|
|
|
1916
2021
|
for (const id of ids) {
|
|
1917
2022
|
const pre = doc.get_attr(id, "transform");
|
|
1918
2023
|
if (pre === null) continue;
|
|
1919
|
-
const ops =
|
|
2024
|
+
const ops = require_model.transform.parse(pre);
|
|
1920
2025
|
if (ops === null) continue;
|
|
1921
2026
|
if (ops.length === 1 && ops[0].type === "matrix") continue;
|
|
1922
2027
|
members.push({
|
|
@@ -1926,104 +2031,11 @@ function createSvgEditor(opts) {
|
|
|
1926
2031
|
});
|
|
1927
2032
|
}
|
|
1928
2033
|
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
2034
|
const apply = () => {
|
|
2023
2035
|
for (const m of members) {
|
|
2024
|
-
let mat =
|
|
2025
|
-
for (const op of m.ops) mat =
|
|
2026
|
-
doc.set_attr(m.id, "transform",
|
|
2036
|
+
let mat = FLATTEN_IDENT;
|
|
2037
|
+
for (const op of m.ops) mat = flatten_mul(mat, flatten_op_to_mat(op));
|
|
2038
|
+
doc.set_attr(m.id, "transform", require_model.transform.emit([{
|
|
2027
2039
|
type: "matrix",
|
|
2028
2040
|
a: mat[0],
|
|
2029
2041
|
b: mat[1],
|
|
@@ -2073,7 +2085,7 @@ function createSvgEditor(opts) {
|
|
|
2073
2085
|
for (const id of ids) {
|
|
2074
2086
|
const bbox = geometry_provider.bounds_of(id);
|
|
2075
2087
|
if (!bbox) continue;
|
|
2076
|
-
const baseline =
|
|
2088
|
+
const baseline = require_model.translate_pipeline.intent.capture_baseline(doc, id);
|
|
2077
2089
|
if (baseline.type === "unsupported") continue;
|
|
2078
2090
|
members.push({
|
|
2079
2091
|
id,
|
|
@@ -2095,12 +2107,12 @@ function createSvgEditor(opts) {
|
|
|
2095
2107
|
const apply = () => {
|
|
2096
2108
|
for (const m of members) {
|
|
2097
2109
|
const d = deltas.get(m.id);
|
|
2098
|
-
if (d)
|
|
2110
|
+
if (d) require_model.translate_pipeline.intent.apply(doc, m.id, m.baseline, d.x, d.y);
|
|
2099
2111
|
}
|
|
2100
2112
|
emit();
|
|
2101
2113
|
};
|
|
2102
2114
|
const revert = () => {
|
|
2103
|
-
for (const m of members) if (deltas.has(m.id))
|
|
2115
|
+
for (const m of members) if (deltas.has(m.id)) require_model.translate_pipeline.intent.apply(doc, m.id, m.baseline, 0, 0);
|
|
2104
2116
|
emit();
|
|
2105
2117
|
};
|
|
2106
2118
|
apply();
|
|
@@ -2225,8 +2237,8 @@ function createSvgEditor(opts) {
|
|
|
2225
2237
|
});
|
|
2226
2238
|
});
|
|
2227
2239
|
}
|
|
2228
|
-
function group() {
|
|
2229
|
-
const plan =
|
|
2240
|
+
function group$1() {
|
|
2241
|
+
const plan = require_model.group.plan(doc, selection);
|
|
2230
2242
|
if (!plan) return false;
|
|
2231
2243
|
const group_id = doc.create_element("g");
|
|
2232
2244
|
const original_selection = selection;
|
|
@@ -2354,12 +2366,70 @@ function createSvgEditor(opts) {
|
|
|
2354
2366
|
}
|
|
2355
2367
|
};
|
|
2356
2368
|
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Text-creation bracket for the click-to-place text tool. Creates an
|
|
2371
|
+
* empty `<text>` with `initial` attrs, opens a single history preview,
|
|
2372
|
+
* and selects it — the DOM surface then mounts inline content-edit on
|
|
2373
|
+
* it. The surface finalizes the returned session when content-edit
|
|
2374
|
+
* exits:
|
|
2375
|
+
*
|
|
2376
|
+
* - `commit()` — snapshots the live text content into the delta and
|
|
2377
|
+
* commits ONE undo step (create + text together). Redo replays both,
|
|
2378
|
+
* so a redone text insert keeps its content (a plain `insert_preview`
|
|
2379
|
+
* would lose it — text is not an attribute).
|
|
2380
|
+
* - `discard()` — rolls the creation back entirely: no node, no
|
|
2381
|
+
* committed history entry. This is the empty-equals-delete rule for a
|
|
2382
|
+
* freshly-placed node (design:
|
|
2383
|
+
* `docs/wg/feat-svg-editor/text-tool.md`).
|
|
2384
|
+
*
|
|
2385
|
+
* The node is inserted empty on open (so the caret has somewhere to
|
|
2386
|
+
* live); live edits mutate its text in place, and `commit()` reads the
|
|
2387
|
+
* final text back off the document.
|
|
2388
|
+
*/
|
|
2389
|
+
function insert_text_preview(initial, opts) {
|
|
2390
|
+
const parent = opts?.parent ?? doc.root;
|
|
2391
|
+
const id = doc.create_element("text");
|
|
2392
|
+
const previous_selection = selection;
|
|
2393
|
+
const attrs = { ...initial };
|
|
2394
|
+
let committed_text = "";
|
|
2395
|
+
const apply = () => {
|
|
2396
|
+
for (const name in attrs) doc.set_attr(id, name, attrs[name]);
|
|
2397
|
+
if (doc.parent_of(id) === null) doc.insert(id, parent, null);
|
|
2398
|
+
doc.set_text(id, committed_text);
|
|
2399
|
+
set_selection([id]);
|
|
2400
|
+
};
|
|
2401
|
+
const revert = () => {
|
|
2402
|
+
doc.remove(id);
|
|
2403
|
+
set_selection(previous_selection);
|
|
2404
|
+
};
|
|
2405
|
+
const preview = history.preview("insert text");
|
|
2406
|
+
let active = true;
|
|
2407
|
+
preview.set({
|
|
2408
|
+
providerId: PROVIDER_ID,
|
|
2409
|
+
apply,
|
|
2410
|
+
revert
|
|
2411
|
+
});
|
|
2412
|
+
return {
|
|
2413
|
+
id,
|
|
2414
|
+
commit() {
|
|
2415
|
+
if (!active) return;
|
|
2416
|
+
active = false;
|
|
2417
|
+
committed_text = doc.text_of(id);
|
|
2418
|
+
preview.commit();
|
|
2419
|
+
},
|
|
2420
|
+
discard() {
|
|
2421
|
+
if (!active) return;
|
|
2422
|
+
active = false;
|
|
2423
|
+
preview.discard();
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2357
2427
|
/** Per-tag default paint attrs. Wrapped so callers don't need to depend
|
|
2358
2428
|
* on the InsertableTag type — `insert()` accepts arbitrary string tags
|
|
2359
2429
|
* (so `commands.insert("path", ...)` works for paste / RPC) but only
|
|
2360
2430
|
* the closed insertable set gets default paint. */
|
|
2361
2431
|
function default_paint_attrs_for(tag) {
|
|
2362
|
-
if (tag === "rect" || tag === "ellipse" || tag === "line") return
|
|
2432
|
+
if (tag === "rect" || tag === "ellipse" || tag === "line") return require_model.insertions.default_paint_attrs(tag);
|
|
2363
2433
|
return {};
|
|
2364
2434
|
}
|
|
2365
2435
|
function set_text(value) {
|
|
@@ -2408,7 +2478,7 @@ function createSvgEditor(opts) {
|
|
|
2408
2478
|
function enter_content_edit(target) {
|
|
2409
2479
|
const id = target ?? (selection.length === 1 ? selection[0] : null);
|
|
2410
2480
|
if (!id) return false;
|
|
2411
|
-
if (!doc.is_text_edit_target(id)) return false;
|
|
2481
|
+
if (!doc.is_text_edit_target(id) && doc.is_vector_edit_target(id) === null) return false;
|
|
2412
2482
|
if (!content_edit_driver) return false;
|
|
2413
2483
|
return content_edit_driver(id);
|
|
2414
2484
|
}
|
|
@@ -2417,7 +2487,7 @@ function createSvgEditor(opts) {
|
|
|
2417
2487
|
selection = [];
|
|
2418
2488
|
scope = null;
|
|
2419
2489
|
mode = "select";
|
|
2420
|
-
tool =
|
|
2490
|
+
tool = require_model.TOOL_CURSOR;
|
|
2421
2491
|
history.clear();
|
|
2422
2492
|
baseline_doc_version = doc_version;
|
|
2423
2493
|
load_version++;
|
|
@@ -2456,7 +2526,7 @@ function createSvgEditor(opts) {
|
|
|
2456
2526
|
align,
|
|
2457
2527
|
reorder,
|
|
2458
2528
|
remove,
|
|
2459
|
-
group,
|
|
2529
|
+
group: group$1,
|
|
2460
2530
|
insert,
|
|
2461
2531
|
insert_preview,
|
|
2462
2532
|
set_text,
|
|
@@ -2480,7 +2550,7 @@ function createSvgEditor(opts) {
|
|
|
2480
2550
|
selection = [];
|
|
2481
2551
|
scope = null;
|
|
2482
2552
|
mode = "select";
|
|
2483
|
-
tool =
|
|
2553
|
+
tool = require_model.TOOL_CURSOR;
|
|
2484
2554
|
baseline_doc_version = doc_version;
|
|
2485
2555
|
emit();
|
|
2486
2556
|
}
|
|
@@ -2512,6 +2582,10 @@ function createSvgEditor(opts) {
|
|
|
2512
2582
|
emit();
|
|
2513
2583
|
}
|
|
2514
2584
|
const public_editor = {
|
|
2585
|
+
/**
|
|
2586
|
+
* Low-level IR handle. Mutating directly bypasses history; prefer
|
|
2587
|
+
* `editor.commands` for app code.
|
|
2588
|
+
*/
|
|
2515
2589
|
document: doc,
|
|
2516
2590
|
get state() {
|
|
2517
2591
|
return snapshot();
|
|
@@ -2522,9 +2596,28 @@ function createSvgEditor(opts) {
|
|
|
2522
2596
|
node_paint,
|
|
2523
2597
|
dom_computed_property,
|
|
2524
2598
|
dom_computed_paint,
|
|
2599
|
+
/**
|
|
2600
|
+
* Enter content-edit mode on a `<text>` node. Returns `false` (no-op)
|
|
2601
|
+
* when no DOM surface is attached.
|
|
2602
|
+
*/
|
|
2525
2603
|
enter_content_edit,
|
|
2526
2604
|
defs,
|
|
2527
2605
|
commands,
|
|
2606
|
+
/**
|
|
2607
|
+
* Human-readable label for hierarchy panels. SVG has no native "name";
|
|
2608
|
+
* this is the package's single source of truth so panels don't reinvent
|
|
2609
|
+
* the rule.
|
|
2610
|
+
*
|
|
2611
|
+
* Rule:
|
|
2612
|
+
* - `<text>` → text content, whitespace-collapsed and truncated at
|
|
2613
|
+
* ~40 chars (falls back to `"text"` for empty content).
|
|
2614
|
+
* - Otherwise → tag name, suffixed with `#id` when the `id` attribute
|
|
2615
|
+
* is present (e.g. `"rect #sun"`).
|
|
2616
|
+
*
|
|
2617
|
+
* `opts.tagLabel` lets callers substitute a friendlier or localized
|
|
2618
|
+
* term for the raw tag (e.g. `"rect"` → `"Rectangle"`). Only invoked
|
|
2619
|
+
* on the non-text branch.
|
|
2620
|
+
*/
|
|
2528
2621
|
display_label(id, opts) {
|
|
2529
2622
|
const tag = doc.tag_of(id);
|
|
2530
2623
|
if (tag === "text") {
|
|
@@ -2539,30 +2632,59 @@ function createSvgEditor(opts) {
|
|
|
2539
2632
|
tree() {
|
|
2540
2633
|
return tree_snapshot();
|
|
2541
2634
|
},
|
|
2635
|
+
/**
|
|
2636
|
+
* The effective hover from the attached HUD surface — what's under the
|
|
2637
|
+
* pointer, OR whatever `set_surface_hover_override` last pushed. Used
|
|
2638
|
+
* by out-of-canvas UI (layers panel, breadcrumbs) to mirror the canvas
|
|
2639
|
+
* highlight. Returns `null` when nothing is hovered.
|
|
2640
|
+
*/
|
|
2542
2641
|
surface_hover() {
|
|
2543
2642
|
return current_surface_hover;
|
|
2544
2643
|
},
|
|
2644
|
+
/**
|
|
2645
|
+
* Push a hover override into the HUD surface — e.g. when the user
|
|
2646
|
+
* hovers a row in a layers panel. The HUD will render the override's
|
|
2647
|
+
* outline and (when applicable) drive measurement to that node.
|
|
2648
|
+
* Pass `null` to clear and let the pointer pick take over again.
|
|
2649
|
+
*/
|
|
2545
2650
|
set_surface_hover_override(id) {
|
|
2546
2651
|
if (surface_hover_override === id) return;
|
|
2547
2652
|
surface_hover_override = id;
|
|
2548
2653
|
if (surface_hover_override_driver) surface_hover_override_driver(id);
|
|
2549
2654
|
},
|
|
2655
|
+
/**
|
|
2656
|
+
* Subscribe to changes in the effective surface hover. Fires when the
|
|
2657
|
+
* HUD reports a new pointer pick AND when an override is set/cleared.
|
|
2658
|
+
* Cheap channel — does NOT bump `state.version`.
|
|
2659
|
+
*/
|
|
2550
2660
|
subscribe_surface_hover(cb) {
|
|
2551
2661
|
surface_hover_listeners.add(cb);
|
|
2552
2662
|
return () => {
|
|
2553
2663
|
surface_hover_listeners.delete(cb);
|
|
2554
2664
|
};
|
|
2555
2665
|
},
|
|
2666
|
+
/**
|
|
2667
|
+
* Subscribe to bounds-affecting changes. Fires when any document
|
|
2668
|
+
* mutation advances `state.geometry_version` — drag, resize, text
|
|
2669
|
+
* edit, structural insert/remove. Skips presentation-only writes
|
|
2670
|
+
* (fill, opacity, stroke-color).
|
|
2671
|
+
*/
|
|
2556
2672
|
subscribe_geometry(cb) {
|
|
2557
2673
|
geometry_listeners.add(cb);
|
|
2558
2674
|
return () => {
|
|
2559
2675
|
geometry_listeners.delete(cb);
|
|
2560
2676
|
};
|
|
2561
2677
|
},
|
|
2678
|
+
/**
|
|
2679
|
+
* World-space geometry queries. Non-null when a DOM surface is
|
|
2680
|
+
* attached; null otherwise (queries need a renderer to read bbox
|
|
2681
|
+
* from). Read-only — never mutates document state.
|
|
2682
|
+
*/
|
|
2562
2683
|
get geometry() {
|
|
2563
2684
|
return geometry_provider;
|
|
2564
2685
|
},
|
|
2565
2686
|
modes,
|
|
2687
|
+
/** Switch the active tool. No history entry; bumps `state.version`. */
|
|
2566
2688
|
set_tool,
|
|
2567
2689
|
get style() {
|
|
2568
2690
|
return style;
|
|
@@ -2578,6 +2700,7 @@ function createSvgEditor(opts) {
|
|
|
2578
2700
|
_internal: {
|
|
2579
2701
|
doc,
|
|
2580
2702
|
history: { preview: (label) => history.preview(label) },
|
|
2703
|
+
insert_text_preview,
|
|
2581
2704
|
emit,
|
|
2582
2705
|
subscribe_translate_commit(cb) {
|
|
2583
2706
|
translate_commit_listeners.add(cb);
|
|
@@ -2609,37 +2732,112 @@ function createSvgEditor(opts) {
|
|
|
2609
2732
|
applyDefaultBindings(keymap);
|
|
2610
2733
|
return public_editor;
|
|
2611
2734
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
function
|
|
2620
|
-
if (
|
|
2621
|
-
|
|
2622
|
-
|
|
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;
|
|
2735
|
+
/**
|
|
2736
|
+
* Construct a headless SVG editor. The returned object is the public
|
|
2737
|
+
* editor surface — observation (`state`, `subscribe`), commands
|
|
2738
|
+
* (`commands.*`), lifecycle (`attach` / `dispose`), and the typed-read
|
|
2739
|
+
* caches (`node_paint`, `node_properties`). Surfaces (DOM, headless)
|
|
2740
|
+
* attach later via `editor.attach(surface)`.
|
|
2741
|
+
*/
|
|
2742
|
+
function createSvgEditor(opts) {
|
|
2743
|
+
if (opts == null || typeof opts.svg !== "string") {
|
|
2744
|
+
const got = opts == null ? String(opts) : opts.svg === null ? "null" : typeof opts.svg;
|
|
2745
|
+
throw new TypeError(`createSvgEditor({ svg }) requires { svg: string }, got svg=${got}`);
|
|
2628
2746
|
}
|
|
2629
|
-
|
|
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;
|
|
2747
|
+
return _create_svg_editor_internal(opts);
|
|
2634
2748
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2749
|
+
const FLATTEN_IDENT = [
|
|
2750
|
+
1,
|
|
2751
|
+
0,
|
|
2752
|
+
0,
|
|
2753
|
+
1,
|
|
2754
|
+
0,
|
|
2755
|
+
0
|
|
2756
|
+
];
|
|
2757
|
+
function flatten_mul(m1, m2) {
|
|
2758
|
+
const [a1, b1, c1, d1, e1, f1] = m1;
|
|
2759
|
+
const [a2, b2, c2, d2, e2, f2] = m2;
|
|
2760
|
+
return [
|
|
2761
|
+
a1 * a2 + c1 * b2,
|
|
2762
|
+
b1 * a2 + d1 * b2,
|
|
2763
|
+
a1 * c2 + c1 * d2,
|
|
2764
|
+
b1 * c2 + d1 * d2,
|
|
2765
|
+
a1 * e2 + c1 * f2 + e1,
|
|
2766
|
+
b1 * e2 + d1 * f2 + f1
|
|
2767
|
+
];
|
|
2768
|
+
}
|
|
2769
|
+
function flatten_op_to_mat(op) {
|
|
2770
|
+
switch (op.type) {
|
|
2771
|
+
case "matrix": return [
|
|
2772
|
+
op.a,
|
|
2773
|
+
op.b,
|
|
2774
|
+
op.c,
|
|
2775
|
+
op.d,
|
|
2776
|
+
op.e,
|
|
2777
|
+
op.f
|
|
2778
|
+
];
|
|
2779
|
+
case "translate": return [
|
|
2780
|
+
1,
|
|
2781
|
+
0,
|
|
2782
|
+
0,
|
|
2783
|
+
1,
|
|
2784
|
+
op.tx,
|
|
2785
|
+
op.ty
|
|
2786
|
+
];
|
|
2787
|
+
case "rotate": {
|
|
2788
|
+
const rad = op.angle * Math.PI / 180;
|
|
2789
|
+
const c = Math.cos(rad);
|
|
2790
|
+
const s = Math.sin(rad);
|
|
2791
|
+
if (op.cx === 0 && op.cy === 0) return [
|
|
2792
|
+
c,
|
|
2793
|
+
s,
|
|
2794
|
+
-s,
|
|
2795
|
+
c,
|
|
2796
|
+
0,
|
|
2797
|
+
0
|
|
2798
|
+
];
|
|
2799
|
+
const e = op.cx - c * op.cx + s * op.cy;
|
|
2800
|
+
const f = op.cy - s * op.cx - c * op.cy;
|
|
2801
|
+
return [
|
|
2802
|
+
c,
|
|
2803
|
+
s,
|
|
2804
|
+
-s,
|
|
2805
|
+
c,
|
|
2806
|
+
e,
|
|
2807
|
+
f
|
|
2808
|
+
];
|
|
2809
|
+
}
|
|
2810
|
+
case "scale": return [
|
|
2811
|
+
op.sx,
|
|
2812
|
+
0,
|
|
2813
|
+
0,
|
|
2814
|
+
op.sy,
|
|
2815
|
+
0,
|
|
2816
|
+
0
|
|
2817
|
+
];
|
|
2818
|
+
case "skewX": {
|
|
2819
|
+
const rad = op.angle * Math.PI / 180;
|
|
2820
|
+
return [
|
|
2821
|
+
1,
|
|
2822
|
+
0,
|
|
2823
|
+
Math.tan(rad),
|
|
2824
|
+
1,
|
|
2825
|
+
0,
|
|
2826
|
+
0
|
|
2827
|
+
];
|
|
2828
|
+
}
|
|
2829
|
+
case "skewY": {
|
|
2830
|
+
const rad = op.angle * Math.PI / 180;
|
|
2831
|
+
return [
|
|
2832
|
+
1,
|
|
2833
|
+
Math.tan(rad),
|
|
2834
|
+
0,
|
|
2835
|
+
1,
|
|
2836
|
+
0,
|
|
2837
|
+
0
|
|
2838
|
+
];
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2643
2841
|
}
|
|
2644
2842
|
//#endregion
|
|
2645
2843
|
Object.defineProperty(exports, "createSvgEditor", {
|