@grida/svg-editor 1.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +831 -0
- package/dist/dom-CfP_ZURh.js +963 -0
- package/dist/dom-kA8NDuVh.mjs +929 -0
- package/dist/dom.d.mts +16 -0
- package/dist/dom.d.ts +16 -0
- package/dist/dom.js +3 -0
- package/dist/dom.mjs +2 -0
- package/dist/editor-B5z-gTML.mjs +1821 -0
- package/dist/editor-CTtU2gu4.d.ts +607 -0
- package/dist/editor-DQWUWrVZ.js +1833 -0
- package/dist/editor-JY7AQrR1.d.mts +607 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.mjs +2 -0
- package/dist/paint-DHq_3iwU.js +509 -0
- package/dist/paint-DuCg6Y-K.mjs +461 -0
- package/dist/react.d.mts +49 -0
- package/dist/react.d.ts +49 -0
- package/dist/react.js +97 -0
- package/dist/react.mjs +92 -0
- package/package.json +66 -0
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
const require_paint = require("./paint-DHq_3iwU.js");
|
|
24
|
+
let _grida_text_editor_dom = require("@grida/text-editor/dom");
|
|
25
|
+
let _grida_hud = require("@grida/hud");
|
|
26
|
+
let _grida_cmath__measurement = require("@grida/cmath/_measurement");
|
|
27
|
+
let _grida_cmath = require("@grida/cmath");
|
|
28
|
+
_grida_cmath = __toESM(_grida_cmath);
|
|
29
|
+
//#region src/text-surface.ts
|
|
30
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
31
|
+
const XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
32
|
+
var SvgTextSurface = class {
|
|
33
|
+
constructor(textEl) {
|
|
34
|
+
this.prevXmlSpace = void 0;
|
|
35
|
+
this.last_caret_idx = -1;
|
|
36
|
+
this.last_caret_visible = false;
|
|
37
|
+
this.last_sel_start = -1;
|
|
38
|
+
this.last_sel_end = -1;
|
|
39
|
+
this.textEl = textEl;
|
|
40
|
+
const ownerDoc = textEl.ownerDocument;
|
|
41
|
+
const parent = textEl.parentNode;
|
|
42
|
+
if (!parent) throw new Error("text element has no parent");
|
|
43
|
+
const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
|
|
44
|
+
if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
|
|
45
|
+
this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
|
|
46
|
+
textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
|
|
47
|
+
}
|
|
48
|
+
const selection = ownerDoc.createElementNS(SVG_NS, "rect");
|
|
49
|
+
selection.setAttribute("fill", "#2563eb");
|
|
50
|
+
selection.setAttribute("fill-opacity", "0.25");
|
|
51
|
+
selection.setAttribute("pointer-events", "none");
|
|
52
|
+
selection.setAttribute("data-svg-text-edit-selection", "");
|
|
53
|
+
selection.style.display = "none";
|
|
54
|
+
parent.insertBefore(selection, textEl);
|
|
55
|
+
this.selectionRect = selection;
|
|
56
|
+
const caret = ownerDoc.createElementNS(SVG_NS, "rect");
|
|
57
|
+
caret.setAttribute("fill", "#2563eb");
|
|
58
|
+
caret.setAttribute("pointer-events", "none");
|
|
59
|
+
caret.setAttribute("data-svg-text-edit-caret", "");
|
|
60
|
+
caret.style.display = "none";
|
|
61
|
+
parent.insertBefore(caret, textEl.nextSibling);
|
|
62
|
+
this.caretRect = caret;
|
|
63
|
+
}
|
|
64
|
+
setText(text) {
|
|
65
|
+
if (this.textEl.textContent !== text) this.textEl.textContent = text;
|
|
66
|
+
}
|
|
67
|
+
setCaret(index, visible) {
|
|
68
|
+
if (index === this.last_caret_idx && visible === this.last_caret_visible) return;
|
|
69
|
+
this.last_caret_idx = index;
|
|
70
|
+
this.last_caret_visible = visible;
|
|
71
|
+
if (!visible) {
|
|
72
|
+
this.caretRect.style.display = "none";
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const m = this.metrics();
|
|
76
|
+
const x = this.charX(index);
|
|
77
|
+
this.caretRect.setAttribute("x", String(x - .75));
|
|
78
|
+
this.caretRect.setAttribute("y", String(m.top));
|
|
79
|
+
this.caretRect.setAttribute("width", "1.5");
|
|
80
|
+
this.caretRect.setAttribute("height", String(m.height));
|
|
81
|
+
this.caretRect.style.display = "block";
|
|
82
|
+
}
|
|
83
|
+
setSelection(start, end) {
|
|
84
|
+
if (start === this.last_sel_start && end === this.last_sel_end) return;
|
|
85
|
+
this.last_sel_start = start;
|
|
86
|
+
this.last_sel_end = end;
|
|
87
|
+
if (start === end) {
|
|
88
|
+
this.selectionRect.style.display = "none";
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const m = this.metrics();
|
|
92
|
+
const x1 = this.charX(start);
|
|
93
|
+
const x2 = this.charX(end);
|
|
94
|
+
this.selectionRect.setAttribute("x", String(Math.min(x1, x2)));
|
|
95
|
+
this.selectionRect.setAttribute("y", String(m.top));
|
|
96
|
+
this.selectionRect.setAttribute("width", String(Math.abs(x2 - x1)));
|
|
97
|
+
this.selectionRect.setAttribute("height", String(m.height));
|
|
98
|
+
this.selectionRect.style.display = "block";
|
|
99
|
+
}
|
|
100
|
+
dispose(keepEditMutations = false) {
|
|
101
|
+
this.caretRect.remove();
|
|
102
|
+
this.selectionRect.remove();
|
|
103
|
+
if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
|
|
104
|
+
else this.textEl.setAttributeNS(XML_NS, "xml:space", this.prevXmlSpace);
|
|
105
|
+
this.prevXmlSpace = void 0;
|
|
106
|
+
}
|
|
107
|
+
positionAtPoint(clientX, clientY) {
|
|
108
|
+
const ctm = this.textEl.getScreenCTM();
|
|
109
|
+
const svg = this.textEl.ownerSVGElement;
|
|
110
|
+
if (!ctm || !svg) return 0;
|
|
111
|
+
const pt = svg.createSVGPoint();
|
|
112
|
+
pt.x = clientX;
|
|
113
|
+
pt.y = clientY;
|
|
114
|
+
const local = pt.matrixTransform(ctm.inverse());
|
|
115
|
+
return this.localXToCharIndex(local.x);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Single-line `<text>` element: there's no "previous visual line" to move
|
|
119
|
+
* to. Cocoa single-line convention: Up/PageUp/line_start → doc start;
|
|
120
|
+
* Down/PageDown/line_end → doc end.
|
|
121
|
+
*/
|
|
122
|
+
positionForNavigation(_index, direction) {
|
|
123
|
+
const text = this.textEl.textContent ?? "";
|
|
124
|
+
switch (direction) {
|
|
125
|
+
case "up":
|
|
126
|
+
case "page_up":
|
|
127
|
+
case "line_start": return 0;
|
|
128
|
+
case "down":
|
|
129
|
+
case "page_down":
|
|
130
|
+
case "line_end": return text.length;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
metrics() {
|
|
134
|
+
try {
|
|
135
|
+
const b = this.textEl.getBBox();
|
|
136
|
+
if (b.height > 0) return {
|
|
137
|
+
top: b.y,
|
|
138
|
+
height: b.height
|
|
139
|
+
};
|
|
140
|
+
} catch {}
|
|
141
|
+
const fontSize = parseFloat(this.textEl.ownerDocument.defaultView?.getComputedStyle(this.textEl).fontSize ?? "16") || 16;
|
|
142
|
+
return {
|
|
143
|
+
top: parseFloat(this.textEl.getAttribute("y") ?? "0") - fontSize * .85,
|
|
144
|
+
height: fontSize
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
charX(i) {
|
|
148
|
+
const text = this.textEl.textContent ?? "";
|
|
149
|
+
const baseX = parseFloat(this.textEl.getAttribute("x") ?? "0");
|
|
150
|
+
if (text.length === 0) return baseX;
|
|
151
|
+
if (i <= 0) try {
|
|
152
|
+
return this.textEl.getStartPositionOfChar(0).x;
|
|
153
|
+
} catch {
|
|
154
|
+
return baseX;
|
|
155
|
+
}
|
|
156
|
+
if (i >= text.length) try {
|
|
157
|
+
return this.textEl.getEndPositionOfChar(text.length - 1).x;
|
|
158
|
+
} catch {
|
|
159
|
+
return baseX;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return this.textEl.getStartPositionOfChar(i).x;
|
|
163
|
+
} catch {
|
|
164
|
+
return baseX;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
localXToCharIndex(localX) {
|
|
168
|
+
const text = this.textEl.textContent ?? "";
|
|
169
|
+
if (!text) return 0;
|
|
170
|
+
for (let i = 0; i < text.length; i++) try {
|
|
171
|
+
const ext = this.textEl.getExtentOfChar(i);
|
|
172
|
+
if (localX < ext.x + ext.width / 2) return i;
|
|
173
|
+
} catch {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
return text.length;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/dom.ts
|
|
181
|
+
const ID_ATTR = "data-grida-id";
|
|
182
|
+
/** KeyboardEvent.key values for the modifiers the surface tracks. Used to
|
|
183
|
+
* short-circuit window-level `keydown`/`keyup` for non-modifier keystrokes. */
|
|
184
|
+
const IS_MODIFIER_KEY = {
|
|
185
|
+
Shift: true,
|
|
186
|
+
Alt: true,
|
|
187
|
+
Meta: true,
|
|
188
|
+
Control: true
|
|
189
|
+
};
|
|
190
|
+
/** Sentinel placed in `text_edit` before `createTextEditor` returns, so the
|
|
191
|
+
* surface skips render() during the in-flight mount and doesn't yank the
|
|
192
|
+
* live `<text>` element out from under the about-to-mount text surface. */
|
|
193
|
+
const TEXT_EDIT_PENDING = { __pending: true };
|
|
194
|
+
/**
|
|
195
|
+
* Attach a DOM surface to a headless editor. Returns a `SurfaceHandle` whose
|
|
196
|
+
* `detach()` is the inverse — DOM cleared, listeners removed.
|
|
197
|
+
*
|
|
198
|
+
* Usage is one-shot per container: the surface owns the container's children
|
|
199
|
+
* for its lifetime, and `detach()` restores it to empty.
|
|
200
|
+
*/
|
|
201
|
+
function attach_dom_surface(editor, options) {
|
|
202
|
+
const surface = new DomSurface(editor, options.container);
|
|
203
|
+
return editor.attach(surface);
|
|
204
|
+
}
|
|
205
|
+
var DomSurface = class {
|
|
206
|
+
constructor(editor, container) {
|
|
207
|
+
this.editor = editor;
|
|
208
|
+
this.container = container;
|
|
209
|
+
this.svg_root = null;
|
|
210
|
+
this.teardown = [];
|
|
211
|
+
this.element_index = /* @__PURE__ */ new Map();
|
|
212
|
+
this.resize_observer = null;
|
|
213
|
+
this.active_preview = null;
|
|
214
|
+
this.text_edit = null;
|
|
215
|
+
this.text_edit_target = null;
|
|
216
|
+
this.text_edit_original = "";
|
|
217
|
+
this.editor_hover_internal = null;
|
|
218
|
+
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
219
|
+
container.style.userSelect = "none";
|
|
220
|
+
container.style.webkitUserSelect = "none";
|
|
221
|
+
this.hud_canvas = container.ownerDocument.createElement("canvas");
|
|
222
|
+
Object.assign(this.hud_canvas.style, {
|
|
223
|
+
position: "absolute",
|
|
224
|
+
left: "0",
|
|
225
|
+
top: "0",
|
|
226
|
+
pointerEvents: "none"
|
|
227
|
+
});
|
|
228
|
+
container.appendChild(this.hud_canvas);
|
|
229
|
+
this.hud = new _grida_hud.Surface(this.hud_canvas, {
|
|
230
|
+
pick: (p) => this.hit_test(p[0], p[1]),
|
|
231
|
+
shapeOf: (id) => this.shape_of(id),
|
|
232
|
+
onIntent: (i) => this.commit_intent(i),
|
|
233
|
+
style: { chromeColor: editor.style.chrome_color }
|
|
234
|
+
});
|
|
235
|
+
this.render();
|
|
236
|
+
this.sync_canvas_size();
|
|
237
|
+
this.sync_surface_selection();
|
|
238
|
+
this.redraw();
|
|
239
|
+
const win = container.ownerDocument.defaultView ?? window;
|
|
240
|
+
const raf = win.requestAnimationFrame(() => {
|
|
241
|
+
this.sync_canvas_size();
|
|
242
|
+
this.redraw();
|
|
243
|
+
});
|
|
244
|
+
this.teardown.push(() => win.cancelAnimationFrame(raf));
|
|
245
|
+
const unsub = editor.subscribe(() => {
|
|
246
|
+
this.render();
|
|
247
|
+
this.sync_surface_selection();
|
|
248
|
+
this.sync_canvas_size();
|
|
249
|
+
});
|
|
250
|
+
this.teardown.push(unsub);
|
|
251
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
252
|
+
this.resize_observer = new ResizeObserver(() => this.sync_canvas_size());
|
|
253
|
+
this.resize_observer.observe(container);
|
|
254
|
+
this.teardown.push(() => this.resize_observer?.disconnect());
|
|
255
|
+
} else {
|
|
256
|
+
const win = container.ownerDocument.defaultView ?? window;
|
|
257
|
+
const fn = () => this.sync_canvas_size();
|
|
258
|
+
win.addEventListener("resize", fn);
|
|
259
|
+
this.teardown.push(() => win.removeEventListener("resize", fn));
|
|
260
|
+
}
|
|
261
|
+
this.wire_events();
|
|
262
|
+
const internal = editor._internal;
|
|
263
|
+
this.editor_hover_internal = internal;
|
|
264
|
+
internal.set_content_edit_driver((id) => this.enter_content_edit(id));
|
|
265
|
+
this.teardown.push(() => internal.set_content_edit_driver(null));
|
|
266
|
+
internal.set_computed_resolver({
|
|
267
|
+
computed_property: (id, name) => {
|
|
268
|
+
const el = this.element_index.get(id);
|
|
269
|
+
if (!el) return null;
|
|
270
|
+
const value = getComputedStyle(el).getPropertyValue(name);
|
|
271
|
+
return value === "" ? null : value;
|
|
272
|
+
},
|
|
273
|
+
computed_paint: (id, channel) => {
|
|
274
|
+
const el = this.element_index.get(id);
|
|
275
|
+
if (!el) return null;
|
|
276
|
+
const computed = getComputedStyle(el).getPropertyValue(channel);
|
|
277
|
+
if (computed === "") return null;
|
|
278
|
+
return {
|
|
279
|
+
computed,
|
|
280
|
+
resolved_paint: require_paint.parse_paint(computed)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
this.teardown.push(() => internal.set_computed_resolver(null));
|
|
285
|
+
internal.set_surface_hover_override_driver((id) => {
|
|
286
|
+
const response = this.hud.setHoverOverride(id);
|
|
287
|
+
if (response.hoverChanged) internal.push_surface_hover(this.hud.hover());
|
|
288
|
+
if (response.needsRedraw) this.redraw();
|
|
289
|
+
});
|
|
290
|
+
this.teardown.push(() => internal.set_surface_hover_override_driver(null));
|
|
291
|
+
}
|
|
292
|
+
paint(_snapshot) {}
|
|
293
|
+
hit_test(x, y) {
|
|
294
|
+
const owner_doc = this.container.ownerDocument;
|
|
295
|
+
const cr = this.container.getBoundingClientRect();
|
|
296
|
+
const target = owner_doc.elementFromPoint(cr.left + x, cr.top + y);
|
|
297
|
+
if (!(target instanceof SVGElement)) return null;
|
|
298
|
+
const root_id = this.editor.tree().root;
|
|
299
|
+
let cur = target;
|
|
300
|
+
while (cur instanceof Element) {
|
|
301
|
+
const id = cur.getAttribute(ID_ATTR);
|
|
302
|
+
if (id) return id === root_id ? null : id;
|
|
303
|
+
cur = cur.parentElement;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
on_input(_listener) {
|
|
308
|
+
return () => {};
|
|
309
|
+
}
|
|
310
|
+
dispose() {
|
|
311
|
+
if (this.text_edit) {
|
|
312
|
+
this.text_edit.cancel();
|
|
313
|
+
this.text_edit = null;
|
|
314
|
+
this.text_edit_target = null;
|
|
315
|
+
}
|
|
316
|
+
for (const fn of this.teardown) fn();
|
|
317
|
+
this.teardown = [];
|
|
318
|
+
this.hud.dispose();
|
|
319
|
+
this.hud_canvas.remove();
|
|
320
|
+
if (this.svg_root) this.svg_root.remove();
|
|
321
|
+
this.svg_root = null;
|
|
322
|
+
this.element_index.clear();
|
|
323
|
+
this.active_preview = null;
|
|
324
|
+
}
|
|
325
|
+
render() {
|
|
326
|
+
if (this.text_edit) return;
|
|
327
|
+
const owner_doc = this.container.ownerDocument;
|
|
328
|
+
const doc = this.editor._internal.doc;
|
|
329
|
+
const svg_text = this.editor.serialize();
|
|
330
|
+
const wrapper = owner_doc.createElement("div");
|
|
331
|
+
wrapper.innerHTML = svg_text;
|
|
332
|
+
const new_svg = wrapper.querySelector("svg");
|
|
333
|
+
if (!(new_svg instanceof SVGSVGElement)) return;
|
|
334
|
+
if (this.svg_root) this.svg_root.replaceWith(new_svg);
|
|
335
|
+
else this.container.insertBefore(new_svg, this.hud_canvas);
|
|
336
|
+
this.svg_root = new_svg;
|
|
337
|
+
this.element_index.clear();
|
|
338
|
+
const ids = doc.all_elements();
|
|
339
|
+
let i = 0;
|
|
340
|
+
const tag_walk = (el) => {
|
|
341
|
+
if (i < ids.length) {
|
|
342
|
+
const id = ids[i++];
|
|
343
|
+
el.setAttribute(ID_ATTR, id);
|
|
344
|
+
this.element_index.set(id, el);
|
|
345
|
+
}
|
|
346
|
+
for (let c = el.firstElementChild; c; c = c.nextElementSibling) if (c instanceof SVGElement) tag_walk(c);
|
|
347
|
+
};
|
|
348
|
+
tag_walk(new_svg);
|
|
349
|
+
}
|
|
350
|
+
sync_canvas_size() {
|
|
351
|
+
const cr = this.container.getBoundingClientRect();
|
|
352
|
+
this.hud.setSize(cr.width, cr.height);
|
|
353
|
+
this.redraw();
|
|
354
|
+
}
|
|
355
|
+
/** Single per-frame draw entry — merges host-fed extras with surface chrome. */
|
|
356
|
+
redraw() {
|
|
357
|
+
this.hud.draw(this.compute_measurement_extra());
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Build the host-fed measurement guide for the current frame, or
|
|
361
|
+
* `undefined` if no guide should be drawn.
|
|
362
|
+
*
|
|
363
|
+
* Master signal: Alt held (read from `surface.modifiers()`). Each
|
|
364
|
+
* additional condition is a derivation, not a separate flag — this keeps
|
|
365
|
+
* a single source of truth and lets future Alt-consumers (constrained
|
|
366
|
+
* resize, axis-lock, …) live next to this one without re-tracking the key.
|
|
367
|
+
*/
|
|
368
|
+
compute_measurement_extra() {
|
|
369
|
+
if (!this.hud.modifiers().alt) return void 0;
|
|
370
|
+
if (this.hud.gesture().kind !== "idle") return void 0;
|
|
371
|
+
const sel = this.editor.state.selection;
|
|
372
|
+
if (sel.length === 0) return void 0;
|
|
373
|
+
const hover = this.hud.hover();
|
|
374
|
+
if (!hover) return void 0;
|
|
375
|
+
if (sel.includes(hover)) return void 0;
|
|
376
|
+
const a_rects = sel.map((id) => this.container_box(id)).filter((r) => r !== null);
|
|
377
|
+
if (a_rects.length === 0) return void 0;
|
|
378
|
+
const b_rect = this.container_box(hover);
|
|
379
|
+
if (!b_rect) return void 0;
|
|
380
|
+
const m = (0, _grida_cmath__measurement.measure)(_grida_cmath.default.rect.union(a_rects), b_rect);
|
|
381
|
+
if (!m) return void 0;
|
|
382
|
+
return (0, _grida_hud.measurementToHUDDraw)(m, this.editor.style.measurement_color);
|
|
383
|
+
}
|
|
384
|
+
sync_surface_selection() {
|
|
385
|
+
const state = this.editor.state;
|
|
386
|
+
if (state.mode === "edit-content") {
|
|
387
|
+
this.hud.setSelection([]);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.hud.setSelection(state.selection);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Return the selection shape for a node. Vector `<line>` nodes return
|
|
394
|
+
* `{ kind: "line", p1, p2 }` so the HUD lays out endpoint knobs; all
|
|
395
|
+
* other nodes return `{ kind: "rect", rect }` using the container-space
|
|
396
|
+
* bounding box.
|
|
397
|
+
*/
|
|
398
|
+
shape_of(id) {
|
|
399
|
+
if (this.tag_of(id) === "line") {
|
|
400
|
+
const line = this.line_endpoints_in_container(id);
|
|
401
|
+
if (line) return {
|
|
402
|
+
kind: "line",
|
|
403
|
+
p1: line.p1,
|
|
404
|
+
p2: line.p2
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const rect = this.container_box(id);
|
|
408
|
+
if (!rect) return null;
|
|
409
|
+
return {
|
|
410
|
+
kind: "rect",
|
|
411
|
+
rect
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Project an SVG `<line>`'s `x1,y1,x2,y2` from its own coordinate space
|
|
416
|
+
* to the container's coordinate space, where the HUD operates.
|
|
417
|
+
*/
|
|
418
|
+
line_endpoints_in_container(id) {
|
|
419
|
+
const el = this.element_index.get(id);
|
|
420
|
+
if (!(el instanceof SVGGraphicsElement)) return null;
|
|
421
|
+
if (typeof el.getScreenCTM !== "function") return null;
|
|
422
|
+
const ctm = el.getScreenCTM();
|
|
423
|
+
if (!ctm || !this.svg_root) return null;
|
|
424
|
+
const x1 = parseFloat(el.getAttribute("x1") ?? "0");
|
|
425
|
+
const y1 = parseFloat(el.getAttribute("y1") ?? "0");
|
|
426
|
+
const x2 = parseFloat(el.getAttribute("x2") ?? "0");
|
|
427
|
+
const y2 = parseFloat(el.getAttribute("y2") ?? "0");
|
|
428
|
+
if (!Number.isFinite(x1) || !Number.isFinite(y1)) return null;
|
|
429
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) return null;
|
|
430
|
+
const project = (px, py) => {
|
|
431
|
+
return [ctm.a * px + ctm.c * py + ctm.e, ctm.b * px + ctm.d * py + ctm.f];
|
|
432
|
+
};
|
|
433
|
+
const cr = this.container.getBoundingClientRect();
|
|
434
|
+
const [s1x, s1y] = project(x1, y1);
|
|
435
|
+
const [s2x, s2y] = project(x2, y2);
|
|
436
|
+
return {
|
|
437
|
+
p1: [s1x - cr.left + this.container.scrollLeft, s1y - cr.top + this.container.scrollTop],
|
|
438
|
+
p2: [s2x - cr.left + this.container.scrollLeft, s2y - cr.top + this.container.scrollTop]
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
container_box(id) {
|
|
442
|
+
const el = this.element_index.get(id);
|
|
443
|
+
if (!el) return null;
|
|
444
|
+
const ge = el;
|
|
445
|
+
if (typeof ge.getBBox !== "function" || typeof ge.getScreenCTM !== "function") return null;
|
|
446
|
+
let bbox;
|
|
447
|
+
try {
|
|
448
|
+
const b = ge.getBBox();
|
|
449
|
+
bbox = {
|
|
450
|
+
x: b.x,
|
|
451
|
+
y: b.y,
|
|
452
|
+
width: b.width,
|
|
453
|
+
height: b.height
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const ctm = ge.getScreenCTM();
|
|
459
|
+
if (!ctm) return null;
|
|
460
|
+
const project = (px, py) => ({
|
|
461
|
+
x: ctm.a * px + ctm.c * py + ctm.e,
|
|
462
|
+
y: ctm.b * px + ctm.d * py + ctm.f
|
|
463
|
+
});
|
|
464
|
+
const corners = [
|
|
465
|
+
project(bbox.x, bbox.y),
|
|
466
|
+
project(bbox.x + bbox.width, bbox.y),
|
|
467
|
+
project(bbox.x + bbox.width, bbox.y + bbox.height),
|
|
468
|
+
project(bbox.x, bbox.y + bbox.height)
|
|
469
|
+
];
|
|
470
|
+
const xs = corners.map((c) => c.x);
|
|
471
|
+
const ys = corners.map((c) => c.y);
|
|
472
|
+
const left = Math.min(...xs);
|
|
473
|
+
const top = Math.min(...ys);
|
|
474
|
+
const right = Math.max(...xs);
|
|
475
|
+
const bottom = Math.max(...ys);
|
|
476
|
+
const cr = this.container.getBoundingClientRect();
|
|
477
|
+
return {
|
|
478
|
+
x: left - cr.left + this.container.scrollLeft,
|
|
479
|
+
y: top - cr.top + this.container.scrollTop,
|
|
480
|
+
width: right - left,
|
|
481
|
+
height: bottom - top
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
screen_delta_in_own_frame(id, dx, dy) {
|
|
485
|
+
const el = this.element_index.get(id);
|
|
486
|
+
if (!el) return {
|
|
487
|
+
x: dx,
|
|
488
|
+
y: dy
|
|
489
|
+
};
|
|
490
|
+
return this.unproject_delta(el, dx, dy);
|
|
491
|
+
}
|
|
492
|
+
screen_delta_in_parent_frame(id, dx, dy) {
|
|
493
|
+
const el = this.element_index.get(id);
|
|
494
|
+
if (!el) return {
|
|
495
|
+
x: dx,
|
|
496
|
+
y: dy
|
|
497
|
+
};
|
|
498
|
+
const parent = el.parentNode ?? el;
|
|
499
|
+
return this.unproject_delta(parent, dx, dy);
|
|
500
|
+
}
|
|
501
|
+
unproject_delta(frame, dx, dy) {
|
|
502
|
+
const ctm = typeof frame.getScreenCTM === "function" ? frame.getScreenCTM() : null;
|
|
503
|
+
if (!ctm || !this.svg_root) return {
|
|
504
|
+
x: dx,
|
|
505
|
+
y: dy
|
|
506
|
+
};
|
|
507
|
+
const inv = ctm.inverse();
|
|
508
|
+
const p0 = this.svg_root.createSVGPoint();
|
|
509
|
+
p0.x = 0;
|
|
510
|
+
p0.y = 0;
|
|
511
|
+
const p1 = this.svg_root.createSVGPoint();
|
|
512
|
+
p1.x = dx;
|
|
513
|
+
p1.y = dy;
|
|
514
|
+
const tp0 = p0.matrixTransform(inv);
|
|
515
|
+
const tp1 = p1.matrixTransform(inv);
|
|
516
|
+
return {
|
|
517
|
+
x: tp1.x - tp0.x,
|
|
518
|
+
y: tp1.y - tp0.y
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
wire_events() {
|
|
522
|
+
const owner_doc = this.container.ownerDocument;
|
|
523
|
+
const win = owner_doc.defaultView ?? window;
|
|
524
|
+
const on = (target, event, handler) => {
|
|
525
|
+
target.addEventListener(event, handler);
|
|
526
|
+
this.teardown.push(() => target.removeEventListener(event, handler));
|
|
527
|
+
};
|
|
528
|
+
on(this.container, "pointerdown", (e) => this.dispatch_pointer(e, "pointer_down"));
|
|
529
|
+
on(win, "pointermove", (e) => this.dispatch_pointer(e, "pointer_move"));
|
|
530
|
+
on(win, "pointerup", (e) => this.dispatch_pointer(e, "pointer_up"));
|
|
531
|
+
on(owner_doc, "keydown", (e) => this.on_keydown(e));
|
|
532
|
+
on(win, "keydown", (e) => {
|
|
533
|
+
if (e.repeat || !IS_MODIFIER_KEY[e.key]) return;
|
|
534
|
+
this.sync_modifiers(e);
|
|
535
|
+
});
|
|
536
|
+
on(win, "keyup", (e) => {
|
|
537
|
+
if (!IS_MODIFIER_KEY[e.key]) return;
|
|
538
|
+
this.sync_modifiers(e);
|
|
539
|
+
});
|
|
540
|
+
on(win, "blur", () => this.sync_modifiers(null));
|
|
541
|
+
on(this.container, "contextmenu", (e) => e.preventDefault());
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Master signal for modifier-driven UX consumers (measurement, future
|
|
545
|
+
* constrained-resize, …). Modifier changes aren't on the pointer-event
|
|
546
|
+
* path, so derived overlays would otherwise wait for the next pointer
|
|
547
|
+
* move; redraw eagerly. `null` means modifiers are forced clear
|
|
548
|
+
* (blur / focus-out).
|
|
549
|
+
*/
|
|
550
|
+
sync_modifiers(e) {
|
|
551
|
+
const next = e ? {
|
|
552
|
+
shift: e.shiftKey,
|
|
553
|
+
alt: e.altKey,
|
|
554
|
+
meta: e.metaKey,
|
|
555
|
+
ctrl: e.ctrlKey
|
|
556
|
+
} : _grida_hud.NO_MODS;
|
|
557
|
+
const prev = this.hud.modifiers();
|
|
558
|
+
if (prev.shift === next.shift && prev.alt === next.alt && prev.meta === next.meta && prev.ctrl === next.ctrl) return;
|
|
559
|
+
const response = this.hud.dispatch({
|
|
560
|
+
kind: "modifiers",
|
|
561
|
+
mods: next
|
|
562
|
+
});
|
|
563
|
+
this.redraw();
|
|
564
|
+
if (response.cursorChanged) this.sync_cursor();
|
|
565
|
+
if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
|
|
566
|
+
}
|
|
567
|
+
dispatch_pointer(e, kind) {
|
|
568
|
+
if (this.text_edit) {
|
|
569
|
+
if (kind === "pointer_down") {
|
|
570
|
+
const el = this.text_edit_target ? this.element_index.get(this.text_edit_target) : null;
|
|
571
|
+
if (el && e.target instanceof Element && (e.target === el || el.contains(e.target))) this.text_edit.pointerDown(e.clientX, e.clientY, e.shiftKey);
|
|
572
|
+
} else if (kind === "pointer_move") this.text_edit.pointerMove(e.clientX, e.clientY);
|
|
573
|
+
else if (kind === "pointer_up") this.text_edit.pointerUp();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const cr = this.container.getBoundingClientRect();
|
|
577
|
+
const x = e.clientX - cr.left;
|
|
578
|
+
const y = e.clientY - cr.top;
|
|
579
|
+
const mods = {
|
|
580
|
+
shift: e.shiftKey,
|
|
581
|
+
alt: e.altKey,
|
|
582
|
+
meta: e.metaKey,
|
|
583
|
+
ctrl: e.ctrlKey
|
|
584
|
+
};
|
|
585
|
+
const button = e.button === 0 ? "primary" : e.button === 2 ? "secondary" : "middle";
|
|
586
|
+
let event;
|
|
587
|
+
if (kind === "pointer_move") event = {
|
|
588
|
+
kind,
|
|
589
|
+
x,
|
|
590
|
+
y,
|
|
591
|
+
mods
|
|
592
|
+
};
|
|
593
|
+
else {
|
|
594
|
+
event = {
|
|
595
|
+
kind,
|
|
596
|
+
x,
|
|
597
|
+
y,
|
|
598
|
+
button,
|
|
599
|
+
mods
|
|
600
|
+
};
|
|
601
|
+
if (kind === "pointer_down") try {
|
|
602
|
+
this.container.setPointerCapture(e.pointerId);
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
const response = this.hud.dispatch(event);
|
|
606
|
+
if (response.needsRedraw) this.redraw();
|
|
607
|
+
if (response.cursorChanged) this.sync_cursor();
|
|
608
|
+
if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
|
|
609
|
+
}
|
|
610
|
+
sync_cursor() {
|
|
611
|
+
const c = this.hud.cursor();
|
|
612
|
+
let css = "default";
|
|
613
|
+
if (typeof c === "string") css = c === "default" ? "default" : c;
|
|
614
|
+
else if (c.kind === "resize") css = `${c.direction}-resize`;
|
|
615
|
+
else if (c.kind === "rotate") css = "crosshair";
|
|
616
|
+
this.container.style.cursor = css;
|
|
617
|
+
}
|
|
618
|
+
on_keydown(e) {
|
|
619
|
+
if (this.text_edit) return;
|
|
620
|
+
if (e.code === "Escape" && this.active_preview) {
|
|
621
|
+
this.active_preview.session.discard();
|
|
622
|
+
this.active_preview = null;
|
|
623
|
+
}
|
|
624
|
+
this.editor.keymap.dispatch(e);
|
|
625
|
+
}
|
|
626
|
+
commit_intent(intent) {
|
|
627
|
+
switch (intent.kind) {
|
|
628
|
+
case "select":
|
|
629
|
+
this.editor.commands.select(intent.ids, { additive: intent.mode !== "replace" });
|
|
630
|
+
return;
|
|
631
|
+
case "deselect_all":
|
|
632
|
+
this.editor.commands.deselect();
|
|
633
|
+
return;
|
|
634
|
+
case "translate":
|
|
635
|
+
this.handle_translate(intent);
|
|
636
|
+
return;
|
|
637
|
+
case "resize":
|
|
638
|
+
this.handle_resize(intent);
|
|
639
|
+
return;
|
|
640
|
+
case "rotate": return;
|
|
641
|
+
case "marquee_select":
|
|
642
|
+
this.handle_marquee(intent);
|
|
643
|
+
return;
|
|
644
|
+
case "set_endpoint":
|
|
645
|
+
this.handle_set_endpoint(intent);
|
|
646
|
+
return;
|
|
647
|
+
case "enter_content_edit":
|
|
648
|
+
this.editor.commands.select(intent.id);
|
|
649
|
+
this.editor.enter_content_edit(intent.id);
|
|
650
|
+
return;
|
|
651
|
+
case "cancel_gesture":
|
|
652
|
+
if (this.active_preview) {
|
|
653
|
+
this.active_preview.session.discard();
|
|
654
|
+
this.active_preview = null;
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
handle_translate(intent) {
|
|
660
|
+
const ids = intent.ids;
|
|
661
|
+
if (ids.length === 0) return;
|
|
662
|
+
const internal = this.editor_internal();
|
|
663
|
+
const doc = internal.doc;
|
|
664
|
+
const emit = internal.emit;
|
|
665
|
+
if (!this.active_preview || this.active_preview.kind !== "translate") {
|
|
666
|
+
if (this.active_preview) this.active_preview.session.discard();
|
|
667
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const id of ids) baselines.set(id, require_paint.capture_translate_baseline(doc, id));
|
|
669
|
+
this.active_preview = {
|
|
670
|
+
kind: "translate",
|
|
671
|
+
ids,
|
|
672
|
+
baselines,
|
|
673
|
+
session: internal.history.preview("move")
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const baselines = this.active_preview.baselines;
|
|
677
|
+
const deltas = /* @__PURE__ */ new Map();
|
|
678
|
+
for (const id of ids) deltas.set(id, this.screen_delta_in_parent_frame(id, intent.dx, intent.dy));
|
|
679
|
+
const apply = () => {
|
|
680
|
+
for (const id of ids) {
|
|
681
|
+
const baseline = baselines.get(id);
|
|
682
|
+
const d = deltas.get(id);
|
|
683
|
+
if (!baseline || !d) continue;
|
|
684
|
+
require_paint.apply_translate(doc, id, baseline, d.x, d.y);
|
|
685
|
+
}
|
|
686
|
+
emit();
|
|
687
|
+
};
|
|
688
|
+
const revert = () => {
|
|
689
|
+
for (const id of ids) {
|
|
690
|
+
const baseline = baselines.get(id);
|
|
691
|
+
if (!baseline) continue;
|
|
692
|
+
require_paint.apply_translate(doc, id, baseline, 0, 0);
|
|
693
|
+
}
|
|
694
|
+
emit();
|
|
695
|
+
};
|
|
696
|
+
this.active_preview.session.set({
|
|
697
|
+
providerId: "svg-editor",
|
|
698
|
+
apply,
|
|
699
|
+
revert
|
|
700
|
+
});
|
|
701
|
+
if (intent.phase === "commit") {
|
|
702
|
+
this.active_preview.session.commit();
|
|
703
|
+
this.active_preview = null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
handle_resize(intent) {
|
|
707
|
+
const id = intent.ids[0];
|
|
708
|
+
if (!id || !require_paint.is_resizable(this.tag_of(id))) return;
|
|
709
|
+
const internal = this.editor_internal();
|
|
710
|
+
const doc = internal.doc;
|
|
711
|
+
const emit = internal.emit;
|
|
712
|
+
if (!this.active_preview || this.active_preview.kind !== "resize" || this.active_preview.id !== id) {
|
|
713
|
+
if (this.active_preview) this.active_preview.session.discard();
|
|
714
|
+
const bbox = this.bbox_local(id) ?? {
|
|
715
|
+
x: 0,
|
|
716
|
+
y: 0,
|
|
717
|
+
width: 0,
|
|
718
|
+
height: 0
|
|
719
|
+
};
|
|
720
|
+
const initial_screen = this.container_box(id) ?? {
|
|
721
|
+
x: 0,
|
|
722
|
+
y: 0,
|
|
723
|
+
width: 0,
|
|
724
|
+
height: 0
|
|
725
|
+
};
|
|
726
|
+
this.active_preview = {
|
|
727
|
+
kind: "resize",
|
|
728
|
+
id,
|
|
729
|
+
direction: intent.anchor,
|
|
730
|
+
baseline: require_paint.capture_resize_baseline(doc, id, bbox),
|
|
731
|
+
initial_screen,
|
|
732
|
+
session: internal.history.preview("resize")
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const baseline = this.active_preview.baseline;
|
|
736
|
+
const dir = this.active_preview.direction;
|
|
737
|
+
const initial = this.active_preview.initial_screen;
|
|
738
|
+
const dx_screen = intent.rect.width - initial.width;
|
|
739
|
+
const dy_screen = intent.rect.height - initial.height;
|
|
740
|
+
const signed_dx = dir === "w" || dir === "nw" || dir === "sw" ? -dx_screen : dx_screen;
|
|
741
|
+
const signed_dy = dir === "n" || dir === "ne" || dir === "nw" ? -dy_screen : dy_screen;
|
|
742
|
+
const d = this.screen_delta_in_own_frame(id, signed_dx, signed_dy);
|
|
743
|
+
const f = require_paint.compute_resize_factors(baseline, dir, d.x, d.y, false);
|
|
744
|
+
const apply = () => {
|
|
745
|
+
require_paint.apply_resize(doc, id, baseline, f.sx, f.sy, f.origin);
|
|
746
|
+
emit();
|
|
747
|
+
};
|
|
748
|
+
const revert = () => {
|
|
749
|
+
require_paint.apply_resize(doc, id, baseline, 1, 1, f.origin);
|
|
750
|
+
emit();
|
|
751
|
+
};
|
|
752
|
+
this.active_preview.session.set({
|
|
753
|
+
providerId: "svg-editor",
|
|
754
|
+
apply,
|
|
755
|
+
revert
|
|
756
|
+
});
|
|
757
|
+
if (intent.phase === "commit") {
|
|
758
|
+
this.active_preview.session.commit();
|
|
759
|
+
this.active_preview = null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Apply a `set_endpoint` intent — moving one endpoint of a vector
|
|
764
|
+
* `<line>` to a new container-space position. Unprojects to the element's
|
|
765
|
+
* own (SVG) coord space and updates the corresponding attribute.
|
|
766
|
+
*/
|
|
767
|
+
handle_set_endpoint(intent) {
|
|
768
|
+
const id = intent.id;
|
|
769
|
+
if (this.tag_of(id) !== "line") return;
|
|
770
|
+
const internal = this.editor_internal();
|
|
771
|
+
const doc = internal.doc;
|
|
772
|
+
const emit = internal.emit;
|
|
773
|
+
if (!this.active_preview || this.active_preview.kind !== "endpoint" || this.active_preview.id !== id || this.active_preview.endpoint !== intent.endpoint) {
|
|
774
|
+
if (this.active_preview) this.active_preview.session.discard();
|
|
775
|
+
const initial = {
|
|
776
|
+
x1: numAttr(doc, id, "x1"),
|
|
777
|
+
y1: numAttr(doc, id, "y1"),
|
|
778
|
+
x2: numAttr(doc, id, "x2"),
|
|
779
|
+
y2: numAttr(doc, id, "y2")
|
|
780
|
+
};
|
|
781
|
+
this.active_preview = {
|
|
782
|
+
kind: "endpoint",
|
|
783
|
+
id,
|
|
784
|
+
endpoint: intent.endpoint,
|
|
785
|
+
initial,
|
|
786
|
+
session: internal.history.preview("set-endpoint")
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const initial = this.active_preview.initial;
|
|
790
|
+
const endpoint = this.active_preview.endpoint;
|
|
791
|
+
const pos_own = this.container_point_in_own_frame(id, intent.pos[0], intent.pos[1]);
|
|
792
|
+
if (!pos_own) return;
|
|
793
|
+
const target_x = pos_own.x;
|
|
794
|
+
const target_y = pos_own.y;
|
|
795
|
+
const apply = () => {
|
|
796
|
+
if (endpoint === "p1") {
|
|
797
|
+
doc.set_attr(id, "x1", String(target_x));
|
|
798
|
+
doc.set_attr(id, "y1", String(target_y));
|
|
799
|
+
} else {
|
|
800
|
+
doc.set_attr(id, "x2", String(target_x));
|
|
801
|
+
doc.set_attr(id, "y2", String(target_y));
|
|
802
|
+
}
|
|
803
|
+
emit();
|
|
804
|
+
};
|
|
805
|
+
const revert = () => {
|
|
806
|
+
doc.set_attr(id, "x1", String(initial.x1));
|
|
807
|
+
doc.set_attr(id, "y1", String(initial.y1));
|
|
808
|
+
doc.set_attr(id, "x2", String(initial.x2));
|
|
809
|
+
doc.set_attr(id, "y2", String(initial.y2));
|
|
810
|
+
emit();
|
|
811
|
+
};
|
|
812
|
+
this.active_preview.session.set({
|
|
813
|
+
providerId: "svg-editor",
|
|
814
|
+
apply,
|
|
815
|
+
revert
|
|
816
|
+
});
|
|
817
|
+
if (intent.phase === "commit") {
|
|
818
|
+
this.active_preview.session.commit();
|
|
819
|
+
this.active_preview = null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Convert a container-space point to the element's own SVG coord space.
|
|
824
|
+
* Inverse of `line_endpoints_in_container`'s projection.
|
|
825
|
+
*/
|
|
826
|
+
container_point_in_own_frame(id, cx, cy) {
|
|
827
|
+
const el = this.element_index.get(id);
|
|
828
|
+
if (!(el instanceof SVGGraphicsElement)) return null;
|
|
829
|
+
if (typeof el.getScreenCTM !== "function") return null;
|
|
830
|
+
const ctm = el.getScreenCTM();
|
|
831
|
+
if (!ctm || !this.svg_root) return null;
|
|
832
|
+
const cr = this.container.getBoundingClientRect();
|
|
833
|
+
const inv = ctm.inverse();
|
|
834
|
+
const p = this.svg_root.createSVGPoint();
|
|
835
|
+
p.x = cx + cr.left - this.container.scrollLeft;
|
|
836
|
+
p.y = cy + cr.top - this.container.scrollTop;
|
|
837
|
+
const t = p.matrixTransform(inv);
|
|
838
|
+
return {
|
|
839
|
+
x: t.x,
|
|
840
|
+
y: t.y
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
handle_marquee(intent) {
|
|
844
|
+
if (intent.phase !== "commit") return;
|
|
845
|
+
const ids = [];
|
|
846
|
+
for (const [id, el] of this.element_index) {
|
|
847
|
+
if (id === this.editor.tree().root) continue;
|
|
848
|
+
const box = this.container_box(id);
|
|
849
|
+
if (!box) continue;
|
|
850
|
+
if (rect_intersects(box, intent.rect)) ids.push(id);
|
|
851
|
+
}
|
|
852
|
+
if (ids.length === 0) {
|
|
853
|
+
if (!intent.additive) this.editor.commands.deselect();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
this.editor.commands.select(ids, { additive: intent.additive });
|
|
857
|
+
}
|
|
858
|
+
enter_content_edit(id) {
|
|
859
|
+
if (this.text_edit) return false;
|
|
860
|
+
const el = this.element_index.get(id);
|
|
861
|
+
if (!(el instanceof SVGTextElement)) return false;
|
|
862
|
+
const doc = this.editor._internal;
|
|
863
|
+
this.text_edit_target = id;
|
|
864
|
+
this.text_edit_original = doc.doc.text_of(id);
|
|
865
|
+
this.text_edit = TEXT_EDIT_PENDING;
|
|
866
|
+
this.editor.commands.set_mode("edit-content");
|
|
867
|
+
this.sync_surface_selection();
|
|
868
|
+
this.redraw();
|
|
869
|
+
const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el);
|
|
870
|
+
const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
|
871
|
+
let settled = false;
|
|
872
|
+
const cleanup_after_commit_or_cancel = () => {
|
|
873
|
+
this.text_edit = null;
|
|
874
|
+
this.text_edit_target = null;
|
|
875
|
+
this.editor.commands.set_mode("select");
|
|
876
|
+
this.render();
|
|
877
|
+
this.sync_surface_selection();
|
|
878
|
+
this.redraw();
|
|
879
|
+
};
|
|
880
|
+
this.text_edit = (0, _grida_text_editor_dom.createTextEditor)({
|
|
881
|
+
container: this.container,
|
|
882
|
+
initialText: this.text_edit_original,
|
|
883
|
+
layout: text_surface,
|
|
884
|
+
surface: text_surface,
|
|
885
|
+
isMac: is_mac,
|
|
886
|
+
ariaLabel: "edit svg text",
|
|
887
|
+
requiresMutationsForCommit: (text) => /\s{2,}|^\s|\s$/.test(text),
|
|
888
|
+
callbacks: {
|
|
889
|
+
onChange: (text) => {
|
|
890
|
+
doc.doc.set_text(id, text);
|
|
891
|
+
},
|
|
892
|
+
onCommit: (final_text) => {
|
|
893
|
+
if (settled) return;
|
|
894
|
+
settled = true;
|
|
895
|
+
doc.doc.set_text(id, this.text_edit_original);
|
|
896
|
+
cleanup_after_commit_or_cancel();
|
|
897
|
+
if (final_text !== this.text_edit_original) this.editor.commands.set_text(final_text);
|
|
898
|
+
},
|
|
899
|
+
onCancel: () => {
|
|
900
|
+
if (settled) return;
|
|
901
|
+
settled = true;
|
|
902
|
+
doc.doc.set_text(id, this.text_edit_original);
|
|
903
|
+
cleanup_after_commit_or_cancel();
|
|
904
|
+
doc.emit();
|
|
905
|
+
},
|
|
906
|
+
onUndoFallthrough: () => {
|
|
907
|
+
this.text_edit?.commit();
|
|
908
|
+
this.editor.commands.undo();
|
|
909
|
+
},
|
|
910
|
+
onRedoFallthrough: () => {
|
|
911
|
+
this.text_edit?.commit();
|
|
912
|
+
this.editor.commands.redo();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
tag_of(id) {
|
|
919
|
+
return this.editor.tree().nodes.get(id)?.tag ?? "";
|
|
920
|
+
}
|
|
921
|
+
bbox_local(id) {
|
|
922
|
+
const el = this.element_index.get(id);
|
|
923
|
+
if (!el) return null;
|
|
924
|
+
const ge = el;
|
|
925
|
+
if (typeof ge.getBBox !== "function") return null;
|
|
926
|
+
try {
|
|
927
|
+
const b = ge.getBBox();
|
|
928
|
+
return {
|
|
929
|
+
x: b.x,
|
|
930
|
+
y: b.y,
|
|
931
|
+
width: b.width,
|
|
932
|
+
height: b.height
|
|
933
|
+
};
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
editor_internal() {
|
|
939
|
+
return this.editor._internal;
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
function numAttr(doc, id, name) {
|
|
943
|
+
const v = doc.get_attr(id, name);
|
|
944
|
+
if (v === null || v === "") return 0;
|
|
945
|
+
const n = parseFloat(v);
|
|
946
|
+
return Number.isFinite(n) ? n : 0;
|
|
947
|
+
}
|
|
948
|
+
function rect_intersects(a, b) {
|
|
949
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
Object.defineProperty(exports, "__toESM", {
|
|
953
|
+
enumerable: true,
|
|
954
|
+
get: function() {
|
|
955
|
+
return __toESM;
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
Object.defineProperty(exports, "attach_dom_surface", {
|
|
959
|
+
enumerable: true,
|
|
960
|
+
get: function() {
|
|
961
|
+
return attach_dom_surface;
|
|
962
|
+
}
|
|
963
|
+
});
|