@grida/svg-editor 1.0.0-alpha.2 → 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -5
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/{dom-kA8NDuVh.mjs → dom-CmOu0HvI.mjs} +701 -7
- package/dist/dom-Cn-RtjRL.d.ts +48 -0
- package/dist/{dom-CfP_ZURh.js → dom-CoVZzFqy.js} +716 -7
- package/dist/dom-DJnZhtOd.d.mts +48 -0
- package/dist/dom.d.mts +2 -16
- package/dist/dom.d.ts +2 -16
- package/dist/dom.js +1 -1
- package/dist/dom.mjs +1 -1
- package/dist/{editor-M6j8XGO5.mjs → editor-CjK56cgb.mjs} +11 -11
- package/dist/{editor-BryibVvr.d.mts → editor-D2l_CDr0.d.ts} +314 -108
- package/dist/{editor-DllAMsDu.js → editor-D2zZAyny.js} +13 -13
- package/dist/{editor-klT8wu-x.d.ts → editor-Uu6dZX4y.d.mts} +314 -108
- package/dist/index-CHiXYO9-.d.ts +1 -0
- package/dist/index-ThDLM4Am.d.mts +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{paint-DuCg6Y-K.mjs → paint-Cfiw4g_J.mjs} +17 -1
- package/dist/{paint-DHq_3iwU.js → paint-dDV-Trt9.js} +23 -1
- package/dist/presets.d.mts +46 -0
- package/dist/presets.d.ts +46 -0
- package/dist/presets.js +55 -0
- package/dist/presets.mjs +50 -0
- package/dist/react.d.mts +49 -6
- package/dist/react.d.ts +49 -6
- package/dist/react.js +49 -9
- package/dist/react.mjs +49 -10
- package/package.json +9 -4
|
@@ -5,6 +5,15 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
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
|
+
};
|
|
8
17
|
var __copyProps = (to, from, except, desc) => {
|
|
9
18
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
19
|
key = keys[i];
|
|
@@ -20,18 +29,535 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
20
29
|
enumerable: true
|
|
21
30
|
}) : target, mod));
|
|
22
31
|
//#endregion
|
|
23
|
-
const require_paint = require("./paint-
|
|
32
|
+
const require_paint = require("./paint-dDV-Trt9.js");
|
|
24
33
|
let _grida_text_editor_dom = require("@grida/text-editor/dom");
|
|
25
34
|
let _grida_hud = require("@grida/hud");
|
|
26
35
|
let _grida_cmath__measurement = require("@grida/cmath/_measurement");
|
|
27
36
|
let _grida_cmath = require("@grida/cmath");
|
|
28
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
|
|
29
554
|
//#region src/text-surface.ts
|
|
30
555
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
31
556
|
const XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
32
557
|
var SvgTextSurface = class {
|
|
33
558
|
constructor(textEl) {
|
|
34
559
|
this.prevXmlSpace = void 0;
|
|
560
|
+
this.prevPointerEvents = void 0;
|
|
35
561
|
this.last_caret_idx = -1;
|
|
36
562
|
this.last_caret_visible = false;
|
|
37
563
|
this.last_sel_start = -1;
|
|
@@ -45,6 +571,8 @@ var SvgTextSurface = class {
|
|
|
45
571
|
this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
|
|
46
572
|
textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
|
|
47
573
|
}
|
|
574
|
+
this.prevPointerEvents = textEl.getAttribute("pointer-events");
|
|
575
|
+
textEl.setAttribute("pointer-events", "bounding-box");
|
|
48
576
|
const selection = ownerDoc.createElementNS(SVG_NS, "rect");
|
|
49
577
|
selection.setAttribute("fill", "#2563eb");
|
|
50
578
|
selection.setAttribute("fill-opacity", "0.25");
|
|
@@ -102,7 +630,10 @@ var SvgTextSurface = class {
|
|
|
102
630
|
this.selectionRect.remove();
|
|
103
631
|
if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
|
|
104
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);
|
|
105
635
|
this.prevXmlSpace = void 0;
|
|
636
|
+
this.prevPointerEvents = void 0;
|
|
106
637
|
}
|
|
107
638
|
positionAtPoint(clientX, clientY) {
|
|
108
639
|
const ctm = this.textEl.getScreenCTM();
|
|
@@ -192,20 +723,28 @@ const IS_MODIFIER_KEY = {
|
|
|
192
723
|
* live `<text>` element out from under the about-to-mount text surface. */
|
|
193
724
|
const TEXT_EDIT_PENDING = { __pending: true };
|
|
194
725
|
/**
|
|
195
|
-
* Attach a DOM surface to a headless editor. Returns a `
|
|
196
|
-
* `detach()` is the inverse — DOM cleared, listeners removed
|
|
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.
|
|
197
729
|
*
|
|
198
730
|
* Usage is one-shot per container: the surface owns the container's children
|
|
199
731
|
* for its lifetime, and `detach()` restores it to empty.
|
|
200
732
|
*/
|
|
201
733
|
function attach_dom_surface(editor, options) {
|
|
202
|
-
const surface = new DomSurface(editor, options
|
|
203
|
-
|
|
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
|
+
};
|
|
204
744
|
}
|
|
205
745
|
var DomSurface = class {
|
|
206
|
-
constructor(editor,
|
|
746
|
+
constructor(editor, options) {
|
|
207
747
|
this.editor = editor;
|
|
208
|
-
this.container = container;
|
|
209
748
|
this.svg_root = null;
|
|
210
749
|
this.teardown = [];
|
|
211
750
|
this.element_index = /* @__PURE__ */ new Map();
|
|
@@ -215,6 +754,9 @@ var DomSurface = class {
|
|
|
215
754
|
this.text_edit_target = null;
|
|
216
755
|
this.text_edit_original = "";
|
|
217
756
|
this.editor_hover_internal = null;
|
|
757
|
+
this.container = options.container;
|
|
758
|
+
const container = this.container;
|
|
759
|
+
this.fit_on_attach = options.fit === true;
|
|
218
760
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
219
761
|
container.style.userSelect = "none";
|
|
220
762
|
container.style.webkitUserSelect = "none";
|
|
@@ -232,6 +774,14 @@ var DomSurface = class {
|
|
|
232
774
|
onIntent: (i) => this.commit_intent(i),
|
|
233
775
|
style: { chromeColor: editor.style.chrome_color }
|
|
234
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
|
+
}));
|
|
235
785
|
this.render();
|
|
236
786
|
this.sync_canvas_size();
|
|
237
787
|
this.sync_surface_selection();
|
|
@@ -239,9 +789,19 @@ var DomSurface = class {
|
|
|
239
789
|
const win = container.ownerDocument.defaultView ?? window;
|
|
240
790
|
const raf = win.requestAnimationFrame(() => {
|
|
241
791
|
this.sync_canvas_size();
|
|
792
|
+
this.honor_initial_fit();
|
|
242
793
|
this.redraw();
|
|
243
794
|
});
|
|
244
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);
|
|
245
805
|
const unsub = editor.subscribe(() => {
|
|
246
806
|
this.render();
|
|
247
807
|
this.sync_surface_selection();
|
|
@@ -313,6 +873,7 @@ var DomSurface = class {
|
|
|
313
873
|
this.text_edit = null;
|
|
314
874
|
this.text_edit_target = null;
|
|
315
875
|
}
|
|
876
|
+
this.gestures._dispose();
|
|
316
877
|
for (const fn of this.teardown) fn();
|
|
317
878
|
this.teardown = [];
|
|
318
879
|
this.hud.dispose();
|
|
@@ -322,6 +883,10 @@ var DomSurface = class {
|
|
|
322
883
|
this.element_index.clear();
|
|
323
884
|
this.active_preview = null;
|
|
324
885
|
}
|
|
886
|
+
/** Public — invoked by the `DomSurfaceHandle` wrapper before `detach()`. */
|
|
887
|
+
detach_gestures() {
|
|
888
|
+
this.gestures._dispose();
|
|
889
|
+
}
|
|
325
890
|
render() {
|
|
326
891
|
if (this.text_edit) return;
|
|
327
892
|
const owner_doc = this.container.ownerDocument;
|
|
@@ -334,6 +899,8 @@ var DomSurface = class {
|
|
|
334
899
|
if (this.svg_root) this.svg_root.replaceWith(new_svg);
|
|
335
900
|
else this.container.insertBefore(new_svg, this.hud_canvas);
|
|
336
901
|
this.svg_root = new_svg;
|
|
902
|
+
this.apply_svg_layout();
|
|
903
|
+
this.apply_camera_transform();
|
|
337
904
|
this.element_index.clear();
|
|
338
905
|
const ids = doc.all_elements();
|
|
339
906
|
let i = 0;
|
|
@@ -350,8 +917,142 @@ var DomSurface = class {
|
|
|
350
917
|
sync_canvas_size() {
|
|
351
918
|
const cr = this.container.getBoundingClientRect();
|
|
352
919
|
this.hud.setSize(cr.width, cr.height);
|
|
920
|
+
this.camera._set_viewport_size(cr.width, cr.height);
|
|
353
921
|
this.redraw();
|
|
354
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
|
+
}
|
|
355
1056
|
/** Single per-frame draw entry — merges host-fed extras with surface chrome. */
|
|
356
1057
|
redraw() {
|
|
357
1058
|
this.hud.draw(this.compute_measurement_extra());
|
|
@@ -567,8 +1268,10 @@ var DomSurface = class {
|
|
|
567
1268
|
dispatch_pointer(e, kind) {
|
|
568
1269
|
if (this.text_edit) {
|
|
569
1270
|
if (kind === "pointer_down") {
|
|
1271
|
+
e.preventDefault();
|
|
570
1272
|
const el = this.text_edit_target ? this.element_index.get(this.text_edit_target) : null;
|
|
571
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();
|
|
572
1275
|
} else if (kind === "pointer_move") this.text_edit.pointerMove(e.clientX, e.clientY);
|
|
573
1276
|
else if (kind === "pointer_up") this.text_edit.pointerUp();
|
|
574
1277
|
return;
|
|
@@ -949,6 +1652,12 @@ function rect_intersects(a, b) {
|
|
|
949
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;
|
|
950
1653
|
}
|
|
951
1654
|
//#endregion
|
|
1655
|
+
Object.defineProperty(exports, "__exportAll", {
|
|
1656
|
+
enumerable: true,
|
|
1657
|
+
get: function() {
|
|
1658
|
+
return __exportAll;
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
952
1661
|
Object.defineProperty(exports, "__toESM", {
|
|
953
1662
|
enumerable: true,
|
|
954
1663
|
get: function() {
|