@grida/svg-editor 1.0.0-alpha.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +365 -384
  2. package/dist/dom-BEjG2bSw.d.mts +320 -0
  3. package/dist/dom-DYD1BHTo.js +6050 -0
  4. package/dist/dom-Y2QR7QSi.d.ts +318 -0
  5. package/dist/dom-tM3Dr1EK.mjs +5971 -0
  6. package/dist/dom.d.mts +3 -2
  7. package/dist/dom.d.ts +3 -2
  8. package/dist/dom.js +13 -1
  9. package/dist/dom.mjs +2 -2
  10. package/dist/editor-CWNtt1vu.mjs +2998 -0
  11. package/dist/editor-CZgwg8BK.d.mts +2537 -0
  12. package/dist/editor-CesShk9o.js +3004 -0
  13. package/dist/editor-kFTYSb_8.d.ts +2537 -0
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +5 -2
  17. package/dist/index.mjs +3 -2
  18. package/dist/model-D0iEDcg3.mjs +5451 -0
  19. package/dist/model-tywvTGZv.js +5640 -0
  20. package/dist/presets.d.mts +17 -2
  21. package/dist/presets.d.ts +17 -2
  22. package/dist/presets.js +20 -15
  23. package/dist/presets.mjs +19 -15
  24. package/dist/react.d.mts +88 -10
  25. package/dist/react.d.ts +88 -10
  26. package/dist/react.js +169 -14
  27. package/dist/react.mjs +159 -16
  28. package/package.json +35 -9
  29. package/dist/dom-CmOu0HvI.mjs +0 -1623
  30. package/dist/dom-Cn-RtjRL.d.ts +0 -48
  31. package/dist/dom-CoVZzFqy.js +0 -1672
  32. package/dist/dom-DJnZhtOd.d.mts +0 -48
  33. package/dist/editor-CjK56cgb.mjs +0 -1823
  34. package/dist/editor-D2l_CDr0.d.ts +0 -818
  35. package/dist/editor-D2zZAyny.js +0 -1835
  36. package/dist/editor-Uu6dZX4y.d.mts +0 -818
  37. package/dist/index-CHiXYO9-.d.ts +0 -1
  38. package/dist/index-ThDLM4Am.d.mts +0 -1
  39. package/dist/paint-Cfiw4g_J.mjs +0 -477
  40. package/dist/paint-dDV-Trt9.js +0 -531
  41. /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
