@grida/svg-editor 1.0.0-alpha.13 → 1.0.0-alpha.15
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 +111 -42
- package/dist/dom-CK6GlgFF.d.mts +114 -0
- package/dist/dom-CsKXTaNw.d.ts +112 -0
- package/dist/{dom-BlMk07oX.mjs → dom-DILY80j7.mjs} +1622 -619
- package/dist/{dom-Cvm9Towu.js → dom-Dee6FtgZ.js} +1648 -626
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +4 -1
- package/dist/dom.mjs +2 -2
- package/dist/{editor-Bd4-VCEJ.d.ts → editor-BKoo9SPL.d.ts} +643 -25
- package/dist/{editor-DtuRIs-Q.mjs → editor-CvWpD5mu.mjs} +820 -322
- package/dist/{editor-BH03X8cX.d.mts → editor-Dl7c0q5A.d.mts} +643 -25
- package/dist/{editor-CdyC3uAe.js → editor-F8ckj9X1.js} +828 -330
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.mjs +3 -3
- package/dist/model-B2UWgViT.mjs +3729 -0
- package/dist/model-CJ1Ctq14.js +3875 -0
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +3 -3
- package/dist/presets.mjs +2 -2
- package/dist/react.d.mts +18 -6
- package/dist/react.d.ts +18 -6
- package/dist/react.js +24 -3
- package/dist/react.mjs +24 -3
- package/package.json +30 -8
- package/dist/dom-DCX-a8Kr.d.ts +0 -57
- package/dist/dom-DgB4f-TE.d.mts +0 -59
- package/dist/insertions-BJ-6o6o5.js +0 -2399
- package/dist/insertions-Okcuo-Ck.mjs +0 -2176
- /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
|
@@ -1,2399 +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
|
-
let _grida_cmath = require("@grida/cmath");
|
|
33
|
-
_grida_cmath = __toESM(_grida_cmath);
|
|
34
|
-
let _grida_svg_pathdata = require("@grida/svg/pathdata");
|
|
35
|
-
let _grida_svg_parse = require("@grida/svg/parse");
|
|
36
|
-
//#region src/util/dom.ts
|
|
37
|
-
/**
|
|
38
|
-
* `true` when the document's active element is a text-input-like control
|
|
39
|
-
* (input / textarea / contentEditable). Used by keymap + gesture defaults
|
|
40
|
-
* to avoid hijacking keystrokes while the user is typing.
|
|
41
|
-
*/
|
|
42
|
-
function is_text_input_focused() {
|
|
43
|
-
if (typeof document === "undefined") return false;
|
|
44
|
-
const el = document.activeElement;
|
|
45
|
-
if (!el) return false;
|
|
46
|
-
const tag = el.tagName;
|
|
47
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return true;
|
|
48
|
-
if (el.isContentEditable) return true;
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/core/group.ts
|
|
53
|
-
/**
|
|
54
|
-
* Tags that may be valid children of a `<g>` element. Wrapping any other
|
|
55
|
-
* tag in `<g>` would produce content-model-invalid SVG (e.g. `<tspan>`
|
|
56
|
-
* must live inside `<text>`; `<stop>` must live inside a gradient).
|
|
57
|
-
*/
|
|
58
|
-
const STRUCTURAL_GRAPHICS_SET = new Set([
|
|
59
|
-
"g",
|
|
60
|
-
"defs",
|
|
61
|
-
"svg",
|
|
62
|
-
"use",
|
|
63
|
-
"image",
|
|
64
|
-
"switch",
|
|
65
|
-
"foreignObject",
|
|
66
|
-
"path",
|
|
67
|
-
"rect",
|
|
68
|
-
"circle",
|
|
69
|
-
"ellipse",
|
|
70
|
-
"line",
|
|
71
|
-
"polyline",
|
|
72
|
-
"polygon",
|
|
73
|
-
"text",
|
|
74
|
-
"a"
|
|
75
|
-
]);
|
|
76
|
-
/**
|
|
77
|
-
* Tags whose content model is constrained — a freshly-inserted `<g>`
|
|
78
|
-
* here would either be invalid (text-content / gradient / filter
|
|
79
|
-
* parents) or semantically meaningless (defs, symbol).
|
|
80
|
-
*/
|
|
81
|
-
const CONSTRAINED_PARENT_SET = new Set([
|
|
82
|
-
"text",
|
|
83
|
-
"tspan",
|
|
84
|
-
"defs",
|
|
85
|
-
"clipPath",
|
|
86
|
-
"mask",
|
|
87
|
-
"pattern",
|
|
88
|
-
"marker",
|
|
89
|
-
"symbol",
|
|
90
|
-
"filter",
|
|
91
|
-
"linearGradient",
|
|
92
|
-
"radialGradient",
|
|
93
|
-
"animateMotion",
|
|
94
|
-
"switch"
|
|
95
|
-
]);
|
|
96
|
-
/**
|
|
97
|
-
* Read-only policy gate. Returns a plan when grouping the given
|
|
98
|
-
* selection is accepted; returns `null` (rejected) otherwise.
|
|
99
|
-
*
|
|
100
|
-
* Decision tree matches `packages/grida-svg-editor/docs/wg/feat-svg-editor/grouping.md`. Default
|
|
101
|
-
* stance: "when unclear, reject."
|
|
102
|
-
*/
|
|
103
|
-
function plan_group(doc, ids) {
|
|
104
|
-
if (ids.length === 0) return null;
|
|
105
|
-
const parent = doc.parent_of(ids[0]);
|
|
106
|
-
if (parent === null) return null;
|
|
107
|
-
for (const id of ids) {
|
|
108
|
-
if (doc.parent_of(id) !== parent) return null;
|
|
109
|
-
if (!STRUCTURAL_GRAPHICS_SET.has(doc.tag_of(id))) return null;
|
|
110
|
-
}
|
|
111
|
-
if (CONSTRAINED_PARENT_SET.has(doc.tag_of(parent))) return null;
|
|
112
|
-
const siblings = doc.element_children_of(parent);
|
|
113
|
-
const sibling_index = /* @__PURE__ */ new Map();
|
|
114
|
-
for (let i = 0; i < siblings.length; i++) sibling_index.set(siblings[i], i);
|
|
115
|
-
const indices = [];
|
|
116
|
-
for (const id of ids) {
|
|
117
|
-
const i = sibling_index.get(id);
|
|
118
|
-
if (i === void 0) return null;
|
|
119
|
-
indices.push(i);
|
|
120
|
-
}
|
|
121
|
-
const sorted = Array.from(new Set(indices)).sort((a, b) => a - b);
|
|
122
|
-
for (let i = 1; i < sorted.length; i++) if (sorted[i] !== sorted[i - 1] + 1) return null;
|
|
123
|
-
const children = sorted.map((i) => siblings[i]);
|
|
124
|
-
const last_index = sorted[sorted.length - 1];
|
|
125
|
-
const original_positions = /* @__PURE__ */ new Map();
|
|
126
|
-
for (const i of sorted) original_positions.set(siblings[i], siblings[i + 1] ?? null);
|
|
127
|
-
return {
|
|
128
|
-
parent,
|
|
129
|
-
insert_before: siblings[last_index + 1] ?? null,
|
|
130
|
-
children,
|
|
131
|
-
original_positions
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region src/core/translate-pipeline/pipeline.ts
|
|
136
|
-
/** The funnel. Threads `plan` through `stages` in order; aggregates
|
|
137
|
-
* guide emissions. Pure: same inputs → same outputs. */
|
|
138
|
-
function run_translate_pipeline(init, stages, ctx) {
|
|
139
|
-
let plan = init;
|
|
140
|
-
const guides = [];
|
|
141
|
-
for (const stage of stages) {
|
|
142
|
-
const out = stage.run(plan, ctx);
|
|
143
|
-
plan = out.plan;
|
|
144
|
-
if (out.emit?.guide) guides.push(out.emit.guide);
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
plan,
|
|
148
|
-
guides
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
//#endregion
|
|
152
|
-
//#region src/core/transform/parse.ts
|
|
153
|
-
/** SVG `<number>` production (spec-aligned subset). */
|
|
154
|
-
const SVG_NUMBER_SRC = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?";
|
|
155
|
-
/** Recognized function names. Names are case-sensitive per SVG 1.1 §7.6. */
|
|
156
|
-
const FUNCTION_RE = /\s*([A-Za-z]+)\s*\(([^)]*)\)\s*/y;
|
|
157
|
-
/** Number tokens, comma- or whitespace-separated, inside a single call. */
|
|
158
|
-
const NUMBER_GLOBAL_RE = new RegExp(SVG_NUMBER_SRC, "g");
|
|
159
|
-
/** Identity / no-op string forms. SVG 2 §11.6.1 lets `transform="none"`
|
|
160
|
-
* mean the identity transform. CSS-wide keywords are normalized at
|
|
161
|
-
* parse time per SVG 2 cascade. */
|
|
162
|
-
const IDENTITY_RE = /^\s*(?:none|inherit|unset|initial|revert|revert-layer)?\s*$/;
|
|
163
|
-
function parse_args(args) {
|
|
164
|
-
const tokens = args.match(NUMBER_GLOBAL_RE);
|
|
165
|
-
if (!tokens) return [];
|
|
166
|
-
const out = [];
|
|
167
|
-
for (const t of tokens) {
|
|
168
|
-
const n = parseFloat(t);
|
|
169
|
-
if (!Number.isFinite(n)) return null;
|
|
170
|
-
out.push(n);
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
function build_op(name, a) {
|
|
175
|
-
switch (name) {
|
|
176
|
-
case "matrix":
|
|
177
|
-
if (a.length !== 6) return null;
|
|
178
|
-
return {
|
|
179
|
-
type: "matrix",
|
|
180
|
-
a: a[0],
|
|
181
|
-
b: a[1],
|
|
182
|
-
c: a[2],
|
|
183
|
-
d: a[3],
|
|
184
|
-
e: a[4],
|
|
185
|
-
f: a[5]
|
|
186
|
-
};
|
|
187
|
-
case "translate":
|
|
188
|
-
if (a.length !== 1 && a.length !== 2) return null;
|
|
189
|
-
return {
|
|
190
|
-
type: "translate",
|
|
191
|
-
tx: a[0],
|
|
192
|
-
ty: a.length === 2 ? a[1] : 0
|
|
193
|
-
};
|
|
194
|
-
case "rotate":
|
|
195
|
-
if (a.length !== 1 && a.length !== 3) return null;
|
|
196
|
-
return {
|
|
197
|
-
type: "rotate",
|
|
198
|
-
angle: a[0],
|
|
199
|
-
cx: a.length === 3 ? a[1] : 0,
|
|
200
|
-
cy: a.length === 3 ? a[2] : 0,
|
|
201
|
-
explicit_pivot: a.length === 3
|
|
202
|
-
};
|
|
203
|
-
case "scale":
|
|
204
|
-
if (a.length !== 1 && a.length !== 2) return null;
|
|
205
|
-
return {
|
|
206
|
-
type: "scale",
|
|
207
|
-
sx: a[0],
|
|
208
|
-
sy: a.length === 2 ? a[1] : a[0]
|
|
209
|
-
};
|
|
210
|
-
case "skewX":
|
|
211
|
-
if (a.length !== 1) return null;
|
|
212
|
-
return {
|
|
213
|
-
type: "skewX",
|
|
214
|
-
angle: a[0]
|
|
215
|
-
};
|
|
216
|
-
case "skewY":
|
|
217
|
-
if (a.length !== 1) return null;
|
|
218
|
-
return {
|
|
219
|
-
type: "skewY",
|
|
220
|
-
angle: a[0]
|
|
221
|
-
};
|
|
222
|
-
default: return null;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Parse a `transform=""` attribute string into a list of typed ops.
|
|
227
|
-
*
|
|
228
|
-
* parse_transform_list(null) => []
|
|
229
|
-
* parse_transform_list("") => []
|
|
230
|
-
* parse_transform_list("none") => []
|
|
231
|
-
* parse_transform_list("translate(10 20)") => [{type:"translate",tx:10,ty:20}]
|
|
232
|
-
* parse_transform_list("rotate(30 50 50)") => [{type:"rotate",angle:30,cx:50,cy:50}]
|
|
233
|
-
* parse_transform_list("foo(1)") => null // unknown function
|
|
234
|
-
* parse_transform_list("matrix(1 0 0)") => null // wrong arg count
|
|
235
|
-
*/
|
|
236
|
-
function parse_transform_list(input) {
|
|
237
|
-
if (input === null) return [];
|
|
238
|
-
if (IDENTITY_RE.test(input)) return [];
|
|
239
|
-
const ops = [];
|
|
240
|
-
FUNCTION_RE.lastIndex = 0;
|
|
241
|
-
let pos = 0;
|
|
242
|
-
while (pos < input.length) {
|
|
243
|
-
FUNCTION_RE.lastIndex = pos;
|
|
244
|
-
const m = FUNCTION_RE.exec(input);
|
|
245
|
-
if (!m || m.index !== pos) return null;
|
|
246
|
-
const name = m[1];
|
|
247
|
-
const args = parse_args(m[2]);
|
|
248
|
-
if (args === null) return null;
|
|
249
|
-
const op = build_op(name, args);
|
|
250
|
-
if (!op) return null;
|
|
251
|
-
ops.push(op);
|
|
252
|
-
pos = FUNCTION_RE.lastIndex;
|
|
253
|
-
while (pos < input.length && /[\s,]/.test(input[pos])) pos++;
|
|
254
|
-
}
|
|
255
|
-
return ops;
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
//#region src/core/transform/emit.ts
|
|
259
|
-
function n(x) {
|
|
260
|
-
return String(x);
|
|
261
|
-
}
|
|
262
|
-
function emit_op(op) {
|
|
263
|
-
switch (op.type) {
|
|
264
|
-
case "matrix": return `matrix(${n(op.a)} ${n(op.b)} ${n(op.c)} ${n(op.d)} ${n(op.e)} ${n(op.f)})`;
|
|
265
|
-
case "translate": return `translate(${n(op.tx)} ${n(op.ty)})`;
|
|
266
|
-
case "rotate": return `rotate(${n(op.angle)} ${n(op.cx)} ${n(op.cy)})`;
|
|
267
|
-
case "scale": return `scale(${n(op.sx)} ${n(op.sy)})`;
|
|
268
|
-
case "skewX": return `skewX(${n(op.angle)})`;
|
|
269
|
-
case "skewY": return `skewY(${n(op.angle)})`;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
/** Concatenate ops with single-space separator. Returns `""` for empty
|
|
273
|
-
* list (identity). Caller decides whether to write `transform=""`,
|
|
274
|
-
* `transform="none"`, or remove the attribute entirely. */
|
|
275
|
-
function emit_transform_list(ops) {
|
|
276
|
-
return ops.map(emit_op).join(" ");
|
|
277
|
-
}
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/core/transform/classify.ts
|
|
280
|
-
function is_identity_translate(op) {
|
|
281
|
-
return op.type === "translate" && op.tx === 0 && op.ty === 0;
|
|
282
|
-
}
|
|
283
|
-
function is_identity_rotate(op) {
|
|
284
|
-
return op.type === "rotate" && op.angle === 0;
|
|
285
|
-
}
|
|
286
|
-
function classify(ops) {
|
|
287
|
-
const trimmed = ops.filter((op) => !is_identity_translate(op) && !is_identity_rotate(op));
|
|
288
|
-
if (trimmed.length === 0) return "identity";
|
|
289
|
-
if (trimmed.length === 1) {
|
|
290
|
-
if (trimmed[0].type === "translate") return "leading_translate_only";
|
|
291
|
-
if (trimmed[0].type === "rotate") return "single_rotate_only";
|
|
292
|
-
return "mixed";
|
|
293
|
-
}
|
|
294
|
-
if (trimmed.length === 2 && trimmed[0].type === "translate" && trimmed[1].type === "rotate") return "leading_translate_then_single_rotate";
|
|
295
|
-
return "mixed";
|
|
296
|
-
}
|
|
297
|
-
//#endregion
|
|
298
|
-
//#region src/core/transform/recompose.ts
|
|
299
|
-
function recompose_with_pivot(ops, new_cx, new_cy) {
|
|
300
|
-
const tx_op = ops.find((op) => op.type === "translate");
|
|
301
|
-
const tx = tx_op?.type === "translate" ? tx_op.tx : 0;
|
|
302
|
-
const ty = tx_op?.type === "translate" ? tx_op.ty : 0;
|
|
303
|
-
return emit_transform_list(ops.map((op) => op.type === "rotate" ? {
|
|
304
|
-
type: "rotate",
|
|
305
|
-
angle: op.angle,
|
|
306
|
-
cx: new_cx - tx,
|
|
307
|
-
cy: new_cy - ty,
|
|
308
|
-
explicit_pivot: true
|
|
309
|
-
} : op));
|
|
310
|
-
}
|
|
311
|
-
//#endregion
|
|
312
|
-
//#region src/core/transform/project.ts
|
|
313
|
-
function op_matrix(op) {
|
|
314
|
-
switch (op.type) {
|
|
315
|
-
case "matrix": return [[
|
|
316
|
-
op.a,
|
|
317
|
-
op.c,
|
|
318
|
-
op.e
|
|
319
|
-
], [
|
|
320
|
-
op.b,
|
|
321
|
-
op.d,
|
|
322
|
-
op.f
|
|
323
|
-
]];
|
|
324
|
-
case "translate": return [[
|
|
325
|
-
1,
|
|
326
|
-
0,
|
|
327
|
-
op.tx
|
|
328
|
-
], [
|
|
329
|
-
0,
|
|
330
|
-
1,
|
|
331
|
-
op.ty
|
|
332
|
-
]];
|
|
333
|
-
case "rotate": {
|
|
334
|
-
const t = op.angle * Math.PI / 180;
|
|
335
|
-
const cos = Math.cos(t);
|
|
336
|
-
const sin = Math.sin(t);
|
|
337
|
-
return [[
|
|
338
|
-
cos,
|
|
339
|
-
-sin,
|
|
340
|
-
op.cx - op.cx * cos + op.cy * sin
|
|
341
|
-
], [
|
|
342
|
-
sin,
|
|
343
|
-
cos,
|
|
344
|
-
op.cy - op.cx * sin - op.cy * cos
|
|
345
|
-
]];
|
|
346
|
-
}
|
|
347
|
-
case "scale": return [[
|
|
348
|
-
op.sx,
|
|
349
|
-
0,
|
|
350
|
-
0
|
|
351
|
-
], [
|
|
352
|
-
0,
|
|
353
|
-
op.sy,
|
|
354
|
-
0
|
|
355
|
-
]];
|
|
356
|
-
case "skewX": {
|
|
357
|
-
const t = op.angle * Math.PI / 180;
|
|
358
|
-
return [[
|
|
359
|
-
1,
|
|
360
|
-
Math.tan(t),
|
|
361
|
-
0
|
|
362
|
-
], [
|
|
363
|
-
0,
|
|
364
|
-
1,
|
|
365
|
-
0
|
|
366
|
-
]];
|
|
367
|
-
}
|
|
368
|
-
case "skewY": {
|
|
369
|
-
const t = op.angle * Math.PI / 180;
|
|
370
|
-
return [[
|
|
371
|
-
1,
|
|
372
|
-
0,
|
|
373
|
-
0
|
|
374
|
-
], [
|
|
375
|
-
Math.tan(t),
|
|
376
|
-
1,
|
|
377
|
-
0
|
|
378
|
-
]];
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
/** Compose a transform-list into a single 2×3 affine. Ops compose
|
|
383
|
-
* source-order = left-to-right multiplication: `transform="A B C"` maps
|
|
384
|
-
* a column-vector point `p` as `A · B · C · p` (per SVG 1.1 §7.5). */
|
|
385
|
-
function compose(ops) {
|
|
386
|
-
let m = _grida_cmath.default.transform.identity;
|
|
387
|
-
for (const op of ops) m = _grida_cmath.default.transform.multiply(m, op_matrix(op));
|
|
388
|
-
return m;
|
|
389
|
-
}
|
|
390
|
-
/** Axis-aligned doc-space bounding box of `local` under `transform_str`.
|
|
391
|
-
* Returns `local` unchanged when the transform is absent / empty /
|
|
392
|
-
* unparseable (i.e. local-frame ≡ doc-space in those cases). */
|
|
393
|
-
function project_local_bbox(local, transform_str) {
|
|
394
|
-
if (!transform_str) return local;
|
|
395
|
-
const ops = parse_transform_list(transform_str);
|
|
396
|
-
if (ops === null || ops.length === 0) return local;
|
|
397
|
-
const m = compose(ops);
|
|
398
|
-
const projected = [
|
|
399
|
-
[local.x, local.y],
|
|
400
|
-
[local.x + local.width, local.y],
|
|
401
|
-
[local.x + local.width, local.y + local.height],
|
|
402
|
-
[local.x, local.y + local.height]
|
|
403
|
-
].map((p) => _grida_cmath.default.vector2.transform(p, m));
|
|
404
|
-
return _grida_cmath.default.rect.fromPointsOrZero(projected);
|
|
405
|
-
}
|
|
406
|
-
//#endregion
|
|
407
|
-
//#region src/core/hit-shape-svg.ts
|
|
408
|
-
/** Tags that never participate in picking. Containers and non-rendering
|
|
409
|
-
* metadata. The root `<svg>` is in this set — root pickability is a
|
|
410
|
-
* host decision (measurement HUD wants it, selection doesn't), gated
|
|
411
|
-
* by an explicit `allow_root` flag at the caller. */
|
|
412
|
-
const TRANSPARENT_TAGS = new Set([
|
|
413
|
-
"g",
|
|
414
|
-
"svg",
|
|
415
|
-
"defs",
|
|
416
|
-
"symbol",
|
|
417
|
-
"clipPath",
|
|
418
|
-
"mask",
|
|
419
|
-
"marker",
|
|
420
|
-
"pattern",
|
|
421
|
-
"linearGradient",
|
|
422
|
-
"radialGradient",
|
|
423
|
-
"stop",
|
|
424
|
-
"filter",
|
|
425
|
-
"title",
|
|
426
|
-
"desc",
|
|
427
|
-
"metadata",
|
|
428
|
-
"style",
|
|
429
|
-
"script"
|
|
430
|
-
]);
|
|
431
|
-
function is_transparent_tag(tag) {
|
|
432
|
-
return TRANSPARENT_TAGS.has(tag);
|
|
433
|
-
}
|
|
434
|
-
function num$1(doc, id, name, fallback = 0) {
|
|
435
|
-
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Hit-shape derived from document attributes. Returns `null` when the
|
|
439
|
-
* node has no derivable shape (transparent tag, malformed attrs, or a
|
|
440
|
-
* `transform=` we can't compose cleanly — matrix / scale / skew / mixed).
|
|
441
|
-
*
|
|
442
|
-
* For `transform=` values that classify to identity / leading-translate /
|
|
443
|
-
* single-rotate / translate-then-rotate, the returned shape is the
|
|
444
|
-
* post-transform polygon/segment (rotated rects become 4-corner polygons),
|
|
445
|
-
* so picking lands on the rendered outline rather than the silent AABB
|
|
446
|
-
* fallback in `SvgHitShapeDriver`.
|
|
447
|
-
*
|
|
448
|
-
* Caveat: see "Known issues" #1 at the top of this file — ancestor CTM
|
|
449
|
-
* is still NOT composed here. Inside a `<g transform="...">` the shape
|
|
450
|
-
* is still in the ancestor-relative frame.
|
|
451
|
-
*/
|
|
452
|
-
function hit_shape_of_doc(doc, id) {
|
|
453
|
-
const tag = doc.tag_of(id);
|
|
454
|
-
const ops = parse_transform_list(doc.get_attr(id, "transform"));
|
|
455
|
-
if (ops === null) return null;
|
|
456
|
-
if (classify(ops) === "mixed") return null;
|
|
457
|
-
const xform = pick_affine(ops);
|
|
458
|
-
const has_rotation = xform.angle_rad !== 0;
|
|
459
|
-
switch (tag) {
|
|
460
|
-
case "rect": {
|
|
461
|
-
const x = num$1(doc, id, "x");
|
|
462
|
-
const y = num$1(doc, id, "y");
|
|
463
|
-
const w = num$1(doc, id, "width");
|
|
464
|
-
const h = num$1(doc, id, "height");
|
|
465
|
-
if (!has_rotation) {
|
|
466
|
-
const t = xform.translate;
|
|
467
|
-
return {
|
|
468
|
-
kind: "rect",
|
|
469
|
-
x: x + t.x,
|
|
470
|
-
y: y + t.y,
|
|
471
|
-
width: w,
|
|
472
|
-
height: h
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
kind: "polygon",
|
|
477
|
-
pts: [
|
|
478
|
-
{
|
|
479
|
-
x,
|
|
480
|
-
y
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
x: x + w,
|
|
484
|
-
y
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
x: x + w,
|
|
488
|
-
y: y + h
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
x,
|
|
492
|
-
y: y + h
|
|
493
|
-
}
|
|
494
|
-
].map((p) => transform_point(p, xform)),
|
|
495
|
-
closed: true
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
case "circle": {
|
|
499
|
-
const cx = num$1(doc, id, "cx");
|
|
500
|
-
const cy = num$1(doc, id, "cy");
|
|
501
|
-
const r = num$1(doc, id, "r");
|
|
502
|
-
const tc = transform_point({
|
|
503
|
-
x: cx,
|
|
504
|
-
y: cy
|
|
505
|
-
}, xform);
|
|
506
|
-
return {
|
|
507
|
-
kind: "ellipse",
|
|
508
|
-
cx: tc.x,
|
|
509
|
-
cy: tc.y,
|
|
510
|
-
rx: r,
|
|
511
|
-
ry: r
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
case "ellipse": {
|
|
515
|
-
const cx = num$1(doc, id, "cx");
|
|
516
|
-
const cy = num$1(doc, id, "cy");
|
|
517
|
-
const rx = num$1(doc, id, "rx");
|
|
518
|
-
const ry = num$1(doc, id, "ry");
|
|
519
|
-
if (has_rotation) return null;
|
|
520
|
-
const tc = transform_point({
|
|
521
|
-
x: cx,
|
|
522
|
-
y: cy
|
|
523
|
-
}, xform);
|
|
524
|
-
return {
|
|
525
|
-
kind: "ellipse",
|
|
526
|
-
cx: tc.x,
|
|
527
|
-
cy: tc.y,
|
|
528
|
-
rx,
|
|
529
|
-
ry
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
case "line": return {
|
|
533
|
-
kind: "segment",
|
|
534
|
-
a: transform_point({
|
|
535
|
-
x: num$1(doc, id, "x1"),
|
|
536
|
-
y: num$1(doc, id, "y1")
|
|
537
|
-
}, xform),
|
|
538
|
-
b: transform_point({
|
|
539
|
-
x: num$1(doc, id, "x2"),
|
|
540
|
-
y: num$1(doc, id, "y2")
|
|
541
|
-
}, xform)
|
|
542
|
-
};
|
|
543
|
-
case "polyline": {
|
|
544
|
-
const pts = _grida_svg_parse.svg_parse.parse_points(doc.get_attr(id, "points") ?? "");
|
|
545
|
-
if (pts.length === 0) return null;
|
|
546
|
-
return {
|
|
547
|
-
kind: "polyline",
|
|
548
|
-
pts: pts.map((q) => transform_point({
|
|
549
|
-
x: q.x,
|
|
550
|
-
y: q.y
|
|
551
|
-
}, xform)),
|
|
552
|
-
closed: false
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
case "polygon": {
|
|
556
|
-
const pts = _grida_svg_parse.svg_parse.parse_points(doc.get_attr(id, "points") ?? "");
|
|
557
|
-
if (pts.length === 0) return null;
|
|
558
|
-
return {
|
|
559
|
-
kind: "polygon",
|
|
560
|
-
pts: pts.map((q) => transform_point({
|
|
561
|
-
x: q.x,
|
|
562
|
-
y: q.y
|
|
563
|
-
}, xform)),
|
|
564
|
-
closed: true
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
case "path": {
|
|
568
|
-
const d = doc.get_attr(id, "d") ?? "";
|
|
569
|
-
const pts = path_control_polyline(d);
|
|
570
|
-
if (pts.length === 0) return null;
|
|
571
|
-
return {
|
|
572
|
-
kind: "path",
|
|
573
|
-
pts: pts.map((p) => transform_point(p, xform)),
|
|
574
|
-
closed: /[zZ]\s*$/.test(d)
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
default: return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function pick_affine(ops) {
|
|
581
|
-
let tx = 0;
|
|
582
|
-
let ty = 0;
|
|
583
|
-
let pivot = {
|
|
584
|
-
x: 0,
|
|
585
|
-
y: 0
|
|
586
|
-
};
|
|
587
|
-
let angle_rad = 0;
|
|
588
|
-
for (const op of ops) if (op.type === "translate") {
|
|
589
|
-
tx = op.tx;
|
|
590
|
-
ty = op.ty;
|
|
591
|
-
} else if (op.type === "rotate") {
|
|
592
|
-
pivot = {
|
|
593
|
-
x: op.cx,
|
|
594
|
-
y: op.cy
|
|
595
|
-
};
|
|
596
|
-
angle_rad = op.angle * Math.PI / 180;
|
|
597
|
-
}
|
|
598
|
-
return {
|
|
599
|
-
translate: {
|
|
600
|
-
x: tx,
|
|
601
|
-
y: ty
|
|
602
|
-
},
|
|
603
|
-
pivot,
|
|
604
|
-
angle_rad
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
/** Apply (rotate around pivot by angle_rad, then translate). Mirrors the
|
|
608
|
-
* SVG transform-list semantic order: `translate(...) rotate(...)` means
|
|
609
|
-
* matrix product T·R applied to P → translation moves the *rotated*
|
|
610
|
-
* point. */
|
|
611
|
-
function transform_point(p, x) {
|
|
612
|
-
let q = p;
|
|
613
|
-
if (x.angle_rad !== 0) {
|
|
614
|
-
const c = Math.cos(x.angle_rad);
|
|
615
|
-
const s = Math.sin(x.angle_rad);
|
|
616
|
-
const dx = q.x - x.pivot.x;
|
|
617
|
-
const dy = q.y - x.pivot.y;
|
|
618
|
-
q = {
|
|
619
|
-
x: x.pivot.x + dx * c - dy * s,
|
|
620
|
-
y: x.pivot.y + dx * s + dy * c
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
if (x.translate.x !== 0 || x.translate.y !== 0) q = {
|
|
624
|
-
x: q.x + x.translate.x,
|
|
625
|
-
y: q.y + x.translate.y
|
|
626
|
-
};
|
|
627
|
-
return q;
|
|
628
|
-
}
|
|
629
|
-
/** Path-`d` → control-polyline approximation. Each command contributes
|
|
630
|
-
* any control points it carries (Q: x1/y1; C: x1/y1 + x2/y2) plus its
|
|
631
|
-
* endpoint. T/S shorthand and A arcs contribute only the endpoint at
|
|
632
|
-
* this fidelity. Returns `[]` for unparseable input. */
|
|
633
|
-
function path_control_polyline(d) {
|
|
634
|
-
if (!d) return [];
|
|
635
|
-
let path;
|
|
636
|
-
try {
|
|
637
|
-
path = new _grida_svg_pathdata.SVGPathData(d).toAbs();
|
|
638
|
-
} catch {
|
|
639
|
-
return [];
|
|
640
|
-
}
|
|
641
|
-
const out = [];
|
|
642
|
-
for (const cmd of path.commands) {
|
|
643
|
-
const c = cmd;
|
|
644
|
-
if (typeof c.x1 === "number" && typeof c.y1 === "number") out.push({
|
|
645
|
-
x: c.x1,
|
|
646
|
-
y: c.y1
|
|
647
|
-
});
|
|
648
|
-
if (typeof c.x2 === "number" && typeof c.y2 === "number") out.push({
|
|
649
|
-
x: c.x2,
|
|
650
|
-
y: c.y2
|
|
651
|
-
});
|
|
652
|
-
if (typeof c.x === "number" && typeof c.y === "number") out.push({
|
|
653
|
-
x: c.x,
|
|
654
|
-
y: c.y
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
return out;
|
|
658
|
-
}
|
|
659
|
-
//#endregion
|
|
660
|
-
//#region src/core/intents.ts
|
|
661
|
-
function num(doc, id, name, fallback = 0) {
|
|
662
|
-
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
663
|
-
}
|
|
664
|
-
function capture_translate_baseline(doc, id) {
|
|
665
|
-
const tag = doc.tag_of(id);
|
|
666
|
-
const own_transform = doc.get_attr(id, "transform");
|
|
667
|
-
if (own_transform !== null || tag === "g") return {
|
|
668
|
-
type: "viaTransform",
|
|
669
|
-
transform: own_transform
|
|
670
|
-
};
|
|
671
|
-
switch (tag) {
|
|
672
|
-
case "rect": return {
|
|
673
|
-
type: "rect",
|
|
674
|
-
x: num(doc, id, "x"),
|
|
675
|
-
y: num(doc, id, "y")
|
|
676
|
-
};
|
|
677
|
-
case "circle": return {
|
|
678
|
-
type: "circle",
|
|
679
|
-
cx: num(doc, id, "cx"),
|
|
680
|
-
cy: num(doc, id, "cy")
|
|
681
|
-
};
|
|
682
|
-
case "ellipse": return {
|
|
683
|
-
type: "ellipse",
|
|
684
|
-
cx: num(doc, id, "cx"),
|
|
685
|
-
cy: num(doc, id, "cy")
|
|
686
|
-
};
|
|
687
|
-
case "line": return {
|
|
688
|
-
type: "line",
|
|
689
|
-
x1: num(doc, id, "x1"),
|
|
690
|
-
y1: num(doc, id, "y1"),
|
|
691
|
-
x2: num(doc, id, "x2"),
|
|
692
|
-
y2: num(doc, id, "y2")
|
|
693
|
-
};
|
|
694
|
-
case "polyline": return {
|
|
695
|
-
type: "polyline",
|
|
696
|
-
points: doc.get_attr(id, "points") ?? ""
|
|
697
|
-
};
|
|
698
|
-
case "polygon": return {
|
|
699
|
-
type: "polygon",
|
|
700
|
-
points: doc.get_attr(id, "points") ?? ""
|
|
701
|
-
};
|
|
702
|
-
case "path": return {
|
|
703
|
-
type: "path",
|
|
704
|
-
d: doc.get_attr(id, "d") ?? ""
|
|
705
|
-
};
|
|
706
|
-
case "text": return {
|
|
707
|
-
type: "text",
|
|
708
|
-
x: num(doc, id, "x"),
|
|
709
|
-
y: num(doc, id, "y")
|
|
710
|
-
};
|
|
711
|
-
case "tspan": return {
|
|
712
|
-
type: "tspan",
|
|
713
|
-
x: num(doc, id, "x"),
|
|
714
|
-
y: num(doc, id, "y")
|
|
715
|
-
};
|
|
716
|
-
case "image": return {
|
|
717
|
-
type: "image",
|
|
718
|
-
x: num(doc, id, "x"),
|
|
719
|
-
y: num(doc, id, "y")
|
|
720
|
-
};
|
|
721
|
-
case "use": return {
|
|
722
|
-
type: "use",
|
|
723
|
-
x: num(doc, id, "x"),
|
|
724
|
-
y: num(doc, id, "y")
|
|
725
|
-
};
|
|
726
|
-
default: return { type: "unsupported" };
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Batch variant of {@link capture_translate_baseline} — captures baselines
|
|
731
|
-
* for a set of ids into a `ReadonlyMap`. Used wherever a translate
|
|
732
|
-
* operation needs to remember the pre-translation state of multiple
|
|
733
|
-
* nodes (drag gesture, RPC, dwell detection).
|
|
734
|
-
*/
|
|
735
|
-
function capture_translate_baselines(doc, ids) {
|
|
736
|
-
const out = /* @__PURE__ */ new Map();
|
|
737
|
-
for (const id of ids) out.set(id, capture_translate_baseline(doc, id));
|
|
738
|
-
return out;
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Representative anchor point of a `TranslateBaseline` — the attribute
|
|
742
|
-
* coordinate `apply_translate` offsets. Used by callers that need to
|
|
743
|
-
* align baselines to an external lattice (pixel-grid, custom snap).
|
|
744
|
-
*
|
|
745
|
-
* Rules per element kind:
|
|
746
|
-
* - rect / text / tspan / image / use: `(x, y)`
|
|
747
|
-
* - circle / ellipse: `(cx, cy)` (no radius subtracted — consistent
|
|
748
|
-
* anchor across all kinds, not a true bounds top-left)
|
|
749
|
-
* - line / polyline / polygon: min of endpoints / points
|
|
750
|
-
* - path: first M/m command's coords (best-effort; the path-data
|
|
751
|
-
* layer would be needed for a tight bbox)
|
|
752
|
-
* - viaTransform / unsupported: `null` — no document-space anchor
|
|
753
|
-
* available without the doc itself
|
|
754
|
-
*/
|
|
755
|
-
function baseline_anchor(b) {
|
|
756
|
-
switch (b.type) {
|
|
757
|
-
case "rect":
|
|
758
|
-
case "text":
|
|
759
|
-
case "tspan":
|
|
760
|
-
case "image":
|
|
761
|
-
case "use": return {
|
|
762
|
-
x: b.x,
|
|
763
|
-
y: b.y
|
|
764
|
-
};
|
|
765
|
-
case "circle":
|
|
766
|
-
case "ellipse": return {
|
|
767
|
-
x: b.cx,
|
|
768
|
-
y: b.cy
|
|
769
|
-
};
|
|
770
|
-
case "line": return {
|
|
771
|
-
x: Math.min(b.x1, b.x2),
|
|
772
|
-
y: Math.min(b.y1, b.y2)
|
|
773
|
-
};
|
|
774
|
-
case "polyline":
|
|
775
|
-
case "polygon": return _grida_svg_parse.svg_parse.points_top_left(_grida_svg_parse.svg_parse.parse_points(b.points));
|
|
776
|
-
case "path": return _grida_svg_parse.svg_parse.parse_path_first_move(b.d);
|
|
777
|
-
case "viaTransform": {
|
|
778
|
-
const ops = parse_transform_list(b.transform);
|
|
779
|
-
if (ops === null) return null;
|
|
780
|
-
for (const op of ops) {
|
|
781
|
-
if (op.type === "translate") return {
|
|
782
|
-
x: op.tx,
|
|
783
|
-
y: op.ty
|
|
784
|
-
};
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
return null;
|
|
788
|
-
}
|
|
789
|
-
case "unsupported": return null;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Top-left of the union over a collection of `TranslateBaseline`s.
|
|
794
|
-
* Returns `null` when no baseline yields an anchor (e.g. all
|
|
795
|
-
* `viaTransform` / `unsupported`). Callers fall through (no alignment).
|
|
796
|
-
*/
|
|
797
|
-
function baseline_union_top_left(baselines) {
|
|
798
|
-
const anchors = [];
|
|
799
|
-
for (const b of baselines.values()) {
|
|
800
|
-
const p = baseline_anchor(b);
|
|
801
|
-
if (p) anchors.push(p);
|
|
802
|
-
}
|
|
803
|
-
return _grida_svg_parse.svg_parse.points_top_left(anchors);
|
|
804
|
-
}
|
|
805
|
-
function shift_points_string(points, dx, dy) {
|
|
806
|
-
if (dx === 0 && dy === 0) return points;
|
|
807
|
-
return _grida_svg_parse.svg_parse.parse_points(points).map((p) => `${p.x + dx},${p.y + dy}`).join(" ");
|
|
808
|
-
}
|
|
809
|
-
function compose_leading_translate(existing, dx, dy) {
|
|
810
|
-
if (dx === 0 && dy === 0) return existing ? existing : null;
|
|
811
|
-
if (!existing) return `translate(${dx} ${dy})`;
|
|
812
|
-
const lead = _grida_svg_parse.svg_parse.parse_leading_translate(existing);
|
|
813
|
-
if (lead) {
|
|
814
|
-
const tx = lead.tx + dx;
|
|
815
|
-
const ty = lead.ty + dy;
|
|
816
|
-
return lead.rest ? `translate(${tx} ${ty}) ${lead.rest}` : `translate(${tx} ${ty})`;
|
|
817
|
-
}
|
|
818
|
-
return `translate(${dx} ${dy}) ${existing}`;
|
|
819
|
-
}
|
|
820
|
-
function shift_path_d(d, dx, dy) {
|
|
821
|
-
if (dx === 0 && dy === 0) return d;
|
|
822
|
-
try {
|
|
823
|
-
return new _grida_svg_pathdata.SVGPathData(d).transform(_grida_svg_pathdata.SVGPathDataTransformer.TRANSLATE(dx, dy)).encode();
|
|
824
|
-
} catch {
|
|
825
|
-
return d;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
function apply_translate(doc, id, baseline, dx, dy) {
|
|
829
|
-
switch (baseline.type) {
|
|
830
|
-
case "viaTransform":
|
|
831
|
-
doc.set_attr(id, "transform", compose_leading_translate(baseline.transform ?? "", dx, dy));
|
|
832
|
-
return;
|
|
833
|
-
case "rect":
|
|
834
|
-
case "image":
|
|
835
|
-
case "use":
|
|
836
|
-
case "text":
|
|
837
|
-
case "tspan":
|
|
838
|
-
doc.set_attr(id, "x", String(baseline.x + dx));
|
|
839
|
-
doc.set_attr(id, "y", String(baseline.y + dy));
|
|
840
|
-
return;
|
|
841
|
-
case "circle":
|
|
842
|
-
case "ellipse":
|
|
843
|
-
doc.set_attr(id, "cx", String(baseline.cx + dx));
|
|
844
|
-
doc.set_attr(id, "cy", String(baseline.cy + dy));
|
|
845
|
-
return;
|
|
846
|
-
case "line":
|
|
847
|
-
doc.set_attr(id, "x1", String(baseline.x1 + dx));
|
|
848
|
-
doc.set_attr(id, "y1", String(baseline.y1 + dy));
|
|
849
|
-
doc.set_attr(id, "x2", String(baseline.x2 + dx));
|
|
850
|
-
doc.set_attr(id, "y2", String(baseline.y2 + dy));
|
|
851
|
-
return;
|
|
852
|
-
case "polyline":
|
|
853
|
-
case "polygon":
|
|
854
|
-
doc.set_attr(id, "points", shift_points_string(baseline.points, dx, dy));
|
|
855
|
-
return;
|
|
856
|
-
case "path":
|
|
857
|
-
doc.set_attr(id, "d", shift_path_d(baseline.d, dx, dy));
|
|
858
|
-
return;
|
|
859
|
-
case "unsupported": return;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
function is_resizable(tag) {
|
|
863
|
-
switch (tag) {
|
|
864
|
-
case "rect":
|
|
865
|
-
case "image":
|
|
866
|
-
case "use":
|
|
867
|
-
case "circle":
|
|
868
|
-
case "ellipse":
|
|
869
|
-
case "line":
|
|
870
|
-
case "polyline":
|
|
871
|
-
case "polygon":
|
|
872
|
-
case "path":
|
|
873
|
-
case "text": return true;
|
|
874
|
-
default: return false;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
function capture_resize_baseline(doc, id, bbox) {
|
|
878
|
-
const tag = doc.tag_of(id);
|
|
879
|
-
let attrs;
|
|
880
|
-
switch (tag) {
|
|
881
|
-
case "rect":
|
|
882
|
-
attrs = {
|
|
883
|
-
kind: "rect",
|
|
884
|
-
x: num(doc, id, "x"),
|
|
885
|
-
y: num(doc, id, "y"),
|
|
886
|
-
w: num(doc, id, "width", bbox.width),
|
|
887
|
-
h: num(doc, id, "height", bbox.height)
|
|
888
|
-
};
|
|
889
|
-
break;
|
|
890
|
-
case "image":
|
|
891
|
-
attrs = {
|
|
892
|
-
kind: "image",
|
|
893
|
-
x: num(doc, id, "x"),
|
|
894
|
-
y: num(doc, id, "y"),
|
|
895
|
-
w: num(doc, id, "width", bbox.width),
|
|
896
|
-
h: num(doc, id, "height", bbox.height)
|
|
897
|
-
};
|
|
898
|
-
break;
|
|
899
|
-
case "use":
|
|
900
|
-
attrs = {
|
|
901
|
-
kind: "use",
|
|
902
|
-
x: num(doc, id, "x"),
|
|
903
|
-
y: num(doc, id, "y"),
|
|
904
|
-
w: num(doc, id, "width", bbox.width),
|
|
905
|
-
h: num(doc, id, "height", bbox.height)
|
|
906
|
-
};
|
|
907
|
-
break;
|
|
908
|
-
case "circle":
|
|
909
|
-
attrs = {
|
|
910
|
-
kind: "circle",
|
|
911
|
-
cx: num(doc, id, "cx"),
|
|
912
|
-
cy: num(doc, id, "cy"),
|
|
913
|
-
r: num(doc, id, "r")
|
|
914
|
-
};
|
|
915
|
-
break;
|
|
916
|
-
case "ellipse":
|
|
917
|
-
attrs = {
|
|
918
|
-
kind: "ellipse",
|
|
919
|
-
cx: num(doc, id, "cx"),
|
|
920
|
-
cy: num(doc, id, "cy"),
|
|
921
|
-
rx: num(doc, id, "rx"),
|
|
922
|
-
ry: num(doc, id, "ry")
|
|
923
|
-
};
|
|
924
|
-
break;
|
|
925
|
-
case "line":
|
|
926
|
-
attrs = {
|
|
927
|
-
kind: "line",
|
|
928
|
-
x1: num(doc, id, "x1"),
|
|
929
|
-
y1: num(doc, id, "y1"),
|
|
930
|
-
x2: num(doc, id, "x2"),
|
|
931
|
-
y2: num(doc, id, "y2")
|
|
932
|
-
};
|
|
933
|
-
break;
|
|
934
|
-
case "polyline":
|
|
935
|
-
attrs = {
|
|
936
|
-
kind: "polyline",
|
|
937
|
-
points: doc.get_attr(id, "points") ?? ""
|
|
938
|
-
};
|
|
939
|
-
break;
|
|
940
|
-
case "polygon":
|
|
941
|
-
attrs = {
|
|
942
|
-
kind: "polygon",
|
|
943
|
-
points: doc.get_attr(id, "points") ?? ""
|
|
944
|
-
};
|
|
945
|
-
break;
|
|
946
|
-
case "path":
|
|
947
|
-
attrs = {
|
|
948
|
-
kind: "path",
|
|
949
|
-
d: doc.get_attr(id, "d") ?? ""
|
|
950
|
-
};
|
|
951
|
-
break;
|
|
952
|
-
case "text":
|
|
953
|
-
attrs = {
|
|
954
|
-
kind: "text",
|
|
955
|
-
x: num(doc, id, "x"),
|
|
956
|
-
y: num(doc, id, "y"),
|
|
957
|
-
fontSize: parseFloat(doc.get_attr(id, "font-size") ?? "16") || 16
|
|
958
|
-
};
|
|
959
|
-
break;
|
|
960
|
-
default: attrs = { kind: "unsupported" };
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
bbox,
|
|
964
|
-
attrs
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
function compute_resize_factors(baseline, dir, dx, dy, shift) {
|
|
968
|
-
const b = baseline.bbox;
|
|
969
|
-
let anchorX = 0;
|
|
970
|
-
let anchorY = 0;
|
|
971
|
-
let baseHX = 0;
|
|
972
|
-
let baseHY = 0;
|
|
973
|
-
let affectsX = true;
|
|
974
|
-
let affectsY = true;
|
|
975
|
-
switch (dir) {
|
|
976
|
-
case "nw":
|
|
977
|
-
anchorX = b.x + b.width;
|
|
978
|
-
anchorY = b.y + b.height;
|
|
979
|
-
baseHX = b.x;
|
|
980
|
-
baseHY = b.y;
|
|
981
|
-
break;
|
|
982
|
-
case "n":
|
|
983
|
-
anchorX = b.x + b.width / 2;
|
|
984
|
-
anchorY = b.y + b.height;
|
|
985
|
-
baseHX = b.x + b.width / 2;
|
|
986
|
-
baseHY = b.y;
|
|
987
|
-
affectsX = false;
|
|
988
|
-
break;
|
|
989
|
-
case "ne":
|
|
990
|
-
anchorX = b.x;
|
|
991
|
-
anchorY = b.y + b.height;
|
|
992
|
-
baseHX = b.x + b.width;
|
|
993
|
-
baseHY = b.y;
|
|
994
|
-
break;
|
|
995
|
-
case "e":
|
|
996
|
-
anchorX = b.x;
|
|
997
|
-
anchorY = b.y + b.height / 2;
|
|
998
|
-
baseHX = b.x + b.width;
|
|
999
|
-
baseHY = b.y + b.height / 2;
|
|
1000
|
-
affectsY = false;
|
|
1001
|
-
break;
|
|
1002
|
-
case "se":
|
|
1003
|
-
anchorX = b.x;
|
|
1004
|
-
anchorY = b.y;
|
|
1005
|
-
baseHX = b.x + b.width;
|
|
1006
|
-
baseHY = b.y + b.height;
|
|
1007
|
-
break;
|
|
1008
|
-
case "s":
|
|
1009
|
-
anchorX = b.x + b.width / 2;
|
|
1010
|
-
anchorY = b.y;
|
|
1011
|
-
baseHX = b.x + b.width / 2;
|
|
1012
|
-
baseHY = b.y + b.height;
|
|
1013
|
-
affectsX = false;
|
|
1014
|
-
break;
|
|
1015
|
-
case "sw":
|
|
1016
|
-
anchorX = b.x + b.width;
|
|
1017
|
-
anchorY = b.y;
|
|
1018
|
-
baseHX = b.x;
|
|
1019
|
-
baseHY = b.y + b.height;
|
|
1020
|
-
break;
|
|
1021
|
-
case "w":
|
|
1022
|
-
anchorX = b.x + b.width;
|
|
1023
|
-
anchorY = b.y + b.height / 2;
|
|
1024
|
-
baseHX = b.x;
|
|
1025
|
-
baseHY = b.y + b.height / 2;
|
|
1026
|
-
affectsY = false;
|
|
1027
|
-
break;
|
|
1028
|
-
}
|
|
1029
|
-
const newHX = baseHX + (affectsX ? dx : 0);
|
|
1030
|
-
const newHY = baseHY + (affectsY ? dy : 0);
|
|
1031
|
-
const denomX = baseHX - anchorX;
|
|
1032
|
-
const denomY = baseHY - anchorY;
|
|
1033
|
-
let sx = affectsX && denomX !== 0 ? (newHX - anchorX) / denomX : 1;
|
|
1034
|
-
let sy = affectsY && denomY !== 0 ? (newHY - anchorY) / denomY : 1;
|
|
1035
|
-
if (shift && affectsX && affectsY) {
|
|
1036
|
-
const mag = Math.max(Math.abs(sx), Math.abs(sy));
|
|
1037
|
-
sx = sx >= 0 ? mag : -mag;
|
|
1038
|
-
sy = sy >= 0 ? mag : -mag;
|
|
1039
|
-
}
|
|
1040
|
-
sx = Math.max(.001, sx);
|
|
1041
|
-
sy = Math.max(.001, sy);
|
|
1042
|
-
return {
|
|
1043
|
-
sx,
|
|
1044
|
-
sy,
|
|
1045
|
-
origin: {
|
|
1046
|
-
x: anchorX,
|
|
1047
|
-
y: anchorY
|
|
1048
|
-
}
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
function scale_points_string(points, origin, sx, sy) {
|
|
1052
|
-
return _grida_svg_parse.svg_parse.parse_points(points).map((p) => {
|
|
1053
|
-
return `${origin.x + (p.x - origin.x) * sx},${origin.y + (p.y - origin.y) * sy}`;
|
|
1054
|
-
}).join(" ");
|
|
1055
|
-
}
|
|
1056
|
-
function scale_path_d(d, origin, sx, sy) {
|
|
1057
|
-
try {
|
|
1058
|
-
const e = origin.x * (1 - sx);
|
|
1059
|
-
const f = origin.y * (1 - sy);
|
|
1060
|
-
return new _grida_svg_pathdata.SVGPathData(d).transform(_grida_svg_pathdata.SVGPathDataTransformer.MATRIX(sx, 0, 0, sy, e, f)).encode();
|
|
1061
|
-
} catch {
|
|
1062
|
-
return d;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
function bbox_center(points) {
|
|
1066
|
-
if (points.length === 0) return null;
|
|
1067
|
-
const r = _grida_cmath.default.rect.fromPointsOrZero(points.map((p) => [p.x, p.y]));
|
|
1068
|
-
return {
|
|
1069
|
-
cx: r.x + r.width / 2,
|
|
1070
|
-
cy: r.y + r.height / 2
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
function new_local_center(doc, id) {
|
|
1074
|
-
switch (doc.tag_of(id)) {
|
|
1075
|
-
case "rect":
|
|
1076
|
-
case "image":
|
|
1077
|
-
case "use": {
|
|
1078
|
-
const x = num(doc, id, "x");
|
|
1079
|
-
const y = num(doc, id, "y");
|
|
1080
|
-
return {
|
|
1081
|
-
cx: x + num(doc, id, "width") / 2,
|
|
1082
|
-
cy: y + num(doc, id, "height") / 2
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
case "circle":
|
|
1086
|
-
case "ellipse": return {
|
|
1087
|
-
cx: num(doc, id, "cx"),
|
|
1088
|
-
cy: num(doc, id, "cy")
|
|
1089
|
-
};
|
|
1090
|
-
case "line": {
|
|
1091
|
-
const x1 = num(doc, id, "x1");
|
|
1092
|
-
const y1 = num(doc, id, "y1");
|
|
1093
|
-
const x2 = num(doc, id, "x2");
|
|
1094
|
-
const y2 = num(doc, id, "y2");
|
|
1095
|
-
return {
|
|
1096
|
-
cx: (x1 + x2) / 2,
|
|
1097
|
-
cy: (y1 + y2) / 2
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
case "polyline":
|
|
1101
|
-
case "polygon": {
|
|
1102
|
-
const points = doc.get_attr(id, "points");
|
|
1103
|
-
if (!points) return null;
|
|
1104
|
-
return bbox_center(_grida_svg_parse.svg_parse.parse_points(points));
|
|
1105
|
-
}
|
|
1106
|
-
case "path": {
|
|
1107
|
-
const d = doc.get_attr(id, "d");
|
|
1108
|
-
if (!d) return null;
|
|
1109
|
-
return bbox_center(path_control_polyline(d));
|
|
1110
|
-
}
|
|
1111
|
-
default: return null;
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
/** Translate every local-frame coord of `id`'s geometry by (dx, dy). The
|
|
1115
|
-
* primitive arms write intrinsic attrs directly — we can't route through
|
|
1116
|
-
* `apply_translate` here because `capture_translate_baseline` returns
|
|
1117
|
-
* `viaTransform` whenever the node has a `transform=`, and that branch
|
|
1118
|
-
* prepends a `translate()` to the transform string, which would clobber
|
|
1119
|
-
* the pivot rewrite the caller is about to do. */
|
|
1120
|
-
function shift_geometry(doc, id, dx, dy) {
|
|
1121
|
-
switch (doc.tag_of(id)) {
|
|
1122
|
-
case "rect":
|
|
1123
|
-
case "image":
|
|
1124
|
-
case "use":
|
|
1125
|
-
doc.set_attr(id, "x", String(num(doc, id, "x") + dx));
|
|
1126
|
-
doc.set_attr(id, "y", String(num(doc, id, "y") + dy));
|
|
1127
|
-
return;
|
|
1128
|
-
case "circle":
|
|
1129
|
-
case "ellipse":
|
|
1130
|
-
doc.set_attr(id, "cx", String(num(doc, id, "cx") + dx));
|
|
1131
|
-
doc.set_attr(id, "cy", String(num(doc, id, "cy") + dy));
|
|
1132
|
-
return;
|
|
1133
|
-
case "line":
|
|
1134
|
-
doc.set_attr(id, "x1", String(num(doc, id, "x1") + dx));
|
|
1135
|
-
doc.set_attr(id, "y1", String(num(doc, id, "y1") + dy));
|
|
1136
|
-
doc.set_attr(id, "x2", String(num(doc, id, "x2") + dx));
|
|
1137
|
-
doc.set_attr(id, "y2", String(num(doc, id, "y2") + dy));
|
|
1138
|
-
return;
|
|
1139
|
-
case "polyline":
|
|
1140
|
-
case "polygon": {
|
|
1141
|
-
const points = doc.get_attr(id, "points");
|
|
1142
|
-
if (points) doc.set_attr(id, "points", shift_points_string(points, dx, dy));
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
case "path": {
|
|
1146
|
-
const d = doc.get_attr(id, "d");
|
|
1147
|
-
if (d) doc.set_attr(id, "d", shift_path_d(d, dx, dy));
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Commit-only. Moves the rotate pivot to the new local center and shifts
|
|
1154
|
-
* geometry by δ = (R − I) · Δc so the doc-space rendering is unchanged.
|
|
1155
|
-
* Without the shift, changing the pivot offsets the rect in doc space and
|
|
1156
|
-
* the HUD's stable-matrix chrome falls out of alignment with the element.
|
|
1157
|
-
*/
|
|
1158
|
-
function renormalize_rotate_pivot(doc, id) {
|
|
1159
|
-
const existing = doc.get_attr(id, "transform");
|
|
1160
|
-
if (existing === null || existing.indexOf("rotate") === -1) return;
|
|
1161
|
-
const ops = parse_transform_list(existing);
|
|
1162
|
-
if (ops === null) return;
|
|
1163
|
-
const cls = classify(ops);
|
|
1164
|
-
if (cls !== "single_rotate_only" && cls !== "leading_translate_then_single_rotate") return;
|
|
1165
|
-
const rot = find_op(ops, "rotate");
|
|
1166
|
-
if (!rot || rot.explicit_pivot !== true) return;
|
|
1167
|
-
const c_pre = new_local_center(doc, id);
|
|
1168
|
-
if (!c_pre) return;
|
|
1169
|
-
const dc_x = c_pre.cx - rot.cx;
|
|
1170
|
-
const dc_y = c_pre.cy - rot.cy;
|
|
1171
|
-
if (dc_x === 0 && dc_y === 0) return;
|
|
1172
|
-
const theta = rot.angle * Math.PI / 180;
|
|
1173
|
-
const cos = Math.cos(theta);
|
|
1174
|
-
const sin = Math.sin(theta);
|
|
1175
|
-
const dx = (cos - 1) * dc_x - sin * dc_y;
|
|
1176
|
-
const dy = sin * dc_x + (cos - 1) * dc_y;
|
|
1177
|
-
shift_geometry(doc, id, dx, dy);
|
|
1178
|
-
const next = recompose_with_pivot(ops, c_pre.cx + dx, c_pre.cy + dy);
|
|
1179
|
-
if (next === existing) return;
|
|
1180
|
-
doc.set_attr(id, "transform", next);
|
|
1181
|
-
}
|
|
1182
|
-
function apply_resize(doc, id, baseline, sx, sy, origin, phase = "commit") {
|
|
1183
|
-
const a = baseline.attrs;
|
|
1184
|
-
switch (a.kind) {
|
|
1185
|
-
case "rect":
|
|
1186
|
-
case "image":
|
|
1187
|
-
case "use":
|
|
1188
|
-
doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * sx));
|
|
1189
|
-
doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * sy));
|
|
1190
|
-
doc.set_attr(id, "width", String(Math.max(.001, a.w * sx)));
|
|
1191
|
-
doc.set_attr(id, "height", String(Math.max(.001, a.h * sy)));
|
|
1192
|
-
break;
|
|
1193
|
-
case "circle": {
|
|
1194
|
-
const s = Math.min(sx, sy);
|
|
1195
|
-
doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * s));
|
|
1196
|
-
doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * s));
|
|
1197
|
-
doc.set_attr(id, "r", String(Math.max(.001, a.r * s)));
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
case "ellipse":
|
|
1201
|
-
doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * sx));
|
|
1202
|
-
doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * sy));
|
|
1203
|
-
doc.set_attr(id, "rx", String(Math.max(.001, a.rx * sx)));
|
|
1204
|
-
doc.set_attr(id, "ry", String(Math.max(.001, a.ry * sy)));
|
|
1205
|
-
break;
|
|
1206
|
-
case "line":
|
|
1207
|
-
doc.set_attr(id, "x1", String(origin.x + (a.x1 - origin.x) * sx));
|
|
1208
|
-
doc.set_attr(id, "y1", String(origin.y + (a.y1 - origin.y) * sy));
|
|
1209
|
-
doc.set_attr(id, "x2", String(origin.x + (a.x2 - origin.x) * sx));
|
|
1210
|
-
doc.set_attr(id, "y2", String(origin.y + (a.y2 - origin.y) * sy));
|
|
1211
|
-
break;
|
|
1212
|
-
case "polyline":
|
|
1213
|
-
case "polygon":
|
|
1214
|
-
doc.set_attr(id, "points", scale_points_string(a.points, origin, sx, sy));
|
|
1215
|
-
break;
|
|
1216
|
-
case "path":
|
|
1217
|
-
doc.set_attr(id, "d", scale_path_d(a.d, origin, sx, sy));
|
|
1218
|
-
break;
|
|
1219
|
-
case "text": {
|
|
1220
|
-
if (!(sx !== 1 && sy !== 1)) return;
|
|
1221
|
-
const s = Math.min(sx, sy);
|
|
1222
|
-
doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * s));
|
|
1223
|
-
doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * s));
|
|
1224
|
-
doc.set_attr(id, "font-size", String(Math.max(1, a.fontSize * s)));
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
case "unsupported": return;
|
|
1228
|
-
}
|
|
1229
|
-
if (phase === "commit") renormalize_rotate_pivot(doc, id);
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Inspect a node and decide whether the rotate gesture is safe to apply.
|
|
1233
|
-
* Used by the rotate-orchestrator at gesture commit to drop the preview
|
|
1234
|
-
* with a chip rather than emit a defensible-but-noisy `transform=`.
|
|
1235
|
-
*
|
|
1236
|
-
* Order of checks matches the order the user is most likely to see; the
|
|
1237
|
-
* first failure short-circuits.
|
|
1238
|
-
*/
|
|
1239
|
-
function is_rotatable(doc, id) {
|
|
1240
|
-
const ops = parse_transform_list(doc.get_attr(id, "transform"));
|
|
1241
|
-
if (ops === null) return {
|
|
1242
|
-
kind: "refuse",
|
|
1243
|
-
reason: "non-trivial-transform"
|
|
1244
|
-
};
|
|
1245
|
-
if (classify(ops) === "mixed") return {
|
|
1246
|
-
kind: "refuse",
|
|
1247
|
-
reason: "non-trivial-transform"
|
|
1248
|
-
};
|
|
1249
|
-
if (doc.tag_of(id) === "text" || doc.tag_of(id) === "tspan") {
|
|
1250
|
-
const text_rotate = doc.get_attr(id, "rotate");
|
|
1251
|
-
if (text_rotate !== null && text_rotate.trim() !== "") return {
|
|
1252
|
-
kind: "refuse",
|
|
1253
|
-
reason: "text-with-glyph-rotate"
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
const style = doc.get_attr(id, "style");
|
|
1257
|
-
if (style && /(?:^|;)\s*transform\s*:/i.test(style)) return {
|
|
1258
|
-
kind: "refuse",
|
|
1259
|
-
reason: "css-property-transform"
|
|
1260
|
-
};
|
|
1261
|
-
for (const c of doc.children_of(id)) if (doc.is_element(c) && doc.tag_of(c) === "animateTransform") return {
|
|
1262
|
-
kind: "refuse",
|
|
1263
|
-
reason: "animated-transform"
|
|
1264
|
-
};
|
|
1265
|
-
return { kind: "yes" };
|
|
1266
|
-
}
|
|
1267
|
-
function find_op(ops, type) {
|
|
1268
|
-
for (const op of ops) if (op.type === type) return op;
|
|
1269
|
-
return null;
|
|
1270
|
-
}
|
|
1271
|
-
function capture_rotate_baseline(doc, id, pivot) {
|
|
1272
|
-
const transform = doc.get_attr(id, "transform");
|
|
1273
|
-
const ops = parse_transform_list(transform) ?? [];
|
|
1274
|
-
const lead = find_op(ops, "translate");
|
|
1275
|
-
const rot = find_op(ops, "rotate");
|
|
1276
|
-
return {
|
|
1277
|
-
transform,
|
|
1278
|
-
leading_translate: lead ? {
|
|
1279
|
-
x: lead.tx,
|
|
1280
|
-
y: lead.ty
|
|
1281
|
-
} : null,
|
|
1282
|
-
current_rotation_deg: rot?.angle ?? 0,
|
|
1283
|
-
pivot
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
function capture_rotate_baselines(doc, ids, pivot) {
|
|
1287
|
-
const out = /* @__PURE__ */ new Map();
|
|
1288
|
-
for (const id of ids) out.set(id, capture_rotate_baseline(doc, id, pivot));
|
|
1289
|
-
return out;
|
|
1290
|
-
}
|
|
1291
|
-
const RAD_TO_DEG = 180 / Math.PI;
|
|
1292
|
-
/** Snap trailing FP noise off the 10th decimal place. The rad→deg
|
|
1293
|
-
* conversion + addition produce e.g. `29.999999999999996` for what the
|
|
1294
|
-
* user dragged as "30°"; rounding to 1e-9 absorbs the noise without
|
|
1295
|
-
* losing any user-meaningful precision (SVG coordinates rarely go past
|
|
1296
|
-
* 4 decimals in authored content).
|
|
1297
|
-
*
|
|
1298
|
-
* Limits drift accumulation across a long gesture: each frame's emit
|
|
1299
|
-
* re-renders from `current_rotation_deg + delta_deg`, where the delta
|
|
1300
|
-
* is recomputed against the gesture-start anchor — not accumulated. So
|
|
1301
|
-
* noise stays bounded per-frame. */
|
|
1302
|
-
function fmt_angle(n) {
|
|
1303
|
-
return String(Math.round(n * 1e9) / 1e9);
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Compose `baseline.current_rotation_deg + degrees(angle_radians)` into a
|
|
1307
|
-
* single `rotate(θ_total cx cy)` token, preserving any leading translate
|
|
1308
|
-
* the baseline captured. Pivot is expressed in pre-translate local space
|
|
1309
|
-
* (`pivot - leading_translate`), which is the correct space for
|
|
1310
|
-
* `rotate(θ cx cy)` per SVG 1.1 §7.6.
|
|
1311
|
-
*
|
|
1312
|
-
* Identity-restore: when angle === 0 AND current_rotation === 0, restore
|
|
1313
|
-
* the original `transform=` byte-equal (may have been null). This is the
|
|
1314
|
-
* round-trip invariant the "rotated then rotated back" test locks down.
|
|
1315
|
-
*/
|
|
1316
|
-
function apply_rotate(doc, id, baseline, angle_radians) {
|
|
1317
|
-
const angle_deg = angle_radians * RAD_TO_DEG;
|
|
1318
|
-
const total = baseline.current_rotation_deg + angle_deg;
|
|
1319
|
-
if (total === 0 && angle_deg === 0) {
|
|
1320
|
-
doc.set_attr(id, "transform", baseline.transform);
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
const tx = baseline.leading_translate?.x ?? 0;
|
|
1324
|
-
const ty = baseline.leading_translate?.y ?? 0;
|
|
1325
|
-
const cx = baseline.pivot.x - tx;
|
|
1326
|
-
const cy = baseline.pivot.y - ty;
|
|
1327
|
-
const rotate_token = `rotate(${fmt_angle(total)} ${cx} ${cy})`;
|
|
1328
|
-
const str = baseline.leading_translate ? `translate(${tx} ${ty}) ${rotate_token}` : rotate_token;
|
|
1329
|
-
doc.set_attr(id, "transform", str);
|
|
1330
|
-
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Allows identity, leading-translate-only, and `(translate?) rotate(θ cx cy)`
|
|
1333
|
-
* with an explicit 3-arg pivot. Refuses 1-arg `rotate(θ)`: re-emitting it
|
|
1334
|
-
* would canonicalize the source and violate P1 round-trip.
|
|
1335
|
-
*/
|
|
1336
|
-
function is_resizable_node(doc, id) {
|
|
1337
|
-
if (!is_resizable(doc.tag_of(id))) return false;
|
|
1338
|
-
const ops = parse_transform_list(doc.get_attr(id, "transform"));
|
|
1339
|
-
if (ops === null) return false;
|
|
1340
|
-
const cls = classify(ops);
|
|
1341
|
-
if (cls === "identity" || cls === "leading_translate_only") return true;
|
|
1342
|
-
if (cls === "single_rotate_only" || cls === "leading_translate_then_single_rotate") return find_op(ops, "rotate")?.explicit_pivot === true;
|
|
1343
|
-
return false;
|
|
1344
|
-
}
|
|
1345
|
-
//#endregion
|
|
1346
|
-
//#region src/core/translate-pipeline/stages.ts
|
|
1347
|
-
/** Bridges `ctx.input.movement` (Movement) → `plan.delta` (Vec2),
|
|
1348
|
-
* collapsing the lesser axis when `axis_lock === "by_dominance"`. */
|
|
1349
|
-
const stage_axis_lock = {
|
|
1350
|
-
name: "axis_lock",
|
|
1351
|
-
run(plan, ctx) {
|
|
1352
|
-
const m = ctx.input.movement;
|
|
1353
|
-
const locked = ctx.modifiers.axis_lock === "by_dominance" ? _grida_cmath.default.ext.movement.axisLockedByDominance(m) : m;
|
|
1354
|
-
const [x, y] = _grida_cmath.default.ext.movement.normalize(locked);
|
|
1355
|
-
return { plan: {
|
|
1356
|
-
...plan,
|
|
1357
|
-
delta: {
|
|
1358
|
-
x,
|
|
1359
|
-
y
|
|
1360
|
-
}
|
|
1361
|
-
} };
|
|
1362
|
-
}
|
|
1363
|
-
};
|
|
1364
|
-
/** Consults `ctx.snap_session` for geometry-aligned correction; emits a
|
|
1365
|
-
* guide per `ctx.snap_policy`. Identity on `force_disable_snap`, missing
|
|
1366
|
-
* session, or `snap_enabled === false`. */
|
|
1367
|
-
const stage_snap = {
|
|
1368
|
-
name: "snap",
|
|
1369
|
-
run(plan, ctx) {
|
|
1370
|
-
if (ctx.modifiers.force_disable_snap) return { plan };
|
|
1371
|
-
if (!ctx.snap_session) return { plan };
|
|
1372
|
-
if (!ctx.options.snap_enabled) return { plan };
|
|
1373
|
-
const r = ctx.snap_session.snap(plan.delta, {
|
|
1374
|
-
enabled: true,
|
|
1375
|
-
threshold_px: ctx.options.snap_threshold_px
|
|
1376
|
-
}, ctx.snap_policy);
|
|
1377
|
-
return {
|
|
1378
|
-
plan: {
|
|
1379
|
-
...plan,
|
|
1380
|
-
delta: r.delta
|
|
1381
|
-
},
|
|
1382
|
-
emit: r.guide ? { guide: r.guide } : void 0
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
};
|
|
1386
|
-
/** Quantizes the agent-union origin + plan.delta to integer multiples
|
|
1387
|
-
* of `options.pixel_grid_quantum`. Anchor comes from the snap session
|
|
1388
|
-
* when open; falls back to `baseline_union_top_left` (RPC path).
|
|
1389
|
-
* Identity when quantum is `null` or `<= 0`. */
|
|
1390
|
-
const stage_pixel_grid = {
|
|
1391
|
-
name: "pixel_grid",
|
|
1392
|
-
run(plan, ctx) {
|
|
1393
|
-
const q = ctx.options.pixel_grid_quantum;
|
|
1394
|
-
if (q === null || q <= 0) return { plan };
|
|
1395
|
-
const anchor = ctx.snap_session?.baseline_union_readonly ?? baseline_union_top_left(plan.baselines);
|
|
1396
|
-
if (!anchor) return { plan };
|
|
1397
|
-
const qx = Math.round((anchor.x + plan.delta.x) / q) * q - anchor.x;
|
|
1398
|
-
const qy = Math.round((anchor.y + plan.delta.y) / q) * q - anchor.y;
|
|
1399
|
-
return { plan: {
|
|
1400
|
-
...plan,
|
|
1401
|
-
delta: {
|
|
1402
|
-
x: qx,
|
|
1403
|
-
y: qy
|
|
1404
|
-
}
|
|
1405
|
-
} };
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
const STAGES_DEFAULT$1 = Object.freeze([
|
|
1409
|
-
stage_axis_lock,
|
|
1410
|
-
stage_snap,
|
|
1411
|
-
stage_pixel_grid
|
|
1412
|
-
]);
|
|
1413
|
-
const STAGES_NUDGE = Object.freeze([stage_axis_lock, stage_pixel_grid]);
|
|
1414
|
-
const STAGES_RPC$1 = Object.freeze([stage_axis_lock]);
|
|
1415
|
-
//#endregion
|
|
1416
|
-
//#region src/core/translate-pipeline/apply.ts
|
|
1417
|
-
/** Apply the plan: for each id, run `apply_translate` with the
|
|
1418
|
-
* baseline + world-space delta. Does NOT emit; caller wraps with
|
|
1419
|
-
* history machinery and calls `emit()` after. */
|
|
1420
|
-
function applyTranslatePlan(doc, plan) {
|
|
1421
|
-
for (const id of plan.ids) {
|
|
1422
|
-
const baseline = plan.baselines.get(id);
|
|
1423
|
-
if (!baseline) continue;
|
|
1424
|
-
apply_translate(doc, id, baseline, plan.delta.x, plan.delta.y);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
/** Reset each id to its baseline (delta = 0). Used by undo closures. */
|
|
1428
|
-
function revertTranslatePlan(doc, plan) {
|
|
1429
|
-
for (const id of plan.ids) {
|
|
1430
|
-
const baseline = plan.baselines.get(id);
|
|
1431
|
-
if (!baseline) continue;
|
|
1432
|
-
apply_translate(doc, id, baseline, 0, 0);
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
/** Prepare a one-shot, headless translate. Captures baselines, runs
|
|
1436
|
-
* the pipeline with `stages` (default `STAGES_RPC`), returns ready-to-
|
|
1437
|
-
* record closures. Caller wraps in history (e.g. `history.atomic`).
|
|
1438
|
-
* See `./README.md` for the per-caller stage lists. */
|
|
1439
|
-
function prepare_translate_rpc(args) {
|
|
1440
|
-
const { doc, ids, delta, options, emit, stages = STAGES_RPC$1 } = args;
|
|
1441
|
-
const filtered_ids = doc.prune_nested_nodes(ids);
|
|
1442
|
-
const plan0 = {
|
|
1443
|
-
ids: filtered_ids,
|
|
1444
|
-
baselines: capture_translate_baselines(doc, filtered_ids),
|
|
1445
|
-
delta: {
|
|
1446
|
-
x: 0,
|
|
1447
|
-
y: 0
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
1450
|
-
const { plan } = run_translate_pipeline(plan0, stages, {
|
|
1451
|
-
input: {
|
|
1452
|
-
ids: plan0.ids,
|
|
1453
|
-
movement: [delta.x, delta.y]
|
|
1454
|
-
},
|
|
1455
|
-
modifiers: {
|
|
1456
|
-
axis_lock: "off",
|
|
1457
|
-
force_disable_snap: true
|
|
1458
|
-
},
|
|
1459
|
-
options,
|
|
1460
|
-
snap_session: null,
|
|
1461
|
-
snap_policy: "engine"
|
|
1462
|
-
});
|
|
1463
|
-
return {
|
|
1464
|
-
plan,
|
|
1465
|
-
apply: () => {
|
|
1466
|
-
applyTranslatePlan(doc, plan);
|
|
1467
|
-
emit();
|
|
1468
|
-
},
|
|
1469
|
-
revert: () => {
|
|
1470
|
-
revertTranslatePlan(doc, plan);
|
|
1471
|
-
emit();
|
|
1472
|
-
}
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
//#endregion
|
|
1476
|
-
//#region src/core/translate-pipeline/orchestrator.ts
|
|
1477
|
-
const PROVIDER_ID$1 = "svg-editor";
|
|
1478
|
-
var TranslateOrchestrator = class {
|
|
1479
|
-
constructor(deps) {
|
|
1480
|
-
this.deps = deps;
|
|
1481
|
-
this.active = null;
|
|
1482
|
-
this._last_guides = [];
|
|
1483
|
-
}
|
|
1484
|
-
/** Guides emitted by the most recent pipeline run. Cleared on
|
|
1485
|
-
* cancel/dispose. HUD compositors read this to draw snap chrome. */
|
|
1486
|
-
get last_guides() {
|
|
1487
|
-
return this._last_guides;
|
|
1488
|
-
}
|
|
1489
|
-
/** True while a gesture session is open. */
|
|
1490
|
-
has_active_session() {
|
|
1491
|
-
return this.active !== null;
|
|
1492
|
-
}
|
|
1493
|
-
/** Per-frame drive: lazily opens a session on first call, runs the
|
|
1494
|
-
* pipeline, writes apply/revert into the preview, and commits when
|
|
1495
|
-
* `opts.phase === "commit"`. */
|
|
1496
|
-
drive(input, modifiers, opts) {
|
|
1497
|
-
if (this.active === null) this.active = this.open(input.ids, opts.snap, opts.label ?? "move");
|
|
1498
|
-
const session = this.active;
|
|
1499
|
-
const stages = opts.stages ?? STAGES_DEFAULT$1;
|
|
1500
|
-
const result = this.run_pass(session, input.movement, modifiers, opts.policy, stages);
|
|
1501
|
-
session.last_movement = input.movement;
|
|
1502
|
-
session.last_policy = opts.policy;
|
|
1503
|
-
session.last_stages = stages;
|
|
1504
|
-
this.write_preview_delta(session, result.plan);
|
|
1505
|
-
if (opts.phase === "commit") {
|
|
1506
|
-
session.preview.commit();
|
|
1507
|
-
this.dispose_session();
|
|
1508
|
-
}
|
|
1509
|
-
return result;
|
|
1510
|
-
}
|
|
1511
|
-
/** Re-run the current preview frame with new modifiers, reusing the
|
|
1512
|
-
* last-known movement / policy / stages. Used when a modifier key
|
|
1513
|
-
* changes between pointer-move events (Shift down/up mid-drag).
|
|
1514
|
-
* No-op when no session is active. */
|
|
1515
|
-
redrive_modifiers(modifiers) {
|
|
1516
|
-
if (!this.active) return null;
|
|
1517
|
-
const session = this.active;
|
|
1518
|
-
const result = this.run_pass(session, session.last_movement, modifiers, session.last_policy, session.last_stages);
|
|
1519
|
-
this.write_preview_delta(session, result.plan);
|
|
1520
|
-
return result;
|
|
1521
|
-
}
|
|
1522
|
-
/** Cancel an in-flight gesture (Escape, programmatic abort). */
|
|
1523
|
-
cancel() {
|
|
1524
|
-
if (!this.active) return;
|
|
1525
|
-
this.active.preview.discard();
|
|
1526
|
-
this.dispose_session();
|
|
1527
|
-
}
|
|
1528
|
-
/** Build a plan + context, run the pipeline, stash guides. Pure
|
|
1529
|
-
* computation — does not touch the preview. */
|
|
1530
|
-
run_pass(session, movement, modifiers, policy, stages) {
|
|
1531
|
-
const result = run_translate_pipeline({
|
|
1532
|
-
ids: session.ids,
|
|
1533
|
-
baselines: session.baselines,
|
|
1534
|
-
delta: {
|
|
1535
|
-
x: 0,
|
|
1536
|
-
y: 0
|
|
1537
|
-
}
|
|
1538
|
-
}, stages, {
|
|
1539
|
-
input: {
|
|
1540
|
-
ids: session.ids,
|
|
1541
|
-
movement
|
|
1542
|
-
},
|
|
1543
|
-
modifiers,
|
|
1544
|
-
options: this.deps.options(),
|
|
1545
|
-
snap_session: session.snap,
|
|
1546
|
-
snap_policy: policy
|
|
1547
|
-
});
|
|
1548
|
-
this._last_guides = result.guides;
|
|
1549
|
-
return result;
|
|
1550
|
-
}
|
|
1551
|
-
open(ids, snap, label) {
|
|
1552
|
-
const doc = this.deps.get_doc();
|
|
1553
|
-
const filtered = doc.prune_nested_nodes(ids);
|
|
1554
|
-
return {
|
|
1555
|
-
ids: filtered,
|
|
1556
|
-
baselines: capture_translate_baselines(doc, filtered),
|
|
1557
|
-
snap: snap ? this.deps.open_snap(filtered) : null,
|
|
1558
|
-
preview: this.deps.open_preview(label),
|
|
1559
|
-
last_movement: [0, 0],
|
|
1560
|
-
last_policy: "engine",
|
|
1561
|
-
last_stages: STAGES_DEFAULT$1
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
/** Bind a fresh apply/revert pair (closure over `plan`) into the
|
|
1565
|
-
* preview slot. Called from both `drive` (per pointer frame) and
|
|
1566
|
-
* `redrive_modifiers` (on modifier flip). */
|
|
1567
|
-
write_preview_delta(session, plan) {
|
|
1568
|
-
const doc = this.deps.get_doc();
|
|
1569
|
-
const emit = this.deps.emit;
|
|
1570
|
-
session.preview.set({
|
|
1571
|
-
providerId: PROVIDER_ID$1,
|
|
1572
|
-
apply: () => {
|
|
1573
|
-
applyTranslatePlan(doc, plan);
|
|
1574
|
-
emit();
|
|
1575
|
-
},
|
|
1576
|
-
revert: () => {
|
|
1577
|
-
revertTranslatePlan(doc, plan);
|
|
1578
|
-
emit();
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
dispose_session() {
|
|
1583
|
-
if (!this.active) return;
|
|
1584
|
-
this.active.snap?.dispose();
|
|
1585
|
-
this.active = null;
|
|
1586
|
-
this._last_guides = [];
|
|
1587
|
-
}
|
|
1588
|
-
};
|
|
1589
|
-
//#endregion
|
|
1590
|
-
//#region src/core/translate-pipeline/nudge-dwell-watcher.ts
|
|
1591
|
-
/** Hold-time after the last firing detection. Show is immediate (next
|
|
1592
|
-
* frame); only the hide edge is delayed. */
|
|
1593
|
-
const HIDE_MS = 500;
|
|
1594
|
-
var NudgeDwellWatcher = class {
|
|
1595
|
-
constructor(deps) {
|
|
1596
|
-
this.deps = deps;
|
|
1597
|
-
this._guides = [];
|
|
1598
|
-
this.raf_id = null;
|
|
1599
|
-
this.hide_timer = null;
|
|
1600
|
-
this.unsubscribe = deps.editor.subscribe_translate_commit(() => this.schedule_detect());
|
|
1601
|
-
}
|
|
1602
|
-
/** Currently-published dwell guides. Empty between detections. */
|
|
1603
|
-
get guides() {
|
|
1604
|
-
return this._guides;
|
|
1605
|
-
}
|
|
1606
|
-
/** Drop any pending detection / held guide. Idempotent. */
|
|
1607
|
-
cancel_pending() {
|
|
1608
|
-
this.clear_raf();
|
|
1609
|
-
this.clear_hide();
|
|
1610
|
-
this.publish_guides([]);
|
|
1611
|
-
}
|
|
1612
|
-
dispose() {
|
|
1613
|
-
this.unsubscribe();
|
|
1614
|
-
this.cancel_pending();
|
|
1615
|
-
}
|
|
1616
|
-
schedule_detect() {
|
|
1617
|
-
if (this.raf_id !== null) return;
|
|
1618
|
-
this.raf_id = this.deps.window.requestAnimationFrame(() => {
|
|
1619
|
-
this.raf_id = null;
|
|
1620
|
-
this.detect();
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
detect() {
|
|
1624
|
-
const ids = this.deps.editor.state.selection;
|
|
1625
|
-
if (ids.length === 0) {
|
|
1626
|
-
this.publish_guides([]);
|
|
1627
|
-
this.clear_hide();
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
const snap = this.deps.open_snap(ids);
|
|
1631
|
-
if (!snap) {
|
|
1632
|
-
this.publish_guides([]);
|
|
1633
|
-
this.clear_hide();
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
try {
|
|
1637
|
-
const plan0 = {
|
|
1638
|
-
ids: [...ids],
|
|
1639
|
-
baselines: capture_translate_baselines(this.deps.editor.document, ids),
|
|
1640
|
-
delta: {
|
|
1641
|
-
x: 0,
|
|
1642
|
-
y: 0
|
|
1643
|
-
}
|
|
1644
|
-
};
|
|
1645
|
-
const result = run_translate_pipeline(plan0, STAGES_DEFAULT$1, {
|
|
1646
|
-
input: {
|
|
1647
|
-
ids: plan0.ids,
|
|
1648
|
-
movement: [0, 0]
|
|
1649
|
-
},
|
|
1650
|
-
modifiers: {
|
|
1651
|
-
axis_lock: "off",
|
|
1652
|
-
force_disable_snap: false
|
|
1653
|
-
},
|
|
1654
|
-
options: this.deps.options(),
|
|
1655
|
-
snap_session: snap,
|
|
1656
|
-
snap_policy: "aligned"
|
|
1657
|
-
});
|
|
1658
|
-
if (result.guides.length === 0) {
|
|
1659
|
-
this.publish_guides([]);
|
|
1660
|
-
this.clear_hide();
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
this.publish_guides(result.guides);
|
|
1664
|
-
this.arm_hide();
|
|
1665
|
-
} finally {
|
|
1666
|
-
snap.dispose();
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
arm_hide() {
|
|
1670
|
-
this.clear_hide();
|
|
1671
|
-
this.hide_timer = this.deps.window.setTimeout(() => {
|
|
1672
|
-
this.hide_timer = null;
|
|
1673
|
-
this.publish_guides([]);
|
|
1674
|
-
}, HIDE_MS);
|
|
1675
|
-
}
|
|
1676
|
-
publish_guides(next) {
|
|
1677
|
-
if (next.length === 0 && this._guides.length === 0) return;
|
|
1678
|
-
this._guides = next;
|
|
1679
|
-
this.deps.on_guides_change();
|
|
1680
|
-
}
|
|
1681
|
-
clear_raf() {
|
|
1682
|
-
if (this.raf_id === null) return;
|
|
1683
|
-
this.deps.window.cancelAnimationFrame(this.raf_id);
|
|
1684
|
-
this.raf_id = null;
|
|
1685
|
-
}
|
|
1686
|
-
clear_hide() {
|
|
1687
|
-
if (this.hide_timer === null) return;
|
|
1688
|
-
this.deps.window.clearTimeout(this.hide_timer);
|
|
1689
|
-
this.hide_timer = null;
|
|
1690
|
-
}
|
|
1691
|
-
};
|
|
1692
|
-
//#endregion
|
|
1693
|
-
//#region src/core/rotate-pipeline/pipeline.ts
|
|
1694
|
-
/** The funnel. Threads `plan` through `stages` in order. Pure: same
|
|
1695
|
-
* inputs → same outputs. */
|
|
1696
|
-
function run_rotate_pipeline(init, stages, _ctx) {
|
|
1697
|
-
let plan = init;
|
|
1698
|
-
for (const stage of stages) plan = stage.run(plan, _ctx).plan;
|
|
1699
|
-
return { plan };
|
|
1700
|
-
}
|
|
1701
|
-
/** Default stage list for HUD-driven rotate gestures (drag). */
|
|
1702
|
-
const STAGES_DEFAULT = Object.freeze([{
|
|
1703
|
-
name: "angle_snap",
|
|
1704
|
-
run(plan, ctx) {
|
|
1705
|
-
if (ctx.modifiers.force_disable_snap) return { plan };
|
|
1706
|
-
if (ctx.modifiers.angle_snap !== "step") return { plan };
|
|
1707
|
-
const step = ctx.options.angle_snap_step_radians;
|
|
1708
|
-
if (step === null || step <= 0) return { plan };
|
|
1709
|
-
const snapped = Math.round(plan.angle_radians / step) * step;
|
|
1710
|
-
return { plan: {
|
|
1711
|
-
...plan,
|
|
1712
|
-
angle_radians: snapped
|
|
1713
|
-
} };
|
|
1714
|
-
}
|
|
1715
|
-
}]);
|
|
1716
|
-
/** Stage list for headless RPC paths (`commands.rotate`, `rotate_to`).
|
|
1717
|
-
* No snap — the caller passed an exact angle on purpose. */
|
|
1718
|
-
const STAGES_RPC = Object.freeze([]);
|
|
1719
|
-
//#endregion
|
|
1720
|
-
//#region src/core/rotate-pipeline/apply.ts
|
|
1721
|
-
function applyRotatePlan(doc, plan) {
|
|
1722
|
-
for (const m of plan.members) apply_rotate(doc, m.id, m.baseline, plan.angle_radians);
|
|
1723
|
-
}
|
|
1724
|
-
/** Reset each member to its baseline rotation (angle_radians = 0). Used
|
|
1725
|
-
* by undo closures. Identity-restore byte-equality kicks in when the
|
|
1726
|
-
* baseline had no pre-existing rotation. */
|
|
1727
|
-
function revertRotatePlan(doc, plan) {
|
|
1728
|
-
for (const m of plan.members) apply_rotate(doc, m.id, m.baseline, 0);
|
|
1729
|
-
}
|
|
1730
|
-
/** Headless one-shot rotate. Captures per-member baselines around the
|
|
1731
|
-
* shared pivot, runs the pipeline with `stages` (default `STAGES_RPC`),
|
|
1732
|
-
* returns ready-to-record closures + per-member refusal verdicts. The
|
|
1733
|
-
* caller wraps the closures in history (e.g. `history.atomic`).
|
|
1734
|
-
*
|
|
1735
|
-
* Verdicts: if any member returns `kind: "refuse"`, the caller SHOULD
|
|
1736
|
-
* drop the gesture and emit a chip instead of committing. The `apply`
|
|
1737
|
-
* closure here doesn't gate on verdicts — that's a policy decision at
|
|
1738
|
-
* the call site (a programmatic `commands.rotate` may still want to
|
|
1739
|
-
* force-write through; the interactive path does not). */
|
|
1740
|
-
function prepare_rotate_rpc(args) {
|
|
1741
|
-
const { doc, ids, pivot, angle_radians, options, emit, stages = STAGES_RPC } = args;
|
|
1742
|
-
const filtered_ids = doc.prune_nested_nodes(ids);
|
|
1743
|
-
const baselines = capture_rotate_baselines(doc, filtered_ids, pivot);
|
|
1744
|
-
const members = filtered_ids.map((id) => ({
|
|
1745
|
-
id,
|
|
1746
|
-
baseline: baselines.get(id)
|
|
1747
|
-
}));
|
|
1748
|
-
const verdicts = /* @__PURE__ */ new Map();
|
|
1749
|
-
for (const id of filtered_ids) verdicts.set(id, is_rotatable(doc, id));
|
|
1750
|
-
const { plan } = run_rotate_pipeline({
|
|
1751
|
-
members,
|
|
1752
|
-
pivot,
|
|
1753
|
-
angle_radians
|
|
1754
|
-
}, stages, {
|
|
1755
|
-
input: {
|
|
1756
|
-
ids: filtered_ids,
|
|
1757
|
-
angle_radians
|
|
1758
|
-
},
|
|
1759
|
-
modifiers: {
|
|
1760
|
-
angle_snap: "off",
|
|
1761
|
-
force_disable_snap: true
|
|
1762
|
-
},
|
|
1763
|
-
options
|
|
1764
|
-
});
|
|
1765
|
-
return {
|
|
1766
|
-
plan,
|
|
1767
|
-
verdicts,
|
|
1768
|
-
apply: () => {
|
|
1769
|
-
applyRotatePlan(doc, plan);
|
|
1770
|
-
emit();
|
|
1771
|
-
},
|
|
1772
|
-
revert: () => {
|
|
1773
|
-
revertRotatePlan(doc, plan);
|
|
1774
|
-
emit();
|
|
1775
|
-
}
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
//#endregion
|
|
1779
|
-
//#region src/core/rotate-pipeline/orchestrator.ts
|
|
1780
|
-
const PROVIDER_ID = "svg-editor";
|
|
1781
|
-
function ids_key(ids) {
|
|
1782
|
-
return [...ids].sort().join("\0");
|
|
1783
|
-
}
|
|
1784
|
-
var RotateOrchestrator = class {
|
|
1785
|
-
constructor(deps) {
|
|
1786
|
-
this.deps = deps;
|
|
1787
|
-
this.active = null;
|
|
1788
|
-
}
|
|
1789
|
-
has_active_session() {
|
|
1790
|
-
return this.active !== null;
|
|
1791
|
-
}
|
|
1792
|
-
is_active_for(ids) {
|
|
1793
|
-
return this.active !== null && this.active.ids_key === ids_key(ids);
|
|
1794
|
-
}
|
|
1795
|
-
/** Per-frame drive. Opens a session lazily on the first call. Returns
|
|
1796
|
-
* `null` when `ids` is empty. On commit, returns a `RotateCommitOutcome`
|
|
1797
|
-
* the caller uses to surface refusal chips. */
|
|
1798
|
-
drive(input, modifiers, opts) {
|
|
1799
|
-
if (input.ids.length === 0) return null;
|
|
1800
|
-
const key = ids_key(input.ids);
|
|
1801
|
-
if (this.active && this.active.ids_key !== key) {
|
|
1802
|
-
this.active.preview.discard();
|
|
1803
|
-
this.dispose_session();
|
|
1804
|
-
}
|
|
1805
|
-
if (this.active === null) this.active = this.open(input.ids, opts.label ?? "rotate");
|
|
1806
|
-
const session = this.active;
|
|
1807
|
-
const stages = opts.stages ?? STAGES_DEFAULT;
|
|
1808
|
-
const result = this.run_pass(session, input.angle_radians, modifiers, stages);
|
|
1809
|
-
session.last_angle = input.angle_radians;
|
|
1810
|
-
session.last_stages = stages;
|
|
1811
|
-
this.write_preview(session, result.plan);
|
|
1812
|
-
if (opts.phase === "commit") {
|
|
1813
|
-
let outcome;
|
|
1814
|
-
let refused = false;
|
|
1815
|
-
for (const v of session.verdicts.values()) if (v.kind === "refuse") {
|
|
1816
|
-
refused = true;
|
|
1817
|
-
break;
|
|
1818
|
-
}
|
|
1819
|
-
if (refused) {
|
|
1820
|
-
session.preview.discard();
|
|
1821
|
-
outcome = {
|
|
1822
|
-
kind: "refused",
|
|
1823
|
-
verdicts: session.verdicts
|
|
1824
|
-
};
|
|
1825
|
-
} else {
|
|
1826
|
-
session.preview.commit();
|
|
1827
|
-
outcome = {
|
|
1828
|
-
kind: "committed",
|
|
1829
|
-
plan: result.plan
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
this.dispose_session();
|
|
1833
|
-
return {
|
|
1834
|
-
result,
|
|
1835
|
-
outcome
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
return {
|
|
1839
|
-
result,
|
|
1840
|
-
outcome: null
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
redrive_modifiers(modifiers) {
|
|
1844
|
-
if (!this.active) return null;
|
|
1845
|
-
const session = this.active;
|
|
1846
|
-
const result = this.run_pass(session, session.last_angle, modifiers, session.last_stages);
|
|
1847
|
-
this.write_preview(session, result.plan);
|
|
1848
|
-
return result;
|
|
1849
|
-
}
|
|
1850
|
-
cancel() {
|
|
1851
|
-
if (!this.active) return;
|
|
1852
|
-
this.active.preview.discard();
|
|
1853
|
-
this.dispose_session();
|
|
1854
|
-
}
|
|
1855
|
-
run_pass(session, angle_radians, modifiers, stages) {
|
|
1856
|
-
return run_rotate_pipeline({
|
|
1857
|
-
members: session.members,
|
|
1858
|
-
pivot: session.pivot,
|
|
1859
|
-
angle_radians
|
|
1860
|
-
}, stages, {
|
|
1861
|
-
input: {
|
|
1862
|
-
ids: session.members.map((m) => m.id),
|
|
1863
|
-
angle_radians
|
|
1864
|
-
},
|
|
1865
|
-
modifiers,
|
|
1866
|
-
options: this.deps.options()
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
open(ids, label) {
|
|
1870
|
-
const doc = this.deps.get_doc();
|
|
1871
|
-
const filtered = doc.prune_nested_nodes(ids);
|
|
1872
|
-
const pivot = compute_union_center(filtered, this.deps.bbox_world);
|
|
1873
|
-
const baselines = capture_rotate_baselines(doc, filtered, pivot);
|
|
1874
|
-
const members = filtered.map((id) => ({
|
|
1875
|
-
id,
|
|
1876
|
-
baseline: baselines.get(id)
|
|
1877
|
-
}));
|
|
1878
|
-
const verdicts = /* @__PURE__ */ new Map();
|
|
1879
|
-
for (const id of filtered) verdicts.set(id, is_rotatable(doc, id));
|
|
1880
|
-
return {
|
|
1881
|
-
ids_key: ids_key(ids),
|
|
1882
|
-
members,
|
|
1883
|
-
pivot,
|
|
1884
|
-
verdicts,
|
|
1885
|
-
preview: this.deps.open_preview(label),
|
|
1886
|
-
last_angle: 0,
|
|
1887
|
-
last_stages: STAGES_DEFAULT
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
write_preview(session, plan) {
|
|
1891
|
-
const doc = this.deps.get_doc();
|
|
1892
|
-
const emit = this.deps.emit;
|
|
1893
|
-
session.preview.set({
|
|
1894
|
-
providerId: PROVIDER_ID,
|
|
1895
|
-
apply: () => {
|
|
1896
|
-
applyRotatePlan(doc, plan);
|
|
1897
|
-
emit();
|
|
1898
|
-
},
|
|
1899
|
-
revert: () => {
|
|
1900
|
-
revertRotatePlan(doc, plan);
|
|
1901
|
-
emit();
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
dispose_session() {
|
|
1906
|
-
this.active = null;
|
|
1907
|
-
}
|
|
1908
|
-
};
|
|
1909
|
-
function compute_union_center(ids, bbox_world) {
|
|
1910
|
-
const rects = ids.map(bbox_world);
|
|
1911
|
-
const u = _grida_cmath.default.rect.union(rects);
|
|
1912
|
-
return {
|
|
1913
|
-
x: u.x + u.width / 2,
|
|
1914
|
-
y: u.y + u.height / 2
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
//#endregion
|
|
1918
|
-
//#region src/core/paint.ts
|
|
1919
|
-
/**
|
|
1920
|
-
* Parse a *computed* paint string into the discriminated union. Returns null
|
|
1921
|
-
* for `inherit` / `var()` / empty. Returns an invalid-computed-value record
|
|
1922
|
-
* for syntactic errors (rare; we're permissive).
|
|
1923
|
-
*/
|
|
1924
|
-
function parse_paint(declared) {
|
|
1925
|
-
if (declared === null || declared === "") return null;
|
|
1926
|
-
const trimmed = declared.trim();
|
|
1927
|
-
if (trimmed === "") return null;
|
|
1928
|
-
if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
|
|
1929
|
-
if (/^var\s*\(/i.test(trimmed)) return {
|
|
1930
|
-
error: "invalid_at_computed_value_time",
|
|
1931
|
-
reason: "var() substitution requires a cascade engine (not implemented)"
|
|
1932
|
-
};
|
|
1933
|
-
if (trimmed === "none") return { kind: "none" };
|
|
1934
|
-
if (trimmed === "context-fill" || trimmed === "contextFill") return { kind: "context_fill" };
|
|
1935
|
-
if (trimmed === "context-stroke" || trimmed === "contextStroke") return { kind: "context_stroke" };
|
|
1936
|
-
const url_match = trimmed.match(/^url\(\s*(["']?)#([^)"']+)\1\s*\)\s*(.*)$/i);
|
|
1937
|
-
if (url_match) {
|
|
1938
|
-
const id = url_match[2];
|
|
1939
|
-
const rest = url_match[3].trim();
|
|
1940
|
-
let fallback;
|
|
1941
|
-
if (rest !== "") {
|
|
1942
|
-
const f = parse_paint(rest);
|
|
1943
|
-
if (f && f.kind === "none") fallback = { kind: "none" };
|
|
1944
|
-
else if (f && f.kind === "color") fallback = {
|
|
1945
|
-
kind: "color",
|
|
1946
|
-
value: f.value
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
return fallback ? {
|
|
1950
|
-
kind: "ref",
|
|
1951
|
-
id,
|
|
1952
|
-
fallback
|
|
1953
|
-
} : {
|
|
1954
|
-
kind: "ref",
|
|
1955
|
-
id
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
if (/^currentcolor$/i.test(trimmed)) return {
|
|
1959
|
-
kind: "color",
|
|
1960
|
-
value: { kind: "current_color" }
|
|
1961
|
-
};
|
|
1962
|
-
return {
|
|
1963
|
-
kind: "color",
|
|
1964
|
-
value: {
|
|
1965
|
-
kind: "rgb",
|
|
1966
|
-
value: trimmed
|
|
1967
|
-
}
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
/** Serialize a Paint back to an SVG attribute / inline-style value. */
|
|
1971
|
-
function serialize_paint(paint) {
|
|
1972
|
-
switch (paint.kind) {
|
|
1973
|
-
case "none": return "none";
|
|
1974
|
-
case "context_fill": return "context-fill";
|
|
1975
|
-
case "context_stroke": return "context-stroke";
|
|
1976
|
-
case "color": return paint.value.kind === "current_color" ? "currentColor" : paint.value.value;
|
|
1977
|
-
case "ref":
|
|
1978
|
-
if (paint.fallback) {
|
|
1979
|
-
const f = paint.fallback.kind === "none" ? "none" : paint.fallback.value.kind === "current_color" ? "currentColor" : paint.fallback.value.value;
|
|
1980
|
-
return `url(#${paint.id}) ${f}`;
|
|
1981
|
-
}
|
|
1982
|
-
return `url(#${paint.id})`;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
//#endregion
|
|
1986
|
-
//#region src/types.ts
|
|
1987
|
-
const TOOL_CURSOR = { type: "cursor" };
|
|
1988
|
-
const DEFAULT_STYLE = {
|
|
1989
|
-
chrome_color: "#2563eb",
|
|
1990
|
-
handle_size: 8,
|
|
1991
|
-
handle_fill: "#ffffff",
|
|
1992
|
-
handle_stroke: "#2563eb",
|
|
1993
|
-
endpoint_dot_radius: 5,
|
|
1994
|
-
selection_outline_width: 2,
|
|
1995
|
-
measurement_color: "#ff3a30",
|
|
1996
|
-
show_size_meter: true,
|
|
1997
|
-
snap_enabled: true,
|
|
1998
|
-
snap_threshold_px: 6,
|
|
1999
|
-
hit_tolerance_px: 0,
|
|
2000
|
-
snap_to_pixel_grid: false,
|
|
2001
|
-
pixel_grid_size: 1,
|
|
2002
|
-
pixel_grid: true,
|
|
2003
|
-
angle_snap_step_radians: Math.PI / 12
|
|
2004
|
-
};
|
|
2005
|
-
/** v1 default fill — gray so newly-drawn shapes are visible against white.
|
|
2006
|
-
* Matches the main canvas convention. A future preset may override. */
|
|
2007
|
-
const DEFAULT_FILL = "#D9D9D9";
|
|
2008
|
-
/**
|
|
2009
|
-
* Initial attrs for the moment of pointer-down — zero-size at the click
|
|
2010
|
-
* point. Seeds the pending node before any drag movement so the HUD
|
|
2011
|
-
* selection chrome can render a (zero-size) box without a flicker.
|
|
2012
|
-
*
|
|
2013
|
-
* Per tag:
|
|
2014
|
-
* - rect → x = px, y = py, width = 0, height = 0
|
|
2015
|
-
* - ellipse → cx = px, cy = py, rx = 0, ry = 0
|
|
2016
|
-
* - line → x1 = x2 = px, y1 = y2 = py
|
|
2017
|
-
*/
|
|
2018
|
-
function initial_attrs(tag, point) {
|
|
2019
|
-
switch (tag) {
|
|
2020
|
-
case "rect": return {
|
|
2021
|
-
x: fmt(point.x),
|
|
2022
|
-
y: fmt(point.y),
|
|
2023
|
-
width: "0",
|
|
2024
|
-
height: "0"
|
|
2025
|
-
};
|
|
2026
|
-
case "ellipse": return {
|
|
2027
|
-
cx: fmt(point.x),
|
|
2028
|
-
cy: fmt(point.y),
|
|
2029
|
-
rx: "0",
|
|
2030
|
-
ry: "0"
|
|
2031
|
-
};
|
|
2032
|
-
case "line": return {
|
|
2033
|
-
x1: fmt(point.x),
|
|
2034
|
-
y1: fmt(point.y),
|
|
2035
|
-
x2: fmt(point.x),
|
|
2036
|
-
y2: fmt(point.y)
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
/**
|
|
2041
|
-
* Attrs for click-no-drag commit — default-sized shape centered on the
|
|
2042
|
-
* click point. (Rect's `x`/`y` are top-left in SVG, so we offset by
|
|
2043
|
-
* `size/2` to center; ellipse's `cx`/`cy` are already center.)
|
|
2044
|
-
*/
|
|
2045
|
-
function default_attrs(tag, point, size = 100) {
|
|
2046
|
-
switch (tag) {
|
|
2047
|
-
case "rect": return {
|
|
2048
|
-
x: fmt(point.x - size / 2),
|
|
2049
|
-
y: fmt(point.y - size / 2),
|
|
2050
|
-
width: fmt(size),
|
|
2051
|
-
height: fmt(size)
|
|
2052
|
-
};
|
|
2053
|
-
case "ellipse": return {
|
|
2054
|
-
cx: fmt(point.x),
|
|
2055
|
-
cy: fmt(point.y),
|
|
2056
|
-
rx: fmt(size / 2),
|
|
2057
|
-
ry: fmt(size / 2)
|
|
2058
|
-
};
|
|
2059
|
-
case "line": return {
|
|
2060
|
-
x1: fmt(point.x - size / 2),
|
|
2061
|
-
y1: fmt(point.y),
|
|
2062
|
-
x2: fmt(point.x + size / 2),
|
|
2063
|
-
y2: fmt(point.y)
|
|
2064
|
-
};
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
/**
|
|
2068
|
-
* Drag math — pure. Given anchor + current + modifiers, returns geometry
|
|
2069
|
-
* attrs for the in-progress node.
|
|
2070
|
-
*
|
|
2071
|
-
* Per-tag rules:
|
|
2072
|
-
* - rect → x / y / width / height
|
|
2073
|
-
* Shift: width === height (the larger of the two deltas wins, signs
|
|
2074
|
-
* preserved so the rect can still flip across the anchor).
|
|
2075
|
-
* Alt: anchor is treated as center; rect grows symmetrically.
|
|
2076
|
-
* - ellipse → cx / cy / rx / ry
|
|
2077
|
-
* Shift: rx === ry (uniform circle).
|
|
2078
|
-
* Alt: anchor is treated as center (default for ellipse anyway —
|
|
2079
|
-
* Alt makes the anchor act as center even when Shift is on).
|
|
2080
|
-
* Without Alt the anchor is one corner of the ellipse's bbox; with
|
|
2081
|
-
* Alt the anchor is the center.
|
|
2082
|
-
* - line → x1 / y1 / x2 / y2
|
|
2083
|
-
* Shift: angle quantized to 0° / 45° / 90°.
|
|
2084
|
-
* Alt: anchor is the midpoint of the line (mirror the drag).
|
|
2085
|
-
*/
|
|
2086
|
-
function compute_drag_attrs(tag, anchor, current, modifiers) {
|
|
2087
|
-
switch (tag) {
|
|
2088
|
-
case "rect": return rect_attrs(anchor, current, modifiers);
|
|
2089
|
-
case "ellipse": return ellipse_attrs(anchor, current, modifiers);
|
|
2090
|
-
case "line": return line_attrs(anchor, current, modifiers);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
/** Default paint attrs for a freshly-inserted shape. v1: gray fill, no
|
|
2094
|
-
* stroke. Line gets a stroke (otherwise it's invisible — `<line>` has no
|
|
2095
|
-
* fill area). Hard-coded for v1; a future preset may swap these. */
|
|
2096
|
-
function default_paint_attrs(tag) {
|
|
2097
|
-
switch (tag) {
|
|
2098
|
-
case "rect":
|
|
2099
|
-
case "ellipse": return { fill: DEFAULT_FILL };
|
|
2100
|
-
case "line": return {
|
|
2101
|
-
stroke: "#000000",
|
|
2102
|
-
"stroke-width": "1"
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
function rect_attrs(anchor, current, mods) {
|
|
2107
|
-
let dx = current.x - anchor.x;
|
|
2108
|
-
let dy = current.y - anchor.y;
|
|
2109
|
-
if (mods.shift) {
|
|
2110
|
-
const m = Math.max(Math.abs(dx), Math.abs(dy));
|
|
2111
|
-
dx = dx < 0 ? -m : m;
|
|
2112
|
-
dy = dy < 0 ? -m : m;
|
|
2113
|
-
}
|
|
2114
|
-
let x;
|
|
2115
|
-
let y;
|
|
2116
|
-
let w;
|
|
2117
|
-
let h;
|
|
2118
|
-
if (mods.alt) {
|
|
2119
|
-
x = anchor.x - Math.abs(dx);
|
|
2120
|
-
y = anchor.y - Math.abs(dy);
|
|
2121
|
-
w = Math.abs(dx) * 2;
|
|
2122
|
-
h = Math.abs(dy) * 2;
|
|
2123
|
-
} else {
|
|
2124
|
-
x = Math.min(anchor.x, anchor.x + dx);
|
|
2125
|
-
y = Math.min(anchor.y, anchor.y + dy);
|
|
2126
|
-
w = Math.abs(dx);
|
|
2127
|
-
h = Math.abs(dy);
|
|
2128
|
-
}
|
|
2129
|
-
return {
|
|
2130
|
-
x: fmt(x),
|
|
2131
|
-
y: fmt(y),
|
|
2132
|
-
width: fmt(w),
|
|
2133
|
-
height: fmt(h)
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
function ellipse_attrs(anchor, current, mods) {
|
|
2137
|
-
let dx = current.x - anchor.x;
|
|
2138
|
-
let dy = current.y - anchor.y;
|
|
2139
|
-
if (mods.shift) {
|
|
2140
|
-
const m = Math.max(Math.abs(dx), Math.abs(dy));
|
|
2141
|
-
dx = dx < 0 ? -m : m;
|
|
2142
|
-
dy = dy < 0 ? -m : m;
|
|
2143
|
-
}
|
|
2144
|
-
let cx;
|
|
2145
|
-
let cy;
|
|
2146
|
-
let rx;
|
|
2147
|
-
let ry;
|
|
2148
|
-
if (mods.alt) {
|
|
2149
|
-
cx = anchor.x;
|
|
2150
|
-
cy = anchor.y;
|
|
2151
|
-
rx = Math.abs(dx);
|
|
2152
|
-
ry = Math.abs(dy);
|
|
2153
|
-
} else {
|
|
2154
|
-
cx = anchor.x + dx / 2;
|
|
2155
|
-
cy = anchor.y + dy / 2;
|
|
2156
|
-
rx = Math.abs(dx) / 2;
|
|
2157
|
-
ry = Math.abs(dy) / 2;
|
|
2158
|
-
}
|
|
2159
|
-
return {
|
|
2160
|
-
cx: fmt(cx),
|
|
2161
|
-
cy: fmt(cy),
|
|
2162
|
-
rx: fmt(rx),
|
|
2163
|
-
ry: fmt(ry)
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
function line_attrs(anchor, current, mods) {
|
|
2167
|
-
let dx = current.x - anchor.x;
|
|
2168
|
-
let dy = current.y - anchor.y;
|
|
2169
|
-
if (mods.shift) {
|
|
2170
|
-
const len = Math.hypot(dx, dy);
|
|
2171
|
-
if (len > 0) {
|
|
2172
|
-
const angle = Math.atan2(dy, dx);
|
|
2173
|
-
const step = Math.PI / 4;
|
|
2174
|
-
const quantized = Math.round(angle / step) * step;
|
|
2175
|
-
dx = Math.cos(quantized) * len;
|
|
2176
|
-
dy = Math.sin(quantized) * len;
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
let x1;
|
|
2180
|
-
let y1;
|
|
2181
|
-
let x2;
|
|
2182
|
-
let y2;
|
|
2183
|
-
if (mods.alt) {
|
|
2184
|
-
x1 = anchor.x - dx;
|
|
2185
|
-
y1 = anchor.y - dy;
|
|
2186
|
-
x2 = anchor.x + dx;
|
|
2187
|
-
y2 = anchor.y + dy;
|
|
2188
|
-
} else {
|
|
2189
|
-
x1 = anchor.x;
|
|
2190
|
-
y1 = anchor.y;
|
|
2191
|
-
x2 = anchor.x + dx;
|
|
2192
|
-
y2 = anchor.y + dy;
|
|
2193
|
-
}
|
|
2194
|
-
return {
|
|
2195
|
-
x1: fmt(x1),
|
|
2196
|
-
y1: fmt(y1),
|
|
2197
|
-
x2: fmt(x2),
|
|
2198
|
-
y2: fmt(y2)
|
|
2199
|
-
};
|
|
2200
|
-
}
|
|
2201
|
-
/** Format a numeric value for SVG attr output. Rounds to 4 decimals to
|
|
2202
|
-
* suppress IEEE-754 noise (`0.30000000000000004` → `0.3`); `String()`
|
|
2203
|
-
* drops trailing zeros and the decimal point for integers. */
|
|
2204
|
-
function fmt(n) {
|
|
2205
|
-
return String(Math.round(n * 1e4) / 1e4);
|
|
2206
|
-
}
|
|
2207
|
-
//#endregion
|
|
2208
|
-
Object.defineProperty(exports, "DEFAULT_STYLE", {
|
|
2209
|
-
enumerable: true,
|
|
2210
|
-
get: function() {
|
|
2211
|
-
return DEFAULT_STYLE;
|
|
2212
|
-
}
|
|
2213
|
-
});
|
|
2214
|
-
Object.defineProperty(exports, "NudgeDwellWatcher", {
|
|
2215
|
-
enumerable: true,
|
|
2216
|
-
get: function() {
|
|
2217
|
-
return NudgeDwellWatcher;
|
|
2218
|
-
}
|
|
2219
|
-
});
|
|
2220
|
-
Object.defineProperty(exports, "RotateOrchestrator", {
|
|
2221
|
-
enumerable: true,
|
|
2222
|
-
get: function() {
|
|
2223
|
-
return RotateOrchestrator;
|
|
2224
|
-
}
|
|
2225
|
-
});
|
|
2226
|
-
Object.defineProperty(exports, "STAGES_NUDGE", {
|
|
2227
|
-
enumerable: true,
|
|
2228
|
-
get: function() {
|
|
2229
|
-
return STAGES_NUDGE;
|
|
2230
|
-
}
|
|
2231
|
-
});
|
|
2232
|
-
Object.defineProperty(exports, "STRUCTURAL_GRAPHICS_SET", {
|
|
2233
|
-
enumerable: true,
|
|
2234
|
-
get: function() {
|
|
2235
|
-
return STRUCTURAL_GRAPHICS_SET;
|
|
2236
|
-
}
|
|
2237
|
-
});
|
|
2238
|
-
Object.defineProperty(exports, "TOOL_CURSOR", {
|
|
2239
|
-
enumerable: true,
|
|
2240
|
-
get: function() {
|
|
2241
|
-
return TOOL_CURSOR;
|
|
2242
|
-
}
|
|
2243
|
-
});
|
|
2244
|
-
Object.defineProperty(exports, "TranslateOrchestrator", {
|
|
2245
|
-
enumerable: true,
|
|
2246
|
-
get: function() {
|
|
2247
|
-
return TranslateOrchestrator;
|
|
2248
|
-
}
|
|
2249
|
-
});
|
|
2250
|
-
Object.defineProperty(exports, "__exportAll", {
|
|
2251
|
-
enumerable: true,
|
|
2252
|
-
get: function() {
|
|
2253
|
-
return __exportAll;
|
|
2254
|
-
}
|
|
2255
|
-
});
|
|
2256
|
-
Object.defineProperty(exports, "__toESM", {
|
|
2257
|
-
enumerable: true,
|
|
2258
|
-
get: function() {
|
|
2259
|
-
return __toESM;
|
|
2260
|
-
}
|
|
2261
|
-
});
|
|
2262
|
-
Object.defineProperty(exports, "apply_resize", {
|
|
2263
|
-
enumerable: true,
|
|
2264
|
-
get: function() {
|
|
2265
|
-
return apply_resize;
|
|
2266
|
-
}
|
|
2267
|
-
});
|
|
2268
|
-
Object.defineProperty(exports, "apply_rotate", {
|
|
2269
|
-
enumerable: true,
|
|
2270
|
-
get: function() {
|
|
2271
|
-
return apply_rotate;
|
|
2272
|
-
}
|
|
2273
|
-
});
|
|
2274
|
-
Object.defineProperty(exports, "apply_translate", {
|
|
2275
|
-
enumerable: true,
|
|
2276
|
-
get: function() {
|
|
2277
|
-
return apply_translate;
|
|
2278
|
-
}
|
|
2279
|
-
});
|
|
2280
|
-
Object.defineProperty(exports, "capture_resize_baseline", {
|
|
2281
|
-
enumerable: true,
|
|
2282
|
-
get: function() {
|
|
2283
|
-
return capture_resize_baseline;
|
|
2284
|
-
}
|
|
2285
|
-
});
|
|
2286
|
-
Object.defineProperty(exports, "capture_translate_baseline", {
|
|
2287
|
-
enumerable: true,
|
|
2288
|
-
get: function() {
|
|
2289
|
-
return capture_translate_baseline;
|
|
2290
|
-
}
|
|
2291
|
-
});
|
|
2292
|
-
Object.defineProperty(exports, "compute_drag_attrs", {
|
|
2293
|
-
enumerable: true,
|
|
2294
|
-
get: function() {
|
|
2295
|
-
return compute_drag_attrs;
|
|
2296
|
-
}
|
|
2297
|
-
});
|
|
2298
|
-
Object.defineProperty(exports, "compute_resize_factors", {
|
|
2299
|
-
enumerable: true,
|
|
2300
|
-
get: function() {
|
|
2301
|
-
return compute_resize_factors;
|
|
2302
|
-
}
|
|
2303
|
-
});
|
|
2304
|
-
Object.defineProperty(exports, "default_attrs", {
|
|
2305
|
-
enumerable: true,
|
|
2306
|
-
get: function() {
|
|
2307
|
-
return default_attrs;
|
|
2308
|
-
}
|
|
2309
|
-
});
|
|
2310
|
-
Object.defineProperty(exports, "default_paint_attrs", {
|
|
2311
|
-
enumerable: true,
|
|
2312
|
-
get: function() {
|
|
2313
|
-
return default_paint_attrs;
|
|
2314
|
-
}
|
|
2315
|
-
});
|
|
2316
|
-
Object.defineProperty(exports, "emit_transform_list", {
|
|
2317
|
-
enumerable: true,
|
|
2318
|
-
get: function() {
|
|
2319
|
-
return emit_transform_list;
|
|
2320
|
-
}
|
|
2321
|
-
});
|
|
2322
|
-
Object.defineProperty(exports, "hit_shape_of_doc", {
|
|
2323
|
-
enumerable: true,
|
|
2324
|
-
get: function() {
|
|
2325
|
-
return hit_shape_of_doc;
|
|
2326
|
-
}
|
|
2327
|
-
});
|
|
2328
|
-
Object.defineProperty(exports, "initial_attrs", {
|
|
2329
|
-
enumerable: true,
|
|
2330
|
-
get: function() {
|
|
2331
|
-
return initial_attrs;
|
|
2332
|
-
}
|
|
2333
|
-
});
|
|
2334
|
-
Object.defineProperty(exports, "is_resizable", {
|
|
2335
|
-
enumerable: true,
|
|
2336
|
-
get: function() {
|
|
2337
|
-
return is_resizable;
|
|
2338
|
-
}
|
|
2339
|
-
});
|
|
2340
|
-
Object.defineProperty(exports, "is_resizable_node", {
|
|
2341
|
-
enumerable: true,
|
|
2342
|
-
get: function() {
|
|
2343
|
-
return is_resizable_node;
|
|
2344
|
-
}
|
|
2345
|
-
});
|
|
2346
|
-
Object.defineProperty(exports, "is_text_input_focused", {
|
|
2347
|
-
enumerable: true,
|
|
2348
|
-
get: function() {
|
|
2349
|
-
return is_text_input_focused;
|
|
2350
|
-
}
|
|
2351
|
-
});
|
|
2352
|
-
Object.defineProperty(exports, "is_transparent_tag", {
|
|
2353
|
-
enumerable: true,
|
|
2354
|
-
get: function() {
|
|
2355
|
-
return is_transparent_tag;
|
|
2356
|
-
}
|
|
2357
|
-
});
|
|
2358
|
-
Object.defineProperty(exports, "parse_paint", {
|
|
2359
|
-
enumerable: true,
|
|
2360
|
-
get: function() {
|
|
2361
|
-
return parse_paint;
|
|
2362
|
-
}
|
|
2363
|
-
});
|
|
2364
|
-
Object.defineProperty(exports, "parse_transform_list", {
|
|
2365
|
-
enumerable: true,
|
|
2366
|
-
get: function() {
|
|
2367
|
-
return parse_transform_list;
|
|
2368
|
-
}
|
|
2369
|
-
});
|
|
2370
|
-
Object.defineProperty(exports, "plan_group", {
|
|
2371
|
-
enumerable: true,
|
|
2372
|
-
get: function() {
|
|
2373
|
-
return plan_group;
|
|
2374
|
-
}
|
|
2375
|
-
});
|
|
2376
|
-
Object.defineProperty(exports, "prepare_rotate_rpc", {
|
|
2377
|
-
enumerable: true,
|
|
2378
|
-
get: function() {
|
|
2379
|
-
return prepare_rotate_rpc;
|
|
2380
|
-
}
|
|
2381
|
-
});
|
|
2382
|
-
Object.defineProperty(exports, "prepare_translate_rpc", {
|
|
2383
|
-
enumerable: true,
|
|
2384
|
-
get: function() {
|
|
2385
|
-
return prepare_translate_rpc;
|
|
2386
|
-
}
|
|
2387
|
-
});
|
|
2388
|
-
Object.defineProperty(exports, "project_local_bbox", {
|
|
2389
|
-
enumerable: true,
|
|
2390
|
-
get: function() {
|
|
2391
|
-
return project_local_bbox;
|
|
2392
|
-
}
|
|
2393
|
-
});
|
|
2394
|
-
Object.defineProperty(exports, "serialize_paint", {
|
|
2395
|
-
enumerable: true,
|
|
2396
|
-
get: function() {
|
|
2397
|
-
return serialize_paint;
|
|
2398
|
-
}
|
|
2399
|
-
});
|