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