@@ -1,1672 +0,0 @@
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 __exportAll = (all, no_symbols) => {
9
- let target = {};
10
- for (var name in all) __defProp(target, name, {
11
- get: all[name],
12
- enumerable: true
13
- });
14
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
- return target;
16
- };
17
- var __copyProps = (to, from, except, desc) => {
18
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
19
- key = keys[i];
20
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
21
- get: ((k) => from[k]).bind(null, key),
22
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
23
- });
24
- }
25
- return to;
26
- };
27
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
28
- value: mod,
29
- enumerable: true
30
- }) : target, mod));
31
- //#endregion
32
- const require_paint = require("./paint-dDV-Trt9.js");
33
- let _grida_text_editor_dom = require("@grida/text-editor/dom");
34
- let _grida_hud = require("@grida/hud");
35
- let _grida_cmath__measurement = require("@grida/cmath/_measurement");
36
- let _grida_cmath = require("@grida/cmath");
37
- _grida_cmath = __toESM(_grida_cmath);
38
- //#region src/core/camera.ts
39
- /**
40
- * Surface-scoped pan/zoom state.
41
- *
42
- * The public shape leads with the peer convention (`center` / `zoom` /
43
- * `bounds`) and keeps the matrix as an advanced read. Methods mirror
44
- * Figma/Penpot where they overlap.
45
- */
46
- var Camera = class {
47
- constructor(opts) {
48
- this.viewport_w = 0;
49
- this.viewport_h = 0;
50
- this.listeners = /* @__PURE__ */ new Set();
51
- this._constraints = null;
52
- this._transform = opts.initial ?? _grida_cmath.default.transform.identity;
53
- this.resolve_bounds = opts.resolve_bounds;
54
- }
55
- /**
56
- * Current viewport constraint, or `null` for free pan/zoom. Set with
57
- * `camera.constraints = { type: 'cover', bounds: '<root>', padding: 80 }`
58
- * to clamp zoom + pan; assign `null` to clear.
59
- *
60
- * Constraints are applied synchronously inside `set_transform` (and
61
- * `_set_viewport_size`), so every public mutation respects them
62
- * automatically — the host never needs to subscribe-and-clamp itself.
63
- */
64
- get constraints() {
65
- return this._constraints;
66
- }
67
- set constraints(c) {
68
- this._constraints = c;
69
- if (c) this.reenforce();
70
- }
71
- /** Underlying 2D affine. World→screen. */
72
- get transform() {
73
- return this._transform;
74
- }
75
- /** Uniform scale factor. 1 = 100 %. */
76
- get zoom() {
77
- return this._transform[0][0];
78
- }
79
- /** World-space point currently at viewport center. */
80
- get center() {
81
- return this.screen_to_world({
82
- x: this.viewport_w / 2,
83
- y: this.viewport_h / 2
84
- });
85
- }
86
- /** World-space rectangle visible in the viewport. */
87
- get bounds() {
88
- const tl = this.screen_to_world({
89
- x: 0,
90
- y: 0
91
- });
92
- const br = this.screen_to_world({
93
- x: this.viewport_w,
94
- y: this.viewport_h
95
- });
96
- return {
97
- x: tl.x,
98
- y: tl.y,
99
- width: br.x - tl.x,
100
- height: br.y - tl.y
101
- };
102
- }
103
- /** Translate the camera by a screen-space delta. */
104
- pan(delta_screen) {
105
- const t = this._transform;
106
- this.set_transform([[
107
- t[0][0],
108
- t[0][1],
109
- t[0][2] + delta_screen.x
110
- ], [
111
- t[1][0],
112
- t[1][1],
113
- t[1][2] + delta_screen.y
114
- ]]);
115
- }
116
- /**
117
- * Multiply zoom by `factor` keeping `origin_screen` fixed in world space.
118
- * Used by wheel-zoom-at-cursor and pinch-zoom.
119
- */
120
- zoom_at(factor, origin_screen) {
121
- const t = this._transform;
122
- const s2 = t[0][0] * factor;
123
- const tx2 = origin_screen.x * (1 - factor) + factor * t[0][2];
124
- const ty2 = origin_screen.y * (1 - factor) + factor * t[1][2];
125
- this.set_transform([[
126
- s2,
127
- 0,
128
- tx2
129
- ], [
130
- 0,
131
- s2,
132
- ty2
133
- ]]);
134
- }
135
- /** Pan so `c` lands at the viewport center. Zoom unchanged. */
136
- set_center(c) {
137
- const s = this._transform[0][0];
138
- const tx = this.viewport_w / 2 - s * c.x;
139
- const ty = this.viewport_h / 2 - s * c.y;
140
- this.set_transform([[
141
- s,
142
- 0,
143
- tx
144
- ], [
145
- 0,
146
- s,
147
- ty
148
- ]]);
149
- }
150
- /** Set zoom directly; pivot defaults to viewport center. */
151
- set_zoom(z, pivot_screen) {
152
- const current = this._transform[0][0];
153
- if (current === 0) return;
154
- const factor = z / current;
155
- const pivot = pivot_screen ?? {
156
- x: this.viewport_w / 2,
157
- y: this.viewport_h / 2
158
- };
159
- this.zoom_at(factor, pivot);
160
- }
161
- /**
162
- * Replace the entire transform.
163
- *
164
- * Idempotent: when the new transform is element-wise equal to the current
165
- * one, this is a no-op (no notification fires). This is the seam that
166
- * makes external constraint loops (e.g. "subscribe → compute clamped →
167
- * set_transform") terminate: the clamp re-emits the same transform on
168
- * the second pass, set_transform short-circuits, no recursion.
169
- *
170
- * When `camera.constraints` is non-null, the input transform is clamped
171
- * synchronously before being stored — every public mutation respects the
172
- * constraint automatically.
173
- */
174
- set_transform(t) {
175
- const next = this.apply_constraints(t);
176
- if (transform_equal(this._transform, next)) return;
177
- this._transform = next;
178
- this.notify();
179
- }
180
- /** Viewport size in screen pixels. Read by host code computing constraints. */
181
- get viewport_size() {
182
- return {
183
- width: this.viewport_w,
184
- height: this.viewport_h
185
- };
186
- }
187
- /**
188
- * Fit a target into the viewport.
189
- *
190
- * - `"<root>"` — the document root's content bounds (host-resolved).
191
- * - `"<selection>"` — current editor.state.selection's union bounds.
192
- * - `NodeId` — that node's content bounds.
193
- * - `Rect` — an explicit world-space rectangle.
194
- *
195
- * No-ops if the target resolves to `null` (e.g. empty selection) or if
196
- * the viewport size is 0 (no container).
197
- */
198
- fit(target, opts) {
199
- if (this.viewport_w <= 0 || this.viewport_h <= 0) return;
200
- const rect = typeof target === "string" ? this.resolve_bounds(target) : target;
201
- if (!rect || rect.width <= 0 || rect.height <= 0) return;
202
- const margin = opts?.margin ?? 64;
203
- const viewport = {
204
- x: 0,
205
- y: 0,
206
- width: this.viewport_w,
207
- height: this.viewport_h
208
- };
209
- this.set_transform(_grida_cmath.default.ext.viewport.transformToFit(viewport, rect, margin));
210
- }
211
- /** Snap back to identity. */
212
- reset() {
213
- this.set_transform(_grida_cmath.default.transform.identity);
214
- }
215
- /**
216
- * Subscribe to camera changes. Fires on every mutation. Cheap channel —
217
- * does NOT bump `editor.state.version`. Same pattern as
218
- * `editor.subscribe_surface_hover`.
219
- */
220
- subscribe(cb) {
221
- this.listeners.add(cb);
222
- return () => {
223
- this.listeners.delete(cb);
224
- };
225
- }
226
- /** @internal Surface drives this on container resize. */
227
- _set_viewport_size(w, h) {
228
- if (w === this.viewport_w && h === this.viewport_h) return;
229
- this.viewport_w = w;
230
- this.viewport_h = h;
231
- if (this._constraints) {
232
- const next = this.apply_constraints(this._transform);
233
- if (!transform_equal(this._transform, next)) this._transform = next;
234
- }
235
- this.notify();
236
- }
237
- /** Convert a screen-space point to world-space. */
238
- screen_to_world(p) {
239
- const inv = _grida_cmath.default.transform.invert(this._transform);
240
- const [wx, wy] = _grida_cmath.default.vector2.transform([p.x, p.y], inv);
241
- return {
242
- x: wx,
243
- y: wy
244
- };
245
- }
246
- /** Convert a world-space point to screen-space. */
247
- world_to_screen(p) {
248
- const [sx, sy] = _grida_cmath.default.vector2.transform([p.x, p.y], this._transform);
249
- return {
250
- x: sx,
251
- y: sy
252
- };
253
- }
254
- /**
255
- * Apply the current constraint (if any) to a candidate transform.
256
- * Pure: returns the clamped result, never mutates state. Returns the
257
- * input unchanged when constraints are null / bounds are unresolvable /
258
- * viewport is 0.
259
- */
260
- apply_constraints(t) {
261
- if (!this._constraints) return t;
262
- if (this.viewport_w <= 0 || this.viewport_h <= 0) return t;
263
- switch (this._constraints.type) {
264
- case "cover": return clamp_cover(t, this._constraints, this.viewport_w, this.viewport_h, this.resolve_bounds);
265
- }
266
- }
267
- /**
268
- * Re-clamp the stored transform against the current constraint. Called
269
- * from the `constraints` setter; `_set_viewport_size` has its own
270
- * notify-inclusive path.
271
- */
272
- reenforce() {
273
- if (!this._constraints) return;
274
- const next = this.apply_constraints(this._transform);
275
- if (transform_equal(this._transform, next)) return;
276
- this._transform = next;
277
- this.notify();
278
- }
279
- notify() {
280
- for (const cb of this.listeners) cb();
281
- }
282
- };
283
- /**
284
- * Clamp a transform under a `'cover'` constraint:
285
- * - Zoom lower-bounded at fit-with-padding (the slide always fills the
286
- * viewport edge-to-edge).
287
- * - When at min-zoom the slide is locked centered (bounds smaller than
288
- * viewport on the constrained axis is impossible above min_zoom; below
289
- * is impossible because zoom is clamped up).
290
- * - When zoomed in, pan is clamped so the slide always covers the viewport
291
- * (no black bars).
292
- *
293
- * Returns the input transform unchanged when bounds can't be resolved or
294
- * are degenerate.
295
- */
296
- function clamp_cover(t, c, vp_w, vp_h, resolve) {
297
- const bounds = typeof c.bounds === "string" ? resolve(c.bounds) : c.bounds;
298
- if (!bounds || bounds.width <= 0 || bounds.height <= 0) return t;
299
- const padding = c.padding ?? 0;
300
- const eff_w = vp_w - 2 * padding;
301
- const eff_h = vp_h - 2 * padding;
302
- if (eff_w <= 0 || eff_h <= 0) return t;
303
- const min_zoom = Math.min(eff_w / bounds.width, eff_h / bounds.height);
304
- const s = Math.max(t[0][0], min_zoom);
305
- const sw = s * bounds.width;
306
- const sh = s * bounds.height;
307
- const tx = sw > vp_w ? _grida_cmath.default.clamp(t[0][2], vp_w - s * (bounds.x + bounds.width), -s * bounds.x) : (vp_w - sw) / 2 - s * bounds.x;
308
- const ty = sh > vp_h ? _grida_cmath.default.clamp(t[1][2], vp_h - s * (bounds.y + bounds.height), -s * bounds.y) : (vp_h - sh) / 2 - s * bounds.y;
309
- return [[
310
- s,
311
- 0,
312
- tx
313
- ], [
314
- 0,
315
- s,
316
- ty
317
- ]];
318
- }
319
- function transform_equal(a, b) {
320
- return a[0][0] === b[0][0] && a[0][1] === b[0][1] && a[0][2] === b[0][2] && a[1][0] === b[1][0] && a[1][1] === b[1][1] && a[1][2] === b[1][2];
321
- }
322
- //#endregion
323
- //#region src/gestures/gestures.ts
324
- /**
325
- * Sibling to `Keymap`. Owns a list of installed gesture bindings; each
326
- * binding's `install(ctx)` is called eagerly when bound and uninstalled
327
- * on `unbind` or surface detach.
328
- */
329
- var Gestures = class {
330
- constructor(ctx) {
331
- this.ctx = ctx;
332
- this.entries = [];
333
- }
334
- /**
335
- * Install a gesture binding. Returns an unbind function.
336
- * Re-binding the same `id` does NOT replace — both will be active.
337
- * Use `unbind({ id })` first if you want a clean swap.
338
- */
339
- bind(binding) {
340
- const uninstall = binding.install(this.ctx);
341
- const entry = {
342
- binding,
343
- uninstall
344
- };
345
- this.entries.push(entry);
346
- return () => {
347
- const i = this.entries.indexOf(entry);
348
- if (i < 0) return;
349
- this.entries.splice(i, 1);
350
- uninstall();
351
- };
352
- }
353
- /**
354
- * Remove bindings matching the spec. With `{ id }`, all bindings with
355
- * that id are uninstalled. With no spec, this is a no-op (use
356
- * `dispose()` to nuke everything).
357
- */
358
- unbind(spec) {
359
- if (spec.id === void 0) return;
360
- const remaining = [];
361
- for (const entry of this.entries) if (entry.binding.id === spec.id) entry.uninstall();
362
- else remaining.push(entry);
363
- this.entries = remaining;
364
- }
365
- /** All currently installed bindings. Order is registration order. */
366
- bindings() {
367
- return this.entries.map((e) => e.binding);
368
- }
369
- /** @internal Uninstall every binding. Surface calls on detach. */
370
- _dispose() {
371
- for (const entry of this.entries) entry.uninstall();
372
- this.entries = [];
373
- }
374
- };
375
- //#endregion
376
- //#region src/gestures/defaults.ts
377
- /** Default margin for `camera.fit` from keyboard shortcuts. */
378
- const KEYBOARD_FIT_MARGIN = 64;
379
- /** Default zoom step for `Cmd/Ctrl+=` / `Cmd/Ctrl+-`. */
380
- const ZOOM_STEP = 1.2;
381
- /** Per-wheel-unit zoom sensitivity for Cmd/Ctrl+wheel + pinch. */
382
- const WHEEL_ZOOM_SENSITIVITY = .01;
383
- /** Min/max zoom clamps. Generous; hosts that want tighter limits can
384
- * unbind these defaults and bind their own. */
385
- const MIN_ZOOM = .02;
386
- const MAX_ZOOM = 256;
387
- function clamp_zoom(z) {
388
- return _grida_cmath.default.clamp(z, MIN_ZOOM, MAX_ZOOM);
389
- }
390
- /** wheel-pan-zoom: plain wheel = pan, Cmd/Ctrl+wheel + pinch = zoom-at-cursor. */
391
- const WHEEL_PAN_ZOOM = {
392
- id: "wheel-pan-zoom",
393
- install({ container, camera }) {
394
- const on_wheel = (e) => {
395
- e.preventDefault();
396
- if (e.ctrlKey || e.metaKey) {
397
- const factor = 1 - e.deltaY * WHEEL_ZOOM_SENSITIVITY;
398
- const eff = clamp_zoom(camera.zoom * factor) / camera.zoom;
399
- if (eff === 1) return;
400
- const rect = container.getBoundingClientRect();
401
- camera.zoom_at(eff, {
402
- x: e.clientX - rect.left,
403
- y: e.clientY - rect.top
404
- });
405
- } else camera.pan({
406
- x: -e.deltaX,
407
- y: -e.deltaY
408
- });
409
- };
410
- container.addEventListener("wheel", on_wheel, { passive: false });
411
- return () => container.removeEventListener("wheel", on_wheel);
412
- }
413
- };
414
- /**
415
- * Begin a drag-pan from a pointerdown. Attaches `pointermove` / `pointerup`
416
- * listeners scoped to the gesture lifetime, then detaches them on release.
417
- * This is the d3-drag pattern: global listeners only exist while a drag is
418
- * in flight, not for the surface's whole lifetime.
419
- */
420
- function begin_drag_pan(e, container, camera, on_release) {
421
- let last_x = e.clientX;
422
- let last_y = e.clientY;
423
- try {
424
- container.setPointerCapture(e.pointerId);
425
- } catch {}
426
- e.preventDefault();
427
- e.stopPropagation();
428
- const win = container.ownerDocument.defaultView ?? window;
429
- const on_pointermove = (ev) => {
430
- const dx = ev.clientX - last_x;
431
- const dy = ev.clientY - last_y;
432
- last_x = ev.clientX;
433
- last_y = ev.clientY;
434
- camera.pan({
435
- x: dx,
436
- y: dy
437
- });
438
- ev.preventDefault();
439
- ev.stopPropagation();
440
- };
441
- const cleanup = () => {
442
- win.removeEventListener("pointermove", on_pointermove, true);
443
- win.removeEventListener("pointerup", on_pointerup, true);
444
- win.removeEventListener("pointercancel", on_pointerup, true);
445
- on_release?.();
446
- };
447
- const on_pointerup = () => cleanup();
448
- win.addEventListener("pointermove", on_pointermove, true);
449
- win.addEventListener("pointerup", on_pointerup, true);
450
- win.addEventListener("pointercancel", on_pointerup, true);
451
- }
452
- /** The data-driven default set. Order = install order. */
453
- const DEFAULT_GESTURE_BINDINGS = [
454
- WHEEL_PAN_ZOOM,
455
- {
456
- id: "space-drag-pan",
457
- install({ container, camera }) {
458
- let space_held = false;
459
- let prev_cursor = null;
460
- const set_cursor = (next) => {
461
- if (prev_cursor === null) prev_cursor = container.style.cursor;
462
- container.style.cursor = next ?? prev_cursor ?? "";
463
- if (next === null) prev_cursor = null;
464
- };
465
- const on_keydown = (e) => {
466
- if (e.code !== "Space" || e.repeat) return;
467
- if (require_paint.is_text_input_focused()) return;
468
- space_held = true;
469
- set_cursor("grab");
470
- e.preventDefault();
471
- };
472
- const on_keyup = (e) => {
473
- if (e.code !== "Space") return;
474
- space_held = false;
475
- set_cursor(null);
476
- };
477
- const on_pointerdown = (e) => {
478
- if (!space_held || e.button !== 0) return;
479
- set_cursor("grabbing");
480
- begin_drag_pan(e, container, camera, () => set_cursor(space_held ? "grab" : null));
481
- };
482
- const on_blur = () => {
483
- space_held = false;
484
- set_cursor(null);
485
- };
486
- const win = container.ownerDocument.defaultView ?? window;
487
- win.addEventListener("keydown", on_keydown);
488
- win.addEventListener("keyup", on_keyup);
489
- container.addEventListener("pointerdown", on_pointerdown, true);
490
- win.addEventListener("blur", on_blur);
491
- return () => {
492
- win.removeEventListener("keydown", on_keydown);
493
- win.removeEventListener("keyup", on_keyup);
494
- container.removeEventListener("pointerdown", on_pointerdown, true);
495
- win.removeEventListener("blur", on_blur);
496
- if (prev_cursor !== null) container.style.cursor = prev_cursor;
497
- };
498
- }
499
- },
500
- {
501
- id: "middle-mouse-pan",
502
- install({ container, camera }) {
503
- const on_pointerdown = (e) => {
504
- if (e.button !== 1) return;
505
- begin_drag_pan(e, container, camera);
506
- };
507
- const on_auxclick = (e) => {
508
- if (e.button === 1) e.preventDefault();
509
- };
510
- container.addEventListener("pointerdown", on_pointerdown, true);
511
- container.addEventListener("auxclick", on_auxclick);
512
- return () => {
513
- container.removeEventListener("pointerdown", on_pointerdown, true);
514
- container.removeEventListener("auxclick", on_auxclick);
515
- };
516
- }
517
- },
518
- {
519
- id: "keyboard-zoom",
520
- install({ container, camera }) {
521
- const owner_doc = container.ownerDocument;
522
- const on_keydown = (e) => {
523
- const active = owner_doc.activeElement;
524
- if (active && active !== owner_doc.body && !container.contains(active)) return;
525
- if (require_paint.is_text_input_focused()) return;
526
- const mod = e.metaKey || e.ctrlKey;
527
- if (e.shiftKey && !mod && (e.code === "Digit0" || e.code === "Numpad0")) {
528
- camera.reset();
529
- e.preventDefault();
530
- } else if (e.shiftKey && !mod && (e.code === "Digit1" || e.code === "Digit9" || e.code === "Numpad1" || e.code === "Numpad9")) {
531
- camera.fit("<root>", { margin: KEYBOARD_FIT_MARGIN });
532
- e.preventDefault();
533
- } else if (e.shiftKey && !mod && (e.code === "Digit2" || e.code === "Numpad2")) {
534
- camera.fit("<selection>", { margin: KEYBOARD_FIT_MARGIN });
535
- e.preventDefault();
536
- } else if (mod && (e.code === "Equal" || e.code === "NumpadAdd")) {
537
- camera.set_zoom(clamp_zoom(camera.zoom * ZOOM_STEP));
538
- e.preventDefault();
539
- } else if (mod && (e.code === "Minus" || e.code === "NumpadSubtract")) {
540
- camera.set_zoom(clamp_zoom(camera.zoom / ZOOM_STEP));
541
- e.preventDefault();
542
- }
543
- };
544
- owner_doc.addEventListener("keydown", on_keydown);
545
- return () => owner_doc.removeEventListener("keydown", on_keydown);
546
- }
547
- }
548
- ];
549
- /** Install every default binding into the gesture layer. */
550
- function applyDefaultGestures(gestures) {
551
- for (const b of DEFAULT_GESTURE_BINDINGS) gestures.bind(b);
552
- }
553
- //#endregion
554
- //#region src/text-surface.ts
555
- const SVG_NS = "http://www.w3.org/2000/svg";
556
- const XML_NS = "http://www.w3.org/XML/1998/namespace";
557
- var SvgTextSurface = class {
558
- constructor(textEl) {
559
- this.prevXmlSpace = void 0;
560
- this.prevPointerEvents = void 0;
561
- this.last_caret_idx = -1;
562
- this.last_caret_visible = false;
563
- this.last_sel_start = -1;
564
- this.last_sel_end = -1;
565
- this.textEl = textEl;
566
- const ownerDoc = textEl.ownerDocument;
567
- const parent = textEl.parentNode;
568
- if (!parent) throw new Error("text element has no parent");
569
- const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
570
- if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
571
- this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
572
- textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
573
- }
574
- this.prevPointerEvents = textEl.getAttribute("pointer-events");
575
- textEl.setAttribute("pointer-events", "bounding-box");
576
- const selection = ownerDoc.createElementNS(SVG_NS, "rect");
577
- selection.setAttribute("fill", "#2563eb");
578
- selection.setAttribute("fill-opacity", "0.25");
579
- selection.setAttribute("pointer-events", "none");
580
- selection.setAttribute("data-svg-text-edit-selection", "");
581
- selection.style.display = "none";
582
- parent.insertBefore(selection, textEl);
583
- this.selectionRect = selection;
584
- const caret = ownerDoc.createElementNS(SVG_NS, "rect");
585
- caret.setAttribute("fill", "#2563eb");
586
- caret.setAttribute("pointer-events", "none");
587
- caret.setAttribute("data-svg-text-edit-caret", "");
588
- caret.style.display = "none";
589
- parent.insertBefore(caret, textEl.nextSibling);
590
- this.caretRect = caret;
591
- }
592
- setText(text) {
593
- if (this.textEl.textContent !== text) this.textEl.textContent = text;
594
- }
595
- setCaret(index, visible) {
596
- if (index === this.last_caret_idx && visible === this.last_caret_visible) return;
597
- this.last_caret_idx = index;
598
- this.last_caret_visible = visible;
599
- if (!visible) {
600
- this.caretRect.style.display = "none";
601
- return;
602
- }
603
- const m = this.metrics();
604
- const x = this.charX(index);
605
- this.caretRect.setAttribute("x", String(x - .75));
606
- this.caretRect.setAttribute("y", String(m.top));
607
- this.caretRect.setAttribute("width", "1.5");
608
- this.caretRect.setAttribute("height", String(m.height));
609
- this.caretRect.style.display = "block";
610
- }
611
- setSelection(start, end) {
612
- if (start === this.last_sel_start && end === this.last_sel_end) return;
613
- this.last_sel_start = start;
614
- this.last_sel_end = end;
615
- if (start === end) {
616
- this.selectionRect.style.display = "none";
617
- return;
618
- }
619
- const m = this.metrics();
620
- const x1 = this.charX(start);
621
- const x2 = this.charX(end);
622
- this.selectionRect.setAttribute("x", String(Math.min(x1, x2)));
623
- this.selectionRect.setAttribute("y", String(m.top));
624
- this.selectionRect.setAttribute("width", String(Math.abs(x2 - x1)));
625
- this.selectionRect.setAttribute("height", String(m.height));
626
- this.selectionRect.style.display = "block";
627
- }
628
- dispose(keepEditMutations = false) {
629
- this.caretRect.remove();
630
- this.selectionRect.remove();
631
- if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
632
- else this.textEl.setAttributeNS(XML_NS, "xml:space", this.prevXmlSpace);
633
- if (this.prevPointerEvents !== void 0) if (this.prevPointerEvents === null) this.textEl.removeAttribute("pointer-events");
634
- else this.textEl.setAttribute("pointer-events", this.prevPointerEvents);
635
- this.prevXmlSpace = void 0;
636
- this.prevPointerEvents = void 0;
637
- }
638
- positionAtPoint(clientX, clientY) {
639
- const ctm = this.textEl.getScreenCTM();
640
- const svg = this.textEl.ownerSVGElement;
641
- if (!ctm || !svg) return 0;
642
- const pt = svg.createSVGPoint();
643
- pt.x = clientX;
644
- pt.y = clientY;
645
- const local = pt.matrixTransform(ctm.inverse());
646
- return this.localXToCharIndex(local.x);
647
- }
648
- /**
649
- * Single-line `<text>` element: there's no "previous visual line" to move
650
- * to. Cocoa single-line convention: Up/PageUp/line_start → doc start;
651
- * Down/PageDown/line_end → doc end.
652
- */
653
- positionForNavigation(_index, direction) {
654
- const text = this.textEl.textContent ?? "";
655
- switch (direction) {
656
- case "up":
657
- case "page_up":
658
- case "line_start": return 0;
659
- case "down":
660
- case "page_down":
661
- case "line_end": return text.length;
662
- }
663
- }
664
- metrics() {
665
- try {
666
- const b = this.textEl.getBBox();
667
- if (b.height > 0) return {
668
- top: b.y,
669
- height: b.height
670
- };
671
- } catch {}
672
- const fontSize = parseFloat(this.textEl.ownerDocument.defaultView?.getComputedStyle(this.textEl).fontSize ?? "16") || 16;
673
- return {
674
- top: parseFloat(this.textEl.getAttribute("y") ?? "0") - fontSize * .85,
675
- height: fontSize
676
- };
677
- }
678
- charX(i) {
679
- const text = this.textEl.textContent ?? "";
680
- const baseX = parseFloat(this.textEl.getAttribute("x") ?? "0");
681
- if (text.length === 0) return baseX;
682
- if (i <= 0) try {
683
- return this.textEl.getStartPositionOfChar(0).x;
684
- } catch {
685
- return baseX;
686
- }
687
- if (i >= text.length) try {
688
- return this.textEl.getEndPositionOfChar(text.length - 1).x;
689
- } catch {
690
- return baseX;
691
- }
692
- try {
693
- return this.textEl.getStartPositionOfChar(i).x;
694
- } catch {
695
- return baseX;
696
- }
697
- }
698
- localXToCharIndex(localX) {
699
- const text = this.textEl.textContent ?? "";
700
- if (!text) return 0;
701
- for (let i = 0; i < text.length; i++) try {
702
- const ext = this.textEl.getExtentOfChar(i);
703
- if (localX < ext.x + ext.width / 2) return i;
704
- } catch {
705
- break;
706
- }
707
- return text.length;
708
- }
709
- };
710
- //#endregion
711
- //#region src/dom.ts
712
- const ID_ATTR = "data-grida-id";
713
- /** KeyboardEvent.key values for the modifiers the surface tracks. Used to
714
- * short-circuit window-level `keydown`/`keyup` for non-modifier keystrokes. */
715
- const IS_MODIFIER_KEY = {
716
- Shift: true,
717
- Alt: true,
718
- Meta: true,
719
- Control: true
720
- };
721
- /** Sentinel placed in `text_edit` before `createTextEditor` returns, so the
722
- * surface skips render() during the in-flight mount and doesn't yank the
723
- * live `<text>` element out from under the about-to-mount text surface. */
724
- const TEXT_EDIT_PENDING = { __pending: true };
725
- /**
726
- * Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
727
- * whose `detach()` is the inverse — DOM cleared, listeners removed,
728
- * gestures uninstalled.
729
- *
730
- * Usage is one-shot per container: the surface owns the container's children
731
- * for its lifetime, and `detach()` restores it to empty.
732
- */
733
- function attach_dom_surface(editor, options) {
734
- const surface = new DomSurface(editor, options);
735
- const inner = editor.attach(surface);
736
- return {
737
- detach: () => {
738
- surface.detach_gestures();
739
- inner.detach();
740
- },
741
- camera: surface.camera,
742
- gestures: surface.gestures
743
- };
744
- }
745
- var DomSurface = class {
746
- constructor(editor, options) {
747
- this.editor = editor;
748
- this.svg_root = null;
749
- this.teardown = [];
750
- this.element_index = /* @__PURE__ */ new Map();
751
- this.resize_observer = null;
752
- this.active_preview = null;
753
- this.text_edit = null;
754
- this.text_edit_target = null;
755
- this.text_edit_original = "";
756
- this.editor_hover_internal = null;
757
- this.container = options.container;
758
- const container = this.container;
759
- this.fit_on_attach = options.fit === true;
760
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
761
- container.style.userSelect = "none";
762
- container.style.webkitUserSelect = "none";
763
- this.hud_canvas = container.ownerDocument.createElement("canvas");
764
- Object.assign(this.hud_canvas.style, {
765
- position: "absolute",
766
- left: "0",
767
- top: "0",
768
- pointerEvents: "none"
769
- });
770
- container.appendChild(this.hud_canvas);
771
- this.hud = new _grida_hud.Surface(this.hud_canvas, {
772
- pick: (p) => this.hit_test(p[0], p[1]),
773
- shapeOf: (id) => this.shape_of(id),
774
- onIntent: (i) => this.commit_intent(i),
775
- style: { chromeColor: editor.style.chrome_color }
776
- });
777
- this.camera = new Camera({
778
- resolve_bounds: (target) => this.resolve_world_bounds(target),
779
- initial: options.initial_camera
780
- });
781
- this.teardown.push(this.camera.subscribe(() => {
782
- this.apply_camera_transform();
783
- this.redraw();
784
- }));
785
- this.render();
786
- this.sync_canvas_size();
787
- this.sync_surface_selection();
788
- this.redraw();
789
- const win = container.ownerDocument.defaultView ?? window;
790
- const raf = win.requestAnimationFrame(() => {
791
- this.sync_canvas_size();
792
- this.honor_initial_fit();
793
- this.redraw();
794
- });
795
- this.teardown.push(() => win.cancelAnimationFrame(raf));
796
- this.gestures = new Gestures({
797
- container,
798
- svg_root: () => this.svg_root,
799
- hud_canvas: this.hud_canvas,
800
- camera: this.camera,
801
- editor,
802
- handle: { detach: () => {} }
803
- });
804
- if (options.gestures !== false) applyDefaultGestures(this.gestures);
805
- const unsub = editor.subscribe(() => {
806
- this.render();
807
- this.sync_surface_selection();
808
- this.sync_canvas_size();
809
- });
810
- this.teardown.push(unsub);
811
- if (typeof ResizeObserver !== "undefined") {
812
- this.resize_observer = new ResizeObserver(() => this.sync_canvas_size());
813
- this.resize_observer.observe(container);
814
- this.teardown.push(() => this.resize_observer?.disconnect());
815
- } else {
816
- const win = container.ownerDocument.defaultView ?? window;
817
- const fn = () => this.sync_canvas_size();
818
- win.addEventListener("resize", fn);
819
- this.teardown.push(() => win.removeEventListener("resize", fn));
820
- }
821
- this.wire_events();
822
- const internal = editor._internal;
823
- this.editor_hover_internal = internal;
824
- internal.set_content_edit_driver((id) => this.enter_content_edit(id));
825
- this.teardown.push(() => internal.set_content_edit_driver(null));
826
- internal.set_computed_resolver({
827
- computed_property: (id, name) => {
828
- const el = this.element_index.get(id);
829
- if (!el) return null;
830
- const value = getComputedStyle(el).getPropertyValue(name);
831
- return value === "" ? null : value;
832
- },
833
- computed_paint: (id, channel) => {
834
- const el = this.element_index.get(id);
835
- if (!el) return null;
836
- const computed = getComputedStyle(el).getPropertyValue(channel);
837
- if (computed === "") return null;
838
- return {
839
- computed,
840
- resolved_paint: require_paint.parse_paint(computed)
841
- };
842
- }
843
- });
844
- this.teardown.push(() => internal.set_computed_resolver(null));
845
- internal.set_surface_hover_override_driver((id) => {
846
- const response = this.hud.setHoverOverride(id);
847
- if (response.hoverChanged) internal.push_surface_hover(this.hud.hover());
848
- if (response.needsRedraw) this.redraw();
849
- });
850
- this.teardown.push(() => internal.set_surface_hover_override_driver(null));
851
- }
852
- paint(_snapshot) {}
853
- hit_test(x, y) {
854
- const owner_doc = this.container.ownerDocument;
855
- const cr = this.container.getBoundingClientRect();
856
- const target = owner_doc.elementFromPoint(cr.left + x, cr.top + y);
857
- if (!(target instanceof SVGElement)) return null;
858
- const root_id = this.editor.tree().root;
859
- let cur = target;
860
- while (cur instanceof Element) {
861
- const id = cur.getAttribute(ID_ATTR);
862
- if (id) return id === root_id ? null : id;
863
- cur = cur.parentElement;
864
- }
865
- return null;
866
- }
867
- on_input(_listener) {
868
- return () => {};
869
- }
870
- dispose() {
871
- if (this.text_edit) {
872
- this.text_edit.cancel();
873
- this.text_edit = null;
874
- this.text_edit_target = null;
875
- }
876
- this.gestures._dispose();
877
- for (const fn of this.teardown) fn();
878
- this.teardown = [];
879
- this.hud.dispose();
880
- this.hud_canvas.remove();
881
- if (this.svg_root) this.svg_root.remove();
882
- this.svg_root = null;
883
- this.element_index.clear();
884
- this.active_preview = null;
885
- }
886
- /** Public — invoked by the `DomSurfaceHandle` wrapper before `detach()`. */
887
- detach_gestures() {
888
- this.gestures._dispose();
889
- }
890
- render() {
891
- if (this.text_edit) return;
892
- const owner_doc = this.container.ownerDocument;
893
- const doc = this.editor._internal.doc;
894
- const svg_text = this.editor.serialize();
895
- const wrapper = owner_doc.createElement("div");
896
- wrapper.innerHTML = svg_text;
897
- const new_svg = wrapper.querySelector("svg");
898
- if (!(new_svg instanceof SVGSVGElement)) return;
899
- if (this.svg_root) this.svg_root.replaceWith(new_svg);
900
- else this.container.insertBefore(new_svg, this.hud_canvas);
901
- this.svg_root = new_svg;
902
- this.apply_svg_layout();
903
- this.apply_camera_transform();
904
- this.element_index.clear();
905
- const ids = doc.all_elements();
906
- let i = 0;
907
- const tag_walk = (el) => {
908
- if (i < ids.length) {
909
- const id = ids[i++];
910
- el.setAttribute(ID_ATTR, id);
911
- this.element_index.set(id, el);
912
- }
913
- for (let c = el.firstElementChild; c; c = c.nextElementSibling) if (c instanceof SVGElement) tag_walk(c);
914
- };
915
- tag_walk(new_svg);
916
- }
917
- sync_canvas_size() {
918
- const cr = this.container.getBoundingClientRect();
919
- this.hud.setSize(cr.width, cr.height);
920
- this.camera._set_viewport_size(cr.width, cr.height);
921
- this.redraw();
922
- }
923
- /**
924
- * Apply absolute positioning + transform-origin to the SVG so the camera's
925
- * CSS matrix maps SVG-coord (0,0) cleanly to container-screen (tx, ty).
926
- * Called after every render() that may have replaced the root element.
927
- */
928
- apply_svg_layout() {
929
- if (!this.svg_root) return;
930
- const style = this.svg_root.style;
931
- style.position = "absolute";
932
- style.left = "0";
933
- style.top = "0";
934
- style.transformOrigin = "0 0";
935
- }
936
- /**
937
- * Push the current camera transform to the SVG as a CSS `matrix(...)`.
938
- * The HUD canvas stays at identity — selection chrome reads node bounds
939
- * via `getScreenCTM()`, which already includes the CSS transform, so
940
- * chrome aligns automatically and stays 1px sharp at any zoom.
941
- */
942
- apply_camera_transform() {
943
- if (!this.svg_root) return;
944
- const t = this.camera.transform;
945
- this.svg_root.style.transform = `matrix(${t[0][0]}, ${t[1][0]}, ${t[0][1]}, ${t[1][1]}, ${t[0][2]}, ${t[1][2]})`;
946
- }
947
- /** One-shot fit-on-attach. Runs after layout has settled. */
948
- honor_initial_fit() {
949
- if (!this.fit_on_attach) return;
950
- this.fit_on_attach = false;
951
- this.camera.fit("<root>");
952
- }
953
- /**
954
- * BoundsResolver for `Camera.fit(target)`. The Camera class handles Rect
955
- * passthrough itself; this resolver only sees string targets — sentinels
956
- * ("<root>", "<selection>") and NodeIds.
957
- */
958
- resolve_world_bounds(target) {
959
- if (target === "<root>") return this.root_world_bounds();
960
- if (target === "<selection>") {
961
- const sel = this.editor.state.selection;
962
- if (sel.length === 0) return null;
963
- const rects = [];
964
- for (const id of sel) {
965
- const r = this.node_world_bounds(id);
966
- if (r) rects.push(r);
967
- }
968
- if (rects.length === 0) return null;
969
- return _grida_cmath.default.rect.union(rects);
970
- }
971
- return this.node_world_bounds(target);
972
- }
973
- /**
974
- * World-space bounds of the root document. Prefer `viewBox` (the SVG's
975
- * declared world rect), fall back to `width`/`height` attrs, then the
976
- * SVG root's `getBBox()` as a last resort.
977
- */
978
- root_world_bounds() {
979
- const root_id = this.editor.tree().root;
980
- const doc = this.editor.document;
981
- const view_box = doc.get_attr(root_id, "viewBox");
982
- if (view_box) {
983
- const parts = view_box.trim().split(/[\s,]+/).map(Number);
984
- if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) return {
985
- x: parts[0],
986
- y: parts[1],
987
- width: parts[2],
988
- height: parts[3]
989
- };
990
- }
991
- const w = parseFloat(doc.get_attr(root_id, "width") ?? "");
992
- const h = parseFloat(doc.get_attr(root_id, "height") ?? "");
993
- if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) return {
994
- x: 0,
995
- y: 0,
996
- width: w,
997
- height: h
998
- };
999
- if (this.svg_root) try {
1000
- const b = this.svg_root.getBBox();
1001
- if (b.width > 0 && b.height > 0) return {
1002
- x: b.x,
1003
- y: b.y,
1004
- width: b.width,
1005
- height: b.height
1006
- };
1007
- } catch {}
1008
- return null;
1009
- }
1010
- /**
1011
- * World-space bounds of a single node. Uses `getBBox()` (element-local)
1012
- * and projects through `getCTM()` (local → nearest viewport = SVG root).
1013
- */
1014
- node_world_bounds(id) {
1015
- const el = this.element_index.get(id);
1016
- if (!el) return null;
1017
- const ge = el;
1018
- if (typeof ge.getBBox !== "function" || typeof ge.getCTM !== "function") return null;
1019
- let bbox;
1020
- try {
1021
- const b = ge.getBBox();
1022
- bbox = {
1023
- x: b.x,
1024
- y: b.y,
1025
- width: b.width,
1026
- height: b.height
1027
- };
1028
- } catch {
1029
- return null;
1030
- }
1031
- const ctm = ge.getCTM();
1032
- if (!ctm) return bbox;
1033
- const project = (px, py) => ({
1034
- x: ctm.a * px + ctm.c * py + ctm.e,
1035
- y: ctm.b * px + ctm.d * py + ctm.f
1036
- });
1037
- const corners = [
1038
- project(bbox.x, bbox.y),
1039
- project(bbox.x + bbox.width, bbox.y),
1040
- project(bbox.x + bbox.width, bbox.y + bbox.height),
1041
- project(bbox.x, bbox.y + bbox.height)
1042
- ];
1043
- const xs = corners.map((c) => c.x);
1044
- const ys = corners.map((c) => c.y);
1045
- const left = Math.min(...xs);
1046
- const top = Math.min(...ys);
1047
- const right = Math.max(...xs);
1048
- const bottom = Math.max(...ys);
1049
- return {
1050
- x: left,
1051
- y: top,
1052
- width: right - left,
1053
- height: bottom - top
1054
- };
1055
- }
1056
- /** Single per-frame draw entry — merges host-fed extras with surface chrome. */
1057
- redraw() {
1058
- this.hud.draw(this.compute_measurement_extra());
1059
- }
1060
- /**
1061
- * Build the host-fed measurement guide for the current frame, or
1062
- * `undefined` if no guide should be drawn.
1063
- *
1064
- * Master signal: Alt held (read from `surface.modifiers()`). Each
1065
- * additional condition is a derivation, not a separate flag — this keeps
1066
- * a single source of truth and lets future Alt-consumers (constrained
1067
- * resize, axis-lock, …) live next to this one without re-tracking the key.
1068
- */
1069
- compute_measurement_extra() {
1070
- if (!this.hud.modifiers().alt) return void 0;
1071
- if (this.hud.gesture().kind !== "idle") return void 0;
1072
- const sel = this.editor.state.selection;
1073
- if (sel.length === 0) return void 0;
1074
- const hover = this.hud.hover();
1075
- if (!hover) return void 0;
1076
- if (sel.includes(hover)) return void 0;
1077
- const a_rects = sel.map((id) => this.container_box(id)).filter((r) => r !== null);
1078
- if (a_rects.length === 0) return void 0;
1079
- const b_rect = this.container_box(hover);
1080
- if (!b_rect) return void 0;
1081
- const m = (0, _grida_cmath__measurement.measure)(_grida_cmath.default.rect.union(a_rects), b_rect);
1082
- if (!m) return void 0;
1083
- return (0, _grida_hud.measurementToHUDDraw)(m, this.editor.style.measurement_color);
1084
- }
1085
- sync_surface_selection() {
1086
- const state = this.editor.state;
1087
- if (state.mode === "edit-content") {
1088
- this.hud.setSelection([]);
1089
- return;
1090
- }
1091
- this.hud.setSelection(state.selection);
1092
- }
1093
- /**
1094
- * Return the selection shape for a node. Vector `<line>` nodes return
1095
- * `{ kind: "line", p1, p2 }` so the HUD lays out endpoint knobs; all
1096
- * other nodes return `{ kind: "rect", rect }` using the container-space
1097
- * bounding box.
1098
- */
1099
- shape_of(id) {
1100
- if (this.tag_of(id) === "line") {
1101
- const line = this.line_endpoints_in_container(id);
1102
- if (line) return {
1103
- kind: "line",
1104
- p1: line.p1,
1105
- p2: line.p2
1106
- };
1107
- }
1108
- const rect = this.container_box(id);
1109
- if (!rect) return null;
1110
- return {
1111
- kind: "rect",
1112
- rect
1113
- };
1114
- }
1115
- /**
1116
- * Project an SVG `<line>`'s `x1,y1,x2,y2` from its own coordinate space
1117
- * to the container's coordinate space, where the HUD operates.
1118
- */
1119
- line_endpoints_in_container(id) {
1120
- const el = this.element_index.get(id);
1121
- if (!(el instanceof SVGGraphicsElement)) return null;
1122
- if (typeof el.getScreenCTM !== "function") return null;
1123
- const ctm = el.getScreenCTM();
1124
- if (!ctm || !this.svg_root) return null;
1125
- const x1 = parseFloat(el.getAttribute("x1") ?? "0");
1126
- const y1 = parseFloat(el.getAttribute("y1") ?? "0");
1127
- const x2 = parseFloat(el.getAttribute("x2") ?? "0");
1128
- const y2 = parseFloat(el.getAttribute("y2") ?? "0");
1129
- if (!Number.isFinite(x1) || !Number.isFinite(y1)) return null;
1130
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) return null;
1131
- const project = (px, py) => {
1132
- return [ctm.a * px + ctm.c * py + ctm.e, ctm.b * px + ctm.d * py + ctm.f];
1133
- };
1134
- const cr = this.container.getBoundingClientRect();
1135
- const [s1x, s1y] = project(x1, y1);
1136
- const [s2x, s2y] = project(x2, y2);
1137
- return {
1138
- p1: [s1x - cr.left + this.container.scrollLeft, s1y - cr.top + this.container.scrollTop],
1139
- p2: [s2x - cr.left + this.container.scrollLeft, s2y - cr.top + this.container.scrollTop]
1140
- };
1141
- }
1142
- container_box(id) {
1143
- const el = this.element_index.get(id);
1144
- if (!el) return null;
1145
- const ge = el;
1146
- if (typeof ge.getBBox !== "function" || typeof ge.getScreenCTM !== "function") return null;
1147
- let bbox;
1148
- try {
1149
- const b = ge.getBBox();
1150
- bbox = {
1151
- x: b.x,
1152
- y: b.y,
1153
- width: b.width,
1154
- height: b.height
1155
- };
1156
- } catch {
1157
- return null;
1158
- }
1159
- const ctm = ge.getScreenCTM();
1160
- if (!ctm) return null;
1161
- const project = (px, py) => ({
1162
- x: ctm.a * px + ctm.c * py + ctm.e,
1163
- y: ctm.b * px + ctm.d * py + ctm.f
1164
- });
1165
- const corners = [
1166
- project(bbox.x, bbox.y),
1167
- project(bbox.x + bbox.width, bbox.y),
1168
- project(bbox.x + bbox.width, bbox.y + bbox.height),
1169
- project(bbox.x, bbox.y + bbox.height)
1170
- ];
1171
- const xs = corners.map((c) => c.x);
1172
- const ys = corners.map((c) => c.y);
1173
- const left = Math.min(...xs);
1174
- const top = Math.min(...ys);
1175
- const right = Math.max(...xs);
1176
- const bottom = Math.max(...ys);
1177
- const cr = this.container.getBoundingClientRect();
1178
- return {
1179
- x: left - cr.left + this.container.scrollLeft,
1180
- y: top - cr.top + this.container.scrollTop,
1181
- width: right - left,
1182
- height: bottom - top
1183
- };
1184
- }
1185
- screen_delta_in_own_frame(id, dx, dy) {
1186
- const el = this.element_index.get(id);
1187
- if (!el) return {
1188
- x: dx,
1189
- y: dy
1190
- };
1191
- return this.unproject_delta(el, dx, dy);
1192
- }
1193
- screen_delta_in_parent_frame(id, dx, dy) {
1194
- const el = this.element_index.get(id);
1195
- if (!el) return {
1196
- x: dx,
1197
- y: dy
1198
- };
1199
- const parent = el.parentNode ?? el;
1200
- return this.unproject_delta(parent, dx, dy);
1201
- }
1202
- unproject_delta(frame, dx, dy) {
1203
- const ctm = typeof frame.getScreenCTM === "function" ? frame.getScreenCTM() : null;
1204
- if (!ctm || !this.svg_root) return {
1205
- x: dx,
1206
- y: dy
1207
- };
1208
- const inv = ctm.inverse();
1209
- const p0 = this.svg_root.createSVGPoint();
1210
- p0.x = 0;
1211
- p0.y = 0;
1212
- const p1 = this.svg_root.createSVGPoint();
1213
- p1.x = dx;
1214
- p1.y = dy;
1215
- const tp0 = p0.matrixTransform(inv);
1216
- const tp1 = p1.matrixTransform(inv);
1217
- return {
1218
- x: tp1.x - tp0.x,
1219
- y: tp1.y - tp0.y
1220
- };
1221
- }
1222
- wire_events() {
1223
- const owner_doc = this.container.ownerDocument;
1224
- const win = owner_doc.defaultView ?? window;
1225
- const on = (target, event, handler) => {
1226
- target.addEventListener(event, handler);
1227
- this.teardown.push(() => target.removeEventListener(event, handler));
1228
- };
1229
- on(this.container, "pointerdown", (e) => this.dispatch_pointer(e, "pointer_down"));
1230
- on(win, "pointermove", (e) => this.dispatch_pointer(e, "pointer_move"));
1231
- on(win, "pointerup", (e) => this.dispatch_pointer(e, "pointer_up"));
1232
- on(owner_doc, "keydown", (e) => this.on_keydown(e));
1233
- on(win, "keydown", (e) => {
1234
- if (e.repeat || !IS_MODIFIER_KEY[e.key]) return;
1235
- this.sync_modifiers(e);
1236
- });
1237
- on(win, "keyup", (e) => {
1238
- if (!IS_MODIFIER_KEY[e.key]) return;
1239
- this.sync_modifiers(e);
1240
- });
1241
- on(win, "blur", () => this.sync_modifiers(null));
1242
- on(this.container, "contextmenu", (e) => e.preventDefault());
1243
- }
1244
- /**
1245
- * Master signal for modifier-driven UX consumers (measurement, future
1246
- * constrained-resize, …). Modifier changes aren't on the pointer-event
1247
- * path, so derived overlays would otherwise wait for the next pointer
1248
- * move; redraw eagerly. `null` means modifiers are forced clear
1249
- * (blur / focus-out).
1250
- */
1251
- sync_modifiers(e) {
1252
- const next = e ? {
1253
- shift: e.shiftKey,
1254
- alt: e.altKey,
1255
- meta: e.metaKey,
1256
- ctrl: e.ctrlKey
1257
- } : _grida_hud.NO_MODS;
1258
- const prev = this.hud.modifiers();
1259
- if (prev.shift === next.shift && prev.alt === next.alt && prev.meta === next.meta && prev.ctrl === next.ctrl) return;
1260
- const response = this.hud.dispatch({
1261
- kind: "modifiers",
1262
- mods: next
1263
- });
1264
- this.redraw();
1265
- if (response.cursorChanged) this.sync_cursor();
1266
- if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
1267
- }
1268
- dispatch_pointer(e, kind) {
1269
- if (this.text_edit) {
1270
- if (kind === "pointer_down") {
1271
- e.preventDefault();
1272
- const el = this.text_edit_target ? this.element_index.get(this.text_edit_target) : null;
1273
- if (el && e.target instanceof Element && (e.target === el || el.contains(e.target))) this.text_edit.pointerDown(e.clientX, e.clientY, e.shiftKey);
1274
- else this.text_edit.commit();
1275
- } else if (kind === "pointer_move") this.text_edit.pointerMove(e.clientX, e.clientY);
1276
- else if (kind === "pointer_up") this.text_edit.pointerUp();
1277
- return;
1278
- }
1279
- const cr = this.container.getBoundingClientRect();
1280
- const x = e.clientX - cr.left;
1281
- const y = e.clientY - cr.top;
1282
- const mods = {
1283
- shift: e.shiftKey,
1284
- alt: e.altKey,
1285
- meta: e.metaKey,
1286
- ctrl: e.ctrlKey
1287
- };
1288
- const button = e.button === 0 ? "primary" : e.button === 2 ? "secondary" : "middle";
1289
- let event;
1290
- if (kind === "pointer_move") event = {
1291
- kind,
1292
- x,
1293
- y,
1294
- mods
1295
- };
1296
- else {
1297
- event = {
1298
- kind,
1299
- x,
1300
- y,
1301
- button,
1302
- mods
1303
- };
1304
- if (kind === "pointer_down") try {
1305
- this.container.setPointerCapture(e.pointerId);
1306
- } catch {}
1307
- }
1308
- const response = this.hud.dispatch(event);
1309
- if (response.needsRedraw) this.redraw();
1310
- if (response.cursorChanged) this.sync_cursor();
1311
- if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
1312
- }
1313
- sync_cursor() {
1314
- const c = this.hud.cursor();
1315
- let css = "default";
1316
- if (typeof c === "string") css = c === "default" ? "default" : c;
1317
- else if (c.kind === "resize") css = `${c.direction}-resize`;
1318
- else if (c.kind === "rotate") css = "crosshair";
1319
- this.container.style.cursor = css;
1320
- }
1321
- on_keydown(e) {
1322
- if (this.text_edit) return;
1323
- if (e.code === "Escape" && this.active_preview) {
1324
- this.active_preview.session.discard();
1325
- this.active_preview = null;
1326
- }
1327
- this.editor.keymap.dispatch(e);
1328
- }
1329
- commit_intent(intent) {
1330
- switch (intent.kind) {
1331
- case "select":
1332
- this.editor.commands.select(intent.ids, { additive: intent.mode !== "replace" });
1333
- return;
1334
- case "deselect_all":
1335
- this.editor.commands.deselect();
1336
- return;
1337
- case "translate":
1338
- this.handle_translate(intent);
1339
- return;
1340
- case "resize":
1341
- this.handle_resize(intent);
1342
- return;
1343
- case "rotate": return;
1344
- case "marquee_select":
1345
- this.handle_marquee(intent);
1346
- return;
1347
- case "set_endpoint":
1348
- this.handle_set_endpoint(intent);
1349
- return;
1350
- case "enter_content_edit":
1351
- this.editor.commands.select(intent.id);
1352
- this.editor.enter_content_edit(intent.id);
1353
- return;
1354
- case "cancel_gesture":
1355
- if (this.active_preview) {
1356
- this.active_preview.session.discard();
1357
- this.active_preview = null;
1358
- }
1359
- return;
1360
- }
1361
- }
1362
- handle_translate(intent) {
1363
- const ids = intent.ids;
1364
- if (ids.length === 0) return;
1365
- const internal = this.editor_internal();
1366
- const doc = internal.doc;
1367
- const emit = internal.emit;
1368
- if (!this.active_preview || this.active_preview.kind !== "translate") {
1369
- if (this.active_preview) this.active_preview.session.discard();
1370
- const baselines = /* @__PURE__ */ new Map();
1371
- for (const id of ids) baselines.set(id, require_paint.capture_translate_baseline(doc, id));
1372
- this.active_preview = {
1373
- kind: "translate",
1374
- ids,
1375
- baselines,
1376
- session: internal.history.preview("move")
1377
- };
1378
- }
1379
- const baselines = this.active_preview.baselines;
1380
- const deltas = /* @__PURE__ */ new Map();
1381
- for (const id of ids) deltas.set(id, this.screen_delta_in_parent_frame(id, intent.dx, intent.dy));
1382
- const apply = () => {
1383
- for (const id of ids) {
1384
- const baseline = baselines.get(id);
1385
- const d = deltas.get(id);
1386
- if (!baseline || !d) continue;
1387
- require_paint.apply_translate(doc, id, baseline, d.x, d.y);
1388
- }
1389
- emit();
1390
- };
1391
- const revert = () => {
1392
- for (const id of ids) {
1393
- const baseline = baselines.get(id);
1394
- if (!baseline) continue;
1395
- require_paint.apply_translate(doc, id, baseline, 0, 0);
1396
- }
1397
- emit();
1398
- };
1399
- this.active_preview.session.set({
1400
- providerId: "svg-editor",
1401
- apply,
1402
- revert
1403
- });
1404
- if (intent.phase === "commit") {
1405
- this.active_preview.session.commit();
1406
- this.active_preview = null;
1407
- }
1408
- }
1409
- handle_resize(intent) {
1410
- const id = intent.ids[0];
1411
- if (!id || !require_paint.is_resizable(this.tag_of(id))) return;
1412
- const internal = this.editor_internal();
1413
- const doc = internal.doc;
1414
- const emit = internal.emit;
1415
- if (!this.active_preview || this.active_preview.kind !== "resize" || this.active_preview.id !== id) {
1416
- if (this.active_preview) this.active_preview.session.discard();
1417
- const bbox = this.bbox_local(id) ?? {
1418
- x: 0,
1419
- y: 0,
1420
- width: 0,
1421
- height: 0
1422
- };
1423
- const initial_screen = this.container_box(id) ?? {
1424
- x: 0,
1425
- y: 0,
1426
- width: 0,
1427
- height: 0
1428
- };
1429
- this.active_preview = {
1430
- kind: "resize",
1431
- id,
1432
- direction: intent.anchor,
1433
- baseline: require_paint.capture_resize_baseline(doc, id, bbox),
1434
- initial_screen,
1435
- session: internal.history.preview("resize")
1436
- };
1437
- }
1438
- const baseline = this.active_preview.baseline;
1439
- const dir = this.active_preview.direction;
1440
- const initial = this.active_preview.initial_screen;
1441
- const dx_screen = intent.rect.width - initial.width;
1442
- const dy_screen = intent.rect.height - initial.height;
1443
- const signed_dx = dir === "w" || dir === "nw" || dir === "sw" ? -dx_screen : dx_screen;
1444
- const signed_dy = dir === "n" || dir === "ne" || dir === "nw" ? -dy_screen : dy_screen;
1445
- const d = this.screen_delta_in_own_frame(id, signed_dx, signed_dy);
1446
- const f = require_paint.compute_resize_factors(baseline, dir, d.x, d.y, false);
1447
- const apply = () => {
1448
- require_paint.apply_resize(doc, id, baseline, f.sx, f.sy, f.origin);
1449
- emit();
1450
- };
1451
- const revert = () => {
1452
- require_paint.apply_resize(doc, id, baseline, 1, 1, f.origin);
1453
- emit();
1454
- };
1455
- this.active_preview.session.set({
1456
- providerId: "svg-editor",
1457
- apply,
1458
- revert
1459
- });
1460
- if (intent.phase === "commit") {
1461
- this.active_preview.session.commit();
1462
- this.active_preview = null;
1463
- }
1464
- }
1465
- /**
1466
- * Apply a `set_endpoint` intent — moving one endpoint of a vector
1467
- * `<line>` to a new container-space position. Unprojects to the element's
1468
- * own (SVG) coord space and updates the corresponding attribute.
1469
- */
1470
- handle_set_endpoint(intent) {
1471
- const id = intent.id;
1472
- if (this.tag_of(id) !== "line") return;
1473
- const internal = this.editor_internal();
1474
- const doc = internal.doc;
1475
- const emit = internal.emit;
1476
- if (!this.active_preview || this.active_preview.kind !== "endpoint" || this.active_preview.id !== id || this.active_preview.endpoint !== intent.endpoint) {
1477
- if (this.active_preview) this.active_preview.session.discard();
1478
- const initial = {
1479
- x1: numAttr(doc, id, "x1"),
1480
- y1: numAttr(doc, id, "y1"),
1481
- x2: numAttr(doc, id, "x2"),
1482
- y2: numAttr(doc, id, "y2")
1483
- };
1484
- this.active_preview = {
1485
- kind: "endpoint",
1486
- id,
1487
- endpoint: intent.endpoint,
1488
- initial,
1489
- session: internal.history.preview("set-endpoint")
1490
- };
1491
- }
1492
- const initial = this.active_preview.initial;
1493
- const endpoint = this.active_preview.endpoint;
1494
- const pos_own = this.container_point_in_own_frame(id, intent.pos[0], intent.pos[1]);
1495
- if (!pos_own) return;
1496
- const target_x = pos_own.x;
1497
- const target_y = pos_own.y;
1498
- const apply = () => {
1499
- if (endpoint === "p1") {
1500
- doc.set_attr(id, "x1", String(target_x));
1501
- doc.set_attr(id, "y1", String(target_y));
1502
- } else {
1503
- doc.set_attr(id, "x2", String(target_x));
1504
- doc.set_attr(id, "y2", String(target_y));
1505
- }
1506
- emit();
1507
- };
1508
- const revert = () => {
1509
- doc.set_attr(id, "x1", String(initial.x1));
1510
- doc.set_attr(id, "y1", String(initial.y1));
1511
- doc.set_attr(id, "x2", String(initial.x2));
1512
- doc.set_attr(id, "y2", String(initial.y2));
1513
- emit();
1514
- };
1515
- this.active_preview.session.set({
1516
- providerId: "svg-editor",
1517
- apply,
1518
- revert
1519
- });
1520
- if (intent.phase === "commit") {
1521
- this.active_preview.session.commit();
1522
- this.active_preview = null;
1523
- }
1524
- }
1525
- /**
1526
- * Convert a container-space point to the element's own SVG coord space.
1527
- * Inverse of `line_endpoints_in_container`'s projection.
1528
- */
1529
- container_point_in_own_frame(id, cx, cy) {
1530
- const el = this.element_index.get(id);
1531
- if (!(el instanceof SVGGraphicsElement)) return null;
1532
- if (typeof el.getScreenCTM !== "function") return null;
1533
- const ctm = el.getScreenCTM();
1534
- if (!ctm || !this.svg_root) return null;
1535
- const cr = this.container.getBoundingClientRect();
1536
- const inv = ctm.inverse();
1537
- const p = this.svg_root.createSVGPoint();
1538
- p.x = cx + cr.left - this.container.scrollLeft;
1539
- p.y = cy + cr.top - this.container.scrollTop;
1540
- const t = p.matrixTransform(inv);
1541
- return {
1542
- x: t.x,
1543
- y: t.y
1544
- };
1545
- }
1546
- handle_marquee(intent) {
1547
- if (intent.phase !== "commit") return;
1548
- const ids = [];
1549
- for (const [id, el] of this.element_index) {
1550
- if (id === this.editor.tree().root) continue;
1551
- const box = this.container_box(id);
1552
- if (!box) continue;
1553
- if (rect_intersects(box, intent.rect)) ids.push(id);
1554
- }
1555
- if (ids.length === 0) {
1556
- if (!intent.additive) this.editor.commands.deselect();
1557
- return;
1558
- }
1559
- this.editor.commands.select(ids, { additive: intent.additive });
1560
- }
1561
- enter_content_edit(id) {
1562
- if (this.text_edit) return false;
1563
- const el = this.element_index.get(id);
1564
- if (!(el instanceof SVGTextElement)) return false;
1565
- const doc = this.editor._internal;
1566
- this.text_edit_target = id;
1567
- this.text_edit_original = doc.doc.text_of(id);
1568
- this.text_edit = TEXT_EDIT_PENDING;
1569
- this.editor.commands.set_mode("edit-content");
1570
- this.sync_surface_selection();
1571
- this.redraw();
1572
- const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el);
1573
- const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
1574
- let settled = false;
1575
- const cleanup_after_commit_or_cancel = () => {
1576
- this.text_edit = null;
1577
- this.text_edit_target = null;
1578
- this.editor.commands.set_mode("select");
1579
- this.render();
1580
- this.sync_surface_selection();
1581
- this.redraw();
1582
- };
1583
- this.text_edit = (0, _grida_text_editor_dom.createTextEditor)({
1584
- container: this.container,
1585
- initialText: this.text_edit_original,
1586
- layout: text_surface,
1587
- surface: text_surface,
1588
- isMac: is_mac,
1589
- ariaLabel: "edit svg text",
1590
- requiresMutationsForCommit: (text) => /\s{2,}|^\s|\s$/.test(text),
1591
- callbacks: {
1592
- onChange: (text) => {
1593
- doc.doc.set_text(id, text);
1594
- },
1595
- onCommit: (final_text) => {
1596
- if (settled) return;
1597
- settled = true;
1598
- doc.doc.set_text(id, this.text_edit_original);
1599
- cleanup_after_commit_or_cancel();
1600
- if (final_text !== this.text_edit_original) this.editor.commands.set_text(final_text);
1601
- },
1602
- onCancel: () => {
1603
- if (settled) return;
1604
- settled = true;
1605
- doc.doc.set_text(id, this.text_edit_original);
1606
- cleanup_after_commit_or_cancel();
1607
- doc.emit();
1608
- },
1609
- onUndoFallthrough: () => {
1610
- this.text_edit?.commit();
1611
- this.editor.commands.undo();
1612
- },
1613
- onRedoFallthrough: () => {
1614
- this.text_edit?.commit();
1615
- this.editor.commands.redo();
1616
- }
1617
- }
1618
- });
1619
- return true;
1620
- }
1621
- tag_of(id) {
1622
- return this.editor.tree().nodes.get(id)?.tag ?? "";
1623
- }
1624
- bbox_local(id) {
1625
- const el = this.element_index.get(id);
1626
- if (!el) return null;
1627
- const ge = el;
1628
- if (typeof ge.getBBox !== "function") return null;
1629
- try {
1630
- const b = ge.getBBox();
1631
- return {
1632
- x: b.x,
1633
- y: b.y,
1634
- width: b.width,
1635
- height: b.height
1636
- };
1637
- } catch {
1638
- return null;
1639
- }
1640
- }
1641
- editor_internal() {
1642
- return this.editor._internal;
1643
- }
1644
- };
1645
- function numAttr(doc, id, name) {
1646
- const v = doc.get_attr(id, name);
1647
- if (v === null || v === "") return 0;
1648
- const n = parseFloat(v);
1649
- return Number.isFinite(n) ? n : 0;
1650
- }
1651
- function rect_intersects(a, b) {
1652
- 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;
1653
- }
1654
- //#endregion
1655
- Object.defineProperty(exports, "__exportAll", {
1656
- enumerable: true,
1657
- get: function() {
1658
- return __exportAll;
1659
- }
1660
- });
1661
- Object.defineProperty(exports, "__toESM", {
1662
- enumerable: true,
1663
- get: function() {
1664
- return __toESM;
1665
- }
1666
- });
1667
- Object.defineProperty(exports, "attach_dom_surface", {
1668
- enumerable: true,
1669
- get: function() {
1670
- return attach_dom_surface;
1671
- }
1672
- });