@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
|
@@ -0,0 +1,3875 @@
|
|
|
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_parse = require("@grida/svg/parse");
|
|
35
|
+
let _grida_svg_pathdata = require("@grida/svg/pathdata");
|
|
36
|
+
let _grida_vn = require("@grida/vn");
|
|
37
|
+
_grida_vn = __toESM(_grida_vn);
|
|
38
|
+
//#region src/util/dom.ts
|
|
39
|
+
/**
|
|
40
|
+
* `true` when the document's active element is a text-input-like control
|
|
41
|
+
* (input / textarea / contentEditable). Used by keymap + gesture defaults
|
|
42
|
+
* to avoid hijacking keystrokes while the user is typing.
|
|
43
|
+
*/
|
|
44
|
+
function is_text_input_focused() {
|
|
45
|
+
if (typeof document === "undefined") return false;
|
|
46
|
+
const el = document.activeElement;
|
|
47
|
+
if (!el) return false;
|
|
48
|
+
const tag = el.tagName;
|
|
49
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return true;
|
|
50
|
+
if (el.isContentEditable) return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/util/equal.ts
|
|
55
|
+
function array_shallow_equal(a, b) {
|
|
56
|
+
if (a === b) return true;
|
|
57
|
+
if (a.length !== b.length) return false;
|
|
58
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/core/group.ts
|
|
63
|
+
let group;
|
|
64
|
+
(function(_group) {
|
|
65
|
+
const STRUCTURAL_GRAPHICS = _group.STRUCTURAL_GRAPHICS = new Set([
|
|
66
|
+
"g",
|
|
67
|
+
"defs",
|
|
68
|
+
"svg",
|
|
69
|
+
"use",
|
|
70
|
+
"image",
|
|
71
|
+
"switch",
|
|
72
|
+
"foreignObject",
|
|
73
|
+
"path",
|
|
74
|
+
"rect",
|
|
75
|
+
"circle",
|
|
76
|
+
"ellipse",
|
|
77
|
+
"line",
|
|
78
|
+
"polyline",
|
|
79
|
+
"polygon",
|
|
80
|
+
"text",
|
|
81
|
+
"a"
|
|
82
|
+
]);
|
|
83
|
+
const CONSTRAINED_PARENT = _group.CONSTRAINED_PARENT = new Set([
|
|
84
|
+
"text",
|
|
85
|
+
"tspan",
|
|
86
|
+
"defs",
|
|
87
|
+
"clipPath",
|
|
88
|
+
"mask",
|
|
89
|
+
"pattern",
|
|
90
|
+
"marker",
|
|
91
|
+
"symbol",
|
|
92
|
+
"filter",
|
|
93
|
+
"linearGradient",
|
|
94
|
+
"radialGradient",
|
|
95
|
+
"animateMotion",
|
|
96
|
+
"switch"
|
|
97
|
+
]);
|
|
98
|
+
function plan(doc, ids) {
|
|
99
|
+
if (ids.length === 0) return null;
|
|
100
|
+
const parent = doc.parent_of(ids[0]);
|
|
101
|
+
if (parent === null) return null;
|
|
102
|
+
for (const id of ids) {
|
|
103
|
+
if (doc.parent_of(id) !== parent) return null;
|
|
104
|
+
if (!STRUCTURAL_GRAPHICS.has(doc.tag_of(id))) return null;
|
|
105
|
+
}
|
|
106
|
+
if (CONSTRAINED_PARENT.has(doc.tag_of(parent))) return null;
|
|
107
|
+
const siblings = doc.element_children_of(parent);
|
|
108
|
+
const sibling_index = /* @__PURE__ */ new Map();
|
|
109
|
+
for (let i = 0; i < siblings.length; i++) sibling_index.set(siblings[i], i);
|
|
110
|
+
const indices = [];
|
|
111
|
+
for (const id of ids) {
|
|
112
|
+
const i = sibling_index.get(id);
|
|
113
|
+
if (i === void 0) return null;
|
|
114
|
+
indices.push(i);
|
|
115
|
+
}
|
|
116
|
+
const sorted = Array.from(new Set(indices)).sort((a, b) => a - b);
|
|
117
|
+
for (let i = 1; i < sorted.length; i++) if (sorted[i] !== sorted[i - 1] + 1) return null;
|
|
118
|
+
const children = sorted.map((i) => siblings[i]);
|
|
119
|
+
const last_index = sorted[sorted.length - 1];
|
|
120
|
+
const original_positions = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const i of sorted) original_positions.set(siblings[i], siblings[i + 1] ?? null);
|
|
122
|
+
return {
|
|
123
|
+
parent,
|
|
124
|
+
insert_before: siblings[last_index + 1] ?? null,
|
|
125
|
+
children,
|
|
126
|
+
original_positions
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
_group.plan = plan;
|
|
130
|
+
})(group || (group = {}));
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/core/transform.ts
|
|
133
|
+
let transform;
|
|
134
|
+
(function(_transform) {
|
|
135
|
+
/** SVG `<number>` production (spec-aligned subset). */
|
|
136
|
+
const SVG_NUMBER_SRC = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?";
|
|
137
|
+
/** Recognized function names. Names are case-sensitive per SVG 1.1 §7.6. */
|
|
138
|
+
const FUNCTION_RE = /\s*([A-Za-z]+)\s*\(([^)]*)\)\s*/y;
|
|
139
|
+
const NUMBER_GLOBAL_RE = new RegExp(SVG_NUMBER_SRC, "g");
|
|
140
|
+
/** SVG 2 §11.6.1: `transform="none"` ≡ identity. CSS-wide keywords are
|
|
141
|
+
* normalized at parse time per SVG 2 cascade. */
|
|
142
|
+
const IDENTITY_RE = /^\s*(?:none|inherit|unset|initial|revert|revert-layer)?\s*$/;
|
|
143
|
+
function parse_args(args) {
|
|
144
|
+
const tokens = args.match(NUMBER_GLOBAL_RE);
|
|
145
|
+
if (!tokens) return [];
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const t of tokens) {
|
|
148
|
+
const n = parseFloat(t);
|
|
149
|
+
if (!Number.isFinite(n)) return null;
|
|
150
|
+
out.push(n);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function build_op(name, a) {
|
|
155
|
+
switch (name) {
|
|
156
|
+
case "matrix":
|
|
157
|
+
if (a.length !== 6) return null;
|
|
158
|
+
return {
|
|
159
|
+
type: "matrix",
|
|
160
|
+
a: a[0],
|
|
161
|
+
b: a[1],
|
|
162
|
+
c: a[2],
|
|
163
|
+
d: a[3],
|
|
164
|
+
e: a[4],
|
|
165
|
+
f: a[5]
|
|
166
|
+
};
|
|
167
|
+
case "translate":
|
|
168
|
+
if (a.length !== 1 && a.length !== 2) return null;
|
|
169
|
+
return {
|
|
170
|
+
type: "translate",
|
|
171
|
+
tx: a[0],
|
|
172
|
+
ty: a.length === 2 ? a[1] : 0
|
|
173
|
+
};
|
|
174
|
+
case "rotate":
|
|
175
|
+
if (a.length !== 1 && a.length !== 3) return null;
|
|
176
|
+
return {
|
|
177
|
+
type: "rotate",
|
|
178
|
+
angle: a[0],
|
|
179
|
+
cx: a.length === 3 ? a[1] : 0,
|
|
180
|
+
cy: a.length === 3 ? a[2] : 0,
|
|
181
|
+
explicit_pivot: a.length === 3
|
|
182
|
+
};
|
|
183
|
+
case "scale":
|
|
184
|
+
if (a.length !== 1 && a.length !== 2) return null;
|
|
185
|
+
return {
|
|
186
|
+
type: "scale",
|
|
187
|
+
sx: a[0],
|
|
188
|
+
sy: a.length === 2 ? a[1] : a[0]
|
|
189
|
+
};
|
|
190
|
+
case "skewX":
|
|
191
|
+
if (a.length !== 1) return null;
|
|
192
|
+
return {
|
|
193
|
+
type: "skewX",
|
|
194
|
+
angle: a[0]
|
|
195
|
+
};
|
|
196
|
+
case "skewY":
|
|
197
|
+
if (a.length !== 1) return null;
|
|
198
|
+
return {
|
|
199
|
+
type: "skewY",
|
|
200
|
+
angle: a[0]
|
|
201
|
+
};
|
|
202
|
+
default: return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function parse(input) {
|
|
206
|
+
if (input === null) return [];
|
|
207
|
+
if (IDENTITY_RE.test(input)) return [];
|
|
208
|
+
const ops = [];
|
|
209
|
+
FUNCTION_RE.lastIndex = 0;
|
|
210
|
+
let pos = 0;
|
|
211
|
+
while (pos < input.length) {
|
|
212
|
+
FUNCTION_RE.lastIndex = pos;
|
|
213
|
+
const m = FUNCTION_RE.exec(input);
|
|
214
|
+
if (!m || m.index !== pos) return null;
|
|
215
|
+
const name = m[1];
|
|
216
|
+
const args = parse_args(m[2]);
|
|
217
|
+
if (args === null) return null;
|
|
218
|
+
const op = build_op(name, args);
|
|
219
|
+
if (!op) return null;
|
|
220
|
+
ops.push(op);
|
|
221
|
+
pos = FUNCTION_RE.lastIndex;
|
|
222
|
+
while (pos < input.length && /[\s,]/.test(input[pos])) pos++;
|
|
223
|
+
}
|
|
224
|
+
return ops;
|
|
225
|
+
}
|
|
226
|
+
_transform.parse = parse;
|
|
227
|
+
function n(x) {
|
|
228
|
+
return String(x);
|
|
229
|
+
}
|
|
230
|
+
function emit_op(op) {
|
|
231
|
+
switch (op.type) {
|
|
232
|
+
case "matrix": return `matrix(${n(op.a)} ${n(op.b)} ${n(op.c)} ${n(op.d)} ${n(op.e)} ${n(op.f)})`;
|
|
233
|
+
case "translate": return `translate(${n(op.tx)} ${n(op.ty)})`;
|
|
234
|
+
case "rotate": return `rotate(${n(op.angle)} ${n(op.cx)} ${n(op.cy)})`;
|
|
235
|
+
case "scale": return `scale(${n(op.sx)} ${n(op.sy)})`;
|
|
236
|
+
case "skewX": return `skewX(${n(op.angle)})`;
|
|
237
|
+
case "skewY": return `skewY(${n(op.angle)})`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
_transform.emit_op = emit_op;
|
|
241
|
+
function emit(ops) {
|
|
242
|
+
return ops.map(emit_op).join(" ");
|
|
243
|
+
}
|
|
244
|
+
_transform.emit = emit;
|
|
245
|
+
function is_identity_translate(op) {
|
|
246
|
+
return op.type === "translate" && op.tx === 0 && op.ty === 0;
|
|
247
|
+
}
|
|
248
|
+
function is_identity_rotate(op) {
|
|
249
|
+
return op.type === "rotate" && op.angle === 0;
|
|
250
|
+
}
|
|
251
|
+
function classify(ops) {
|
|
252
|
+
const trimmed = ops.filter((op) => !is_identity_translate(op) && !is_identity_rotate(op));
|
|
253
|
+
if (trimmed.length === 0) return "identity";
|
|
254
|
+
if (trimmed.length === 1) {
|
|
255
|
+
if (trimmed[0].type === "translate") return "leading_translate_only";
|
|
256
|
+
if (trimmed[0].type === "rotate") return "single_rotate_only";
|
|
257
|
+
return "mixed";
|
|
258
|
+
}
|
|
259
|
+
if (trimmed.length === 2 && trimmed[0].type === "translate" && trimmed[1].type === "rotate") return "leading_translate_then_single_rotate";
|
|
260
|
+
return "mixed";
|
|
261
|
+
}
|
|
262
|
+
_transform.classify = classify;
|
|
263
|
+
function op_matrix(op) {
|
|
264
|
+
switch (op.type) {
|
|
265
|
+
case "matrix": return [[
|
|
266
|
+
op.a,
|
|
267
|
+
op.c,
|
|
268
|
+
op.e
|
|
269
|
+
], [
|
|
270
|
+
op.b,
|
|
271
|
+
op.d,
|
|
272
|
+
op.f
|
|
273
|
+
]];
|
|
274
|
+
case "translate": return [[
|
|
275
|
+
1,
|
|
276
|
+
0,
|
|
277
|
+
op.tx
|
|
278
|
+
], [
|
|
279
|
+
0,
|
|
280
|
+
1,
|
|
281
|
+
op.ty
|
|
282
|
+
]];
|
|
283
|
+
case "rotate": {
|
|
284
|
+
const t = op.angle * Math.PI / 180;
|
|
285
|
+
const cos = Math.cos(t);
|
|
286
|
+
const sin = Math.sin(t);
|
|
287
|
+
return [[
|
|
288
|
+
cos,
|
|
289
|
+
-sin,
|
|
290
|
+
op.cx - op.cx * cos + op.cy * sin
|
|
291
|
+
], [
|
|
292
|
+
sin,
|
|
293
|
+
cos,
|
|
294
|
+
op.cy - op.cx * sin - op.cy * cos
|
|
295
|
+
]];
|
|
296
|
+
}
|
|
297
|
+
case "scale": return [[
|
|
298
|
+
op.sx,
|
|
299
|
+
0,
|
|
300
|
+
0
|
|
301
|
+
], [
|
|
302
|
+
0,
|
|
303
|
+
op.sy,
|
|
304
|
+
0
|
|
305
|
+
]];
|
|
306
|
+
case "skewX": {
|
|
307
|
+
const t = op.angle * Math.PI / 180;
|
|
308
|
+
return [[
|
|
309
|
+
1,
|
|
310
|
+
Math.tan(t),
|
|
311
|
+
0
|
|
312
|
+
], [
|
|
313
|
+
0,
|
|
314
|
+
1,
|
|
315
|
+
0
|
|
316
|
+
]];
|
|
317
|
+
}
|
|
318
|
+
case "skewY": {
|
|
319
|
+
const t = op.angle * Math.PI / 180;
|
|
320
|
+
return [[
|
|
321
|
+
1,
|
|
322
|
+
0,
|
|
323
|
+
0
|
|
324
|
+
], [
|
|
325
|
+
Math.tan(t),
|
|
326
|
+
1,
|
|
327
|
+
0
|
|
328
|
+
]];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** Compose a transform-list into a single 2×3 affine. Ops compose
|
|
333
|
+
* source-order = left-to-right multiplication: `transform="A B C"`
|
|
334
|
+
* maps a column-vector point `p` as `A · B · C · p` (per SVG 1.1
|
|
335
|
+
* §7.5). */
|
|
336
|
+
function compose(ops) {
|
|
337
|
+
let m = _grida_cmath.default.transform.identity;
|
|
338
|
+
for (const op of ops) m = _grida_cmath.default.transform.multiply(m, op_matrix(op));
|
|
339
|
+
return m;
|
|
340
|
+
}
|
|
341
|
+
function project(local, transform_str) {
|
|
342
|
+
if (!transform_str) return local;
|
|
343
|
+
const ops = parse(transform_str);
|
|
344
|
+
if (ops === null || ops.length === 0) return local;
|
|
345
|
+
const m = compose(ops);
|
|
346
|
+
const projected = [
|
|
347
|
+
[local.x, local.y],
|
|
348
|
+
[local.x + local.width, local.y],
|
|
349
|
+
[local.x + local.width, local.y + local.height],
|
|
350
|
+
[local.x, local.y + local.height]
|
|
351
|
+
].map((p) => _grida_cmath.default.vector2.transform(p, m));
|
|
352
|
+
return _grida_cmath.default.rect.fromPointsOrZero(projected);
|
|
353
|
+
}
|
|
354
|
+
_transform.project = project;
|
|
355
|
+
function recompose(ops, new_cx, new_cy) {
|
|
356
|
+
const tx_op = ops.find((op) => op.type === "translate");
|
|
357
|
+
const tx = tx_op?.type === "translate" ? tx_op.tx : 0;
|
|
358
|
+
const ty = tx_op?.type === "translate" ? tx_op.ty : 0;
|
|
359
|
+
return emit(ops.map((op) => op.type === "rotate" ? {
|
|
360
|
+
type: "rotate",
|
|
361
|
+
angle: op.angle,
|
|
362
|
+
cx: new_cx - tx,
|
|
363
|
+
cy: new_cy - ty,
|
|
364
|
+
explicit_pivot: true
|
|
365
|
+
} : op));
|
|
366
|
+
}
|
|
367
|
+
_transform.recompose = recompose;
|
|
368
|
+
})(transform || (transform = {}));
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/core/translate-pipeline/translate-pipeline.ts
|
|
371
|
+
let translate_pipeline;
|
|
372
|
+
(function(_translate_pipeline) {
|
|
373
|
+
let intent;
|
|
374
|
+
(function(_intent) {
|
|
375
|
+
function num(doc, id, name, fallback = 0) {
|
|
376
|
+
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
377
|
+
}
|
|
378
|
+
function capture_baseline(doc, id) {
|
|
379
|
+
const tag = doc.tag_of(id);
|
|
380
|
+
const own_transform = doc.get_attr(id, "transform");
|
|
381
|
+
if (own_transform !== null || tag === "g") return {
|
|
382
|
+
type: "viaTransform",
|
|
383
|
+
transform: own_transform
|
|
384
|
+
};
|
|
385
|
+
switch (tag) {
|
|
386
|
+
case "rect": return {
|
|
387
|
+
type: "rect",
|
|
388
|
+
x: num(doc, id, "x"),
|
|
389
|
+
y: num(doc, id, "y")
|
|
390
|
+
};
|
|
391
|
+
case "circle": return {
|
|
392
|
+
type: "circle",
|
|
393
|
+
cx: num(doc, id, "cx"),
|
|
394
|
+
cy: num(doc, id, "cy")
|
|
395
|
+
};
|
|
396
|
+
case "ellipse": return {
|
|
397
|
+
type: "ellipse",
|
|
398
|
+
cx: num(doc, id, "cx"),
|
|
399
|
+
cy: num(doc, id, "cy")
|
|
400
|
+
};
|
|
401
|
+
case "line": return {
|
|
402
|
+
type: "line",
|
|
403
|
+
x1: num(doc, id, "x1"),
|
|
404
|
+
y1: num(doc, id, "y1"),
|
|
405
|
+
x2: num(doc, id, "x2"),
|
|
406
|
+
y2: num(doc, id, "y2")
|
|
407
|
+
};
|
|
408
|
+
case "polyline": return {
|
|
409
|
+
type: "polyline",
|
|
410
|
+
points: doc.get_attr(id, "points") ?? ""
|
|
411
|
+
};
|
|
412
|
+
case "polygon": return {
|
|
413
|
+
type: "polygon",
|
|
414
|
+
points: doc.get_attr(id, "points") ?? ""
|
|
415
|
+
};
|
|
416
|
+
case "path": return {
|
|
417
|
+
type: "path",
|
|
418
|
+
d: doc.get_attr(id, "d") ?? ""
|
|
419
|
+
};
|
|
420
|
+
case "text": return {
|
|
421
|
+
type: "text",
|
|
422
|
+
x: num(doc, id, "x"),
|
|
423
|
+
y: num(doc, id, "y")
|
|
424
|
+
};
|
|
425
|
+
case "tspan": {
|
|
426
|
+
const dx_attr = doc.get_attr(id, "dx");
|
|
427
|
+
const dy_attr = doc.get_attr(id, "dy");
|
|
428
|
+
return {
|
|
429
|
+
type: "tspan",
|
|
430
|
+
dx: _grida_svg_parse.svg_parse.parse_number(dx_attr),
|
|
431
|
+
dy: _grida_svg_parse.svg_parse.parse_number(dy_attr),
|
|
432
|
+
dx_attr,
|
|
433
|
+
dy_attr
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
case "image": return {
|
|
437
|
+
type: "image",
|
|
438
|
+
x: num(doc, id, "x"),
|
|
439
|
+
y: num(doc, id, "y")
|
|
440
|
+
};
|
|
441
|
+
case "use": return {
|
|
442
|
+
type: "use",
|
|
443
|
+
x: num(doc, id, "x"),
|
|
444
|
+
y: num(doc, id, "y")
|
|
445
|
+
};
|
|
446
|
+
default: return { type: "unsupported" };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
_intent.capture_baseline = capture_baseline;
|
|
450
|
+
function capture_baselines(doc, ids) {
|
|
451
|
+
const out = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const id of ids) out.set(id, capture_baseline(doc, id));
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
_intent.capture_baselines = capture_baselines;
|
|
456
|
+
function baseline_anchor(b) {
|
|
457
|
+
switch (b.type) {
|
|
458
|
+
case "rect":
|
|
459
|
+
case "text":
|
|
460
|
+
case "image":
|
|
461
|
+
case "use": return {
|
|
462
|
+
x: b.x,
|
|
463
|
+
y: b.y
|
|
464
|
+
};
|
|
465
|
+
case "tspan": return null;
|
|
466
|
+
case "circle":
|
|
467
|
+
case "ellipse": return {
|
|
468
|
+
x: b.cx,
|
|
469
|
+
y: b.cy
|
|
470
|
+
};
|
|
471
|
+
case "line": return {
|
|
472
|
+
x: Math.min(b.x1, b.x2),
|
|
473
|
+
y: Math.min(b.y1, b.y2)
|
|
474
|
+
};
|
|
475
|
+
case "polyline":
|
|
476
|
+
case "polygon": return _grida_svg_parse.svg_parse.points_top_left(_grida_svg_parse.svg_parse.parse_points(b.points));
|
|
477
|
+
case "path": return _grida_svg_parse.svg_parse.parse_path_first_move(b.d);
|
|
478
|
+
case "viaTransform": {
|
|
479
|
+
const ops = transform.parse(b.transform);
|
|
480
|
+
if (ops === null) return null;
|
|
481
|
+
for (const op of ops) {
|
|
482
|
+
if (op.type === "translate") return {
|
|
483
|
+
x: op.tx,
|
|
484
|
+
y: op.ty
|
|
485
|
+
};
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
case "unsupported": return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
_intent.baseline_anchor = baseline_anchor;
|
|
494
|
+
function baseline_union_top_left(baselines) {
|
|
495
|
+
const anchors = [];
|
|
496
|
+
for (const b of baselines.values()) {
|
|
497
|
+
const p = baseline_anchor(b);
|
|
498
|
+
if (p) anchors.push(p);
|
|
499
|
+
}
|
|
500
|
+
return _grida_svg_parse.svg_parse.points_top_left(anchors);
|
|
501
|
+
}
|
|
502
|
+
_intent.baseline_union_top_left = baseline_union_top_left;
|
|
503
|
+
function shift_points_string(points, dx, dy) {
|
|
504
|
+
if (dx === 0 && dy === 0) return points;
|
|
505
|
+
return _grida_svg_parse.svg_parse.parse_points(points).map((p) => `${p.x + dx},${p.y + dy}`).join(" ");
|
|
506
|
+
}
|
|
507
|
+
function compose_leading_translate(existing, dx, dy) {
|
|
508
|
+
if (dx === 0 && dy === 0) return existing ? existing : null;
|
|
509
|
+
if (!existing) return `translate(${dx} ${dy})`;
|
|
510
|
+
const lead = _grida_svg_parse.svg_parse.parse_leading_translate(existing);
|
|
511
|
+
if (lead) {
|
|
512
|
+
const tx = lead.tx + dx;
|
|
513
|
+
const ty = lead.ty + dy;
|
|
514
|
+
return lead.rest ? `translate(${tx} ${ty}) ${lead.rest}` : `translate(${tx} ${ty})`;
|
|
515
|
+
}
|
|
516
|
+
return `translate(${dx} ${dy}) ${existing}`;
|
|
517
|
+
}
|
|
518
|
+
_intent.compose_leading_translate = compose_leading_translate;
|
|
519
|
+
/** Rewrite the leading value of a `dx`/`dy` offset list to `value`,
|
|
520
|
+
* preserving any per-glyph kerning tail (`compose("3 1 2", 9)` →
|
|
521
|
+
* `"9 1 2"`). Empty separators are dropped, so a stray leading comma
|
|
522
|
+
* doesn't lose the tail. With no original attribute (or a single
|
|
523
|
+
* value), emit just `value`. The leading value shifts the whole run. */
|
|
524
|
+
function compose_leading_offset(attr, value) {
|
|
525
|
+
if (attr === null) return String(value);
|
|
526
|
+
const tokens = attr.trim().split(/[\s,]+/).filter((t) => t !== "");
|
|
527
|
+
if (tokens.length <= 1) return String(value);
|
|
528
|
+
tokens[0] = String(value);
|
|
529
|
+
return tokens.join(" ");
|
|
530
|
+
}
|
|
531
|
+
function shift_path_d(d, dx, dy) {
|
|
532
|
+
if (dx === 0 && dy === 0) return d;
|
|
533
|
+
try {
|
|
534
|
+
return new _grida_svg_pathdata.SVGPathData(d).transform(_grida_svg_pathdata.SVGPathDataTransformer.TRANSLATE(dx, dy)).encode();
|
|
535
|
+
} catch {
|
|
536
|
+
return d;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function apply(doc, id, baseline, dx, dy) {
|
|
540
|
+
switch (baseline.type) {
|
|
541
|
+
case "viaTransform":
|
|
542
|
+
doc.set_attr(id, "transform", compose_leading_translate(baseline.transform ?? "", dx, dy));
|
|
543
|
+
return;
|
|
544
|
+
case "rect":
|
|
545
|
+
case "image":
|
|
546
|
+
case "use":
|
|
547
|
+
case "text":
|
|
548
|
+
doc.set_attr(id, "x", String(baseline.x + dx));
|
|
549
|
+
doc.set_attr(id, "y", String(baseline.y + dy));
|
|
550
|
+
return;
|
|
551
|
+
case "tspan":
|
|
552
|
+
doc.set_attr(id, "dx", compose_leading_offset(baseline.dx_attr, baseline.dx + dx));
|
|
553
|
+
doc.set_attr(id, "dy", compose_leading_offset(baseline.dy_attr, baseline.dy + dy));
|
|
554
|
+
return;
|
|
555
|
+
case "circle":
|
|
556
|
+
case "ellipse":
|
|
557
|
+
doc.set_attr(id, "cx", String(baseline.cx + dx));
|
|
558
|
+
doc.set_attr(id, "cy", String(baseline.cy + dy));
|
|
559
|
+
return;
|
|
560
|
+
case "line":
|
|
561
|
+
doc.set_attr(id, "x1", String(baseline.x1 + dx));
|
|
562
|
+
doc.set_attr(id, "y1", String(baseline.y1 + dy));
|
|
563
|
+
doc.set_attr(id, "x2", String(baseline.x2 + dx));
|
|
564
|
+
doc.set_attr(id, "y2", String(baseline.y2 + dy));
|
|
565
|
+
return;
|
|
566
|
+
case "polyline":
|
|
567
|
+
case "polygon":
|
|
568
|
+
doc.set_attr(id, "points", shift_points_string(baseline.points, dx, dy));
|
|
569
|
+
return;
|
|
570
|
+
case "path":
|
|
571
|
+
doc.set_attr(id, "d", shift_path_d(baseline.d, dx, dy));
|
|
572
|
+
return;
|
|
573
|
+
case "unsupported": return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
_intent.apply = apply;
|
|
577
|
+
function revert(doc, id, baseline) {
|
|
578
|
+
if (baseline.type === "tspan") {
|
|
579
|
+
doc.set_attr(id, "dx", baseline.dx_attr);
|
|
580
|
+
doc.set_attr(id, "dy", baseline.dy_attr);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
apply(doc, id, baseline, 0, 0);
|
|
584
|
+
}
|
|
585
|
+
_intent.revert = revert;
|
|
586
|
+
})(intent || (intent = _translate_pipeline.intent || (_translate_pipeline.intent = {})));
|
|
587
|
+
let stages;
|
|
588
|
+
(function(_stages) {
|
|
589
|
+
const axis_lock = _stages.axis_lock = {
|
|
590
|
+
name: "axis_lock",
|
|
591
|
+
run(plan, ctx) {
|
|
592
|
+
const m = ctx.input.movement;
|
|
593
|
+
const locked = ctx.modifiers.axis_lock === "by_dominance" ? _grida_cmath.default.ext.movement.axisLockedByDominance(m) : m;
|
|
594
|
+
const [x, y] = _grida_cmath.default.ext.movement.normalize(locked);
|
|
595
|
+
return { plan: {
|
|
596
|
+
...plan,
|
|
597
|
+
delta: {
|
|
598
|
+
x,
|
|
599
|
+
y
|
|
600
|
+
}
|
|
601
|
+
} };
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const snap = _stages.snap = {
|
|
605
|
+
name: "snap",
|
|
606
|
+
run(plan, ctx) {
|
|
607
|
+
if (ctx.modifiers.force_disable_snap) return { plan };
|
|
608
|
+
if (!ctx.snap_session) return { plan };
|
|
609
|
+
if (!ctx.options.snap_enabled) return { plan };
|
|
610
|
+
const r = ctx.snap_session.snap(plan.delta, {
|
|
611
|
+
enabled: true,
|
|
612
|
+
threshold_px: ctx.options.snap_threshold_px
|
|
613
|
+
}, ctx.snap_policy);
|
|
614
|
+
return {
|
|
615
|
+
plan: {
|
|
616
|
+
...plan,
|
|
617
|
+
delta: r.delta
|
|
618
|
+
},
|
|
619
|
+
emit: r.guide ? { guide: r.guide } : void 0
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const pixel_grid = _stages.pixel_grid = {
|
|
624
|
+
name: "pixel_grid",
|
|
625
|
+
run(plan, ctx) {
|
|
626
|
+
const q = ctx.options.pixel_grid_quantum;
|
|
627
|
+
if (q === null || q <= 0) return { plan };
|
|
628
|
+
const anchor = ctx.snap_session?.baseline_union_readonly ?? intent.baseline_union_top_left(plan.baselines);
|
|
629
|
+
if (!anchor) return { plan };
|
|
630
|
+
const qx = Math.round((anchor.x + plan.delta.x) / q) * q - anchor.x;
|
|
631
|
+
const qy = Math.round((anchor.y + plan.delta.y) / q) * q - anchor.y;
|
|
632
|
+
return { plan: {
|
|
633
|
+
...plan,
|
|
634
|
+
delta: {
|
|
635
|
+
x: qx,
|
|
636
|
+
y: qy
|
|
637
|
+
}
|
|
638
|
+
} };
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
_stages.DEFAULT = Object.freeze([
|
|
642
|
+
axis_lock,
|
|
643
|
+
snap,
|
|
644
|
+
pixel_grid
|
|
645
|
+
]);
|
|
646
|
+
_stages.NUDGE = Object.freeze([axis_lock, pixel_grid]);
|
|
647
|
+
_stages.RPC = Object.freeze([axis_lock]);
|
|
648
|
+
})(stages || (stages = _translate_pipeline.stages || (_translate_pipeline.stages = {})));
|
|
649
|
+
function run(init, stages, ctx) {
|
|
650
|
+
let plan = init;
|
|
651
|
+
const guides = [];
|
|
652
|
+
for (const stage of stages) {
|
|
653
|
+
const out = stage.run(plan, ctx);
|
|
654
|
+
plan = out.plan;
|
|
655
|
+
if (out.emit?.guide) guides.push(out.emit.guide);
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
plan,
|
|
659
|
+
guides
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
_translate_pipeline.run = run;
|
|
663
|
+
function apply(doc, plan, project) {
|
|
664
|
+
for (const id of plan.ids) {
|
|
665
|
+
const baseline = plan.baselines.get(id);
|
|
666
|
+
if (!baseline) continue;
|
|
667
|
+
const d = project ? project(id, plan.delta) : plan.delta;
|
|
668
|
+
intent.apply(doc, id, baseline, d.x, d.y);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
_translate_pipeline.apply = apply;
|
|
672
|
+
function revert(doc, plan) {
|
|
673
|
+
for (const id of plan.ids) {
|
|
674
|
+
const baseline = plan.baselines.get(id);
|
|
675
|
+
if (!baseline) continue;
|
|
676
|
+
intent.revert(doc, id, baseline);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
_translate_pipeline.revert = revert;
|
|
680
|
+
function prepare_rpc(args) {
|
|
681
|
+
const { doc, ids, delta, options, emit, stages: stage_list = stages.RPC, project } = args;
|
|
682
|
+
const filtered_ids = doc.prune_nested_nodes(ids);
|
|
683
|
+
const plan0 = {
|
|
684
|
+
ids: filtered_ids,
|
|
685
|
+
baselines: intent.capture_baselines(doc, filtered_ids),
|
|
686
|
+
delta: {
|
|
687
|
+
x: 0,
|
|
688
|
+
y: 0
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
const { plan } = run(plan0, stage_list, {
|
|
692
|
+
input: {
|
|
693
|
+
ids: plan0.ids,
|
|
694
|
+
movement: [delta.x, delta.y]
|
|
695
|
+
},
|
|
696
|
+
modifiers: {
|
|
697
|
+
axis_lock: "off",
|
|
698
|
+
force_disable_snap: true
|
|
699
|
+
},
|
|
700
|
+
options,
|
|
701
|
+
snap_session: null,
|
|
702
|
+
snap_policy: "engine"
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
plan,
|
|
706
|
+
apply: () => {
|
|
707
|
+
apply(doc, plan, project);
|
|
708
|
+
emit();
|
|
709
|
+
},
|
|
710
|
+
revert: () => {
|
|
711
|
+
revert(doc, plan);
|
|
712
|
+
emit();
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
_translate_pipeline.prepare_rpc = prepare_rpc;
|
|
717
|
+
})(translate_pipeline || (translate_pipeline = {}));
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/core/translate-pipeline/orchestrator.ts
|
|
720
|
+
const PROVIDER_ID$2 = "svg-editor";
|
|
721
|
+
var TranslateOrchestrator = class {
|
|
722
|
+
constructor(deps) {
|
|
723
|
+
this.deps = deps;
|
|
724
|
+
this.active = null;
|
|
725
|
+
this._last_guides = [];
|
|
726
|
+
}
|
|
727
|
+
/** Guides emitted by the most recent pipeline run. Cleared on
|
|
728
|
+
* cancel/dispose. HUD compositors read this to draw snap chrome. */
|
|
729
|
+
get last_guides() {
|
|
730
|
+
return this._last_guides;
|
|
731
|
+
}
|
|
732
|
+
/** True while a gesture session is open. */
|
|
733
|
+
has_active_session() {
|
|
734
|
+
return this.active !== null;
|
|
735
|
+
}
|
|
736
|
+
/** Per-frame drive: lazily opens a session on first call, runs the
|
|
737
|
+
* pipeline, writes apply/revert into the preview, and commits when
|
|
738
|
+
* `opts.phase === "commit"`. */
|
|
739
|
+
drive(input, modifiers, opts) {
|
|
740
|
+
if (this.active === null) this.active = this.open(input.ids, opts.snap, opts.label ?? "move");
|
|
741
|
+
const session = this.active;
|
|
742
|
+
const stages = opts.stages ?? translate_pipeline.stages.DEFAULT;
|
|
743
|
+
const result = this.run_pass(session, input.movement, modifiers, opts.policy, stages);
|
|
744
|
+
session.last_movement = input.movement;
|
|
745
|
+
session.last_policy = opts.policy;
|
|
746
|
+
session.last_stages = stages;
|
|
747
|
+
this.write_preview_delta(session, result.plan);
|
|
748
|
+
if (opts.phase === "commit") {
|
|
749
|
+
session.preview.commit();
|
|
750
|
+
this.dispose_session();
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
/** Re-run the current preview frame with new modifiers, reusing the
|
|
755
|
+
* last-known movement / policy / stages. Used when a modifier key
|
|
756
|
+
* changes between pointer-move events (Shift down/up mid-drag).
|
|
757
|
+
* No-op when no session is active. */
|
|
758
|
+
redrive_modifiers(modifiers) {
|
|
759
|
+
if (!this.active) return null;
|
|
760
|
+
const session = this.active;
|
|
761
|
+
const result = this.run_pass(session, session.last_movement, modifiers, session.last_policy, session.last_stages);
|
|
762
|
+
this.write_preview_delta(session, result.plan);
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
/** Cancel an in-flight gesture (Escape, programmatic abort). */
|
|
766
|
+
cancel() {
|
|
767
|
+
if (!this.active) return;
|
|
768
|
+
this.active.preview.discard();
|
|
769
|
+
this.dispose_session();
|
|
770
|
+
}
|
|
771
|
+
/** Build a plan + context, run the pipeline, stash guides. Pure
|
|
772
|
+
* computation — does not touch the preview. */
|
|
773
|
+
run_pass(session, movement, modifiers, policy, stages) {
|
|
774
|
+
const plan0 = {
|
|
775
|
+
ids: session.ids,
|
|
776
|
+
baselines: session.baselines,
|
|
777
|
+
delta: {
|
|
778
|
+
x: 0,
|
|
779
|
+
y: 0
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const ctx = {
|
|
783
|
+
input: {
|
|
784
|
+
ids: session.ids,
|
|
785
|
+
movement
|
|
786
|
+
},
|
|
787
|
+
modifiers,
|
|
788
|
+
options: this.deps.options(),
|
|
789
|
+
snap_session: session.snap,
|
|
790
|
+
snap_policy: policy
|
|
791
|
+
};
|
|
792
|
+
const result = translate_pipeline.run(plan0, stages, ctx);
|
|
793
|
+
this._last_guides = result.guides;
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
open(ids, snap, label) {
|
|
797
|
+
const doc = this.deps.get_doc();
|
|
798
|
+
const filtered = doc.prune_nested_nodes(ids);
|
|
799
|
+
return {
|
|
800
|
+
ids: filtered,
|
|
801
|
+
baselines: translate_pipeline.intent.capture_baselines(doc, filtered),
|
|
802
|
+
snap: snap ? this.deps.open_snap(filtered) : null,
|
|
803
|
+
preview: this.deps.open_preview(label),
|
|
804
|
+
last_movement: [0, 0],
|
|
805
|
+
last_policy: "engine",
|
|
806
|
+
last_stages: translate_pipeline.stages.DEFAULT
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/** Bind a fresh apply/revert pair (closure over `plan`) into the
|
|
810
|
+
* preview slot. Called from both `drive` (per pointer frame) and
|
|
811
|
+
* `redrive_modifiers` (on modifier flip). */
|
|
812
|
+
write_preview_delta(session, plan) {
|
|
813
|
+
const doc = this.deps.get_doc();
|
|
814
|
+
const emit = this.deps.emit;
|
|
815
|
+
const project = this.deps.project_delta;
|
|
816
|
+
session.preview.set({
|
|
817
|
+
providerId: PROVIDER_ID$2,
|
|
818
|
+
apply: () => {
|
|
819
|
+
translate_pipeline.apply(doc, plan, project);
|
|
820
|
+
emit();
|
|
821
|
+
},
|
|
822
|
+
revert: () => {
|
|
823
|
+
translate_pipeline.revert(doc, plan);
|
|
824
|
+
emit();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
dispose_session() {
|
|
829
|
+
if (!this.active) return;
|
|
830
|
+
this.active.snap?.dispose();
|
|
831
|
+
this.active = null;
|
|
832
|
+
this._last_guides = [];
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/core/translate-pipeline/nudge-dwell-watcher.ts
|
|
837
|
+
/** Hold-time after the last firing detection. Show is immediate (next
|
|
838
|
+
* frame); only the hide edge is delayed. */
|
|
839
|
+
const HIDE_MS = 500;
|
|
840
|
+
var NudgeDwellWatcher = class {
|
|
841
|
+
constructor(deps) {
|
|
842
|
+
this.deps = deps;
|
|
843
|
+
this._guides = [];
|
|
844
|
+
this.raf_id = null;
|
|
845
|
+
this.hide_timer = null;
|
|
846
|
+
this.unsubscribe = deps.editor.subscribe_translate_commit(() => this.schedule_detect());
|
|
847
|
+
}
|
|
848
|
+
/** Currently-published dwell guides. Empty between detections. */
|
|
849
|
+
get guides() {
|
|
850
|
+
return this._guides;
|
|
851
|
+
}
|
|
852
|
+
/** Drop any pending detection / held guide. Idempotent. */
|
|
853
|
+
cancel_pending() {
|
|
854
|
+
this.clear_raf();
|
|
855
|
+
this.clear_hide();
|
|
856
|
+
this.publish_guides([]);
|
|
857
|
+
}
|
|
858
|
+
dispose() {
|
|
859
|
+
this.unsubscribe();
|
|
860
|
+
this.cancel_pending();
|
|
861
|
+
}
|
|
862
|
+
schedule_detect() {
|
|
863
|
+
if (this.raf_id !== null) return;
|
|
864
|
+
this.raf_id = this.deps.window.requestAnimationFrame(() => {
|
|
865
|
+
this.raf_id = null;
|
|
866
|
+
this.detect();
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
detect() {
|
|
870
|
+
const ids = this.deps.editor.state.selection;
|
|
871
|
+
if (ids.length === 0) {
|
|
872
|
+
this.publish_guides([]);
|
|
873
|
+
this.clear_hide();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const snap = this.deps.open_snap(ids);
|
|
877
|
+
if (!snap) {
|
|
878
|
+
this.publish_guides([]);
|
|
879
|
+
this.clear_hide();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const plan0 = {
|
|
884
|
+
ids: [...ids],
|
|
885
|
+
baselines: translate_pipeline.intent.capture_baselines(this.deps.editor.document, ids),
|
|
886
|
+
delta: {
|
|
887
|
+
x: 0,
|
|
888
|
+
y: 0
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
const ctx = {
|
|
892
|
+
input: {
|
|
893
|
+
ids: plan0.ids,
|
|
894
|
+
movement: [0, 0]
|
|
895
|
+
},
|
|
896
|
+
modifiers: {
|
|
897
|
+
axis_lock: "off",
|
|
898
|
+
force_disable_snap: false
|
|
899
|
+
},
|
|
900
|
+
options: this.deps.options(),
|
|
901
|
+
snap_session: snap,
|
|
902
|
+
snap_policy: "aligned"
|
|
903
|
+
};
|
|
904
|
+
const result = translate_pipeline.run(plan0, translate_pipeline.stages.DEFAULT, ctx);
|
|
905
|
+
if (result.guides.length === 0) {
|
|
906
|
+
this.publish_guides([]);
|
|
907
|
+
this.clear_hide();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
this.publish_guides(result.guides);
|
|
911
|
+
this.arm_hide();
|
|
912
|
+
} finally {
|
|
913
|
+
snap.dispose();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
arm_hide() {
|
|
917
|
+
this.clear_hide();
|
|
918
|
+
this.hide_timer = this.deps.window.setTimeout(() => {
|
|
919
|
+
this.hide_timer = null;
|
|
920
|
+
this.publish_guides([]);
|
|
921
|
+
}, HIDE_MS);
|
|
922
|
+
}
|
|
923
|
+
publish_guides(next) {
|
|
924
|
+
if (next.length === 0 && this._guides.length === 0) return;
|
|
925
|
+
this._guides = next;
|
|
926
|
+
this.deps.on_guides_change();
|
|
927
|
+
}
|
|
928
|
+
clear_raf() {
|
|
929
|
+
if (this.raf_id === null) return;
|
|
930
|
+
this.deps.window.cancelAnimationFrame(this.raf_id);
|
|
931
|
+
this.raf_id = null;
|
|
932
|
+
}
|
|
933
|
+
clear_hide() {
|
|
934
|
+
if (this.hide_timer === null) return;
|
|
935
|
+
this.deps.window.clearTimeout(this.hide_timer);
|
|
936
|
+
this.hide_timer = null;
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/core/rotate-pipeline/rotate-pipeline.ts
|
|
941
|
+
let rotate_pipeline;
|
|
942
|
+
(function(_rotate_pipeline) {
|
|
943
|
+
let intent;
|
|
944
|
+
(function(_intent) {
|
|
945
|
+
function is_rotatable(doc, id) {
|
|
946
|
+
const own_transform = doc.get_attr(id, "transform");
|
|
947
|
+
const ops = transform.parse(own_transform);
|
|
948
|
+
if (ops === null) return {
|
|
949
|
+
kind: "refuse",
|
|
950
|
+
reason: "non-trivial-transform"
|
|
951
|
+
};
|
|
952
|
+
if (transform.classify(ops) === "mixed") return {
|
|
953
|
+
kind: "refuse",
|
|
954
|
+
reason: "non-trivial-transform"
|
|
955
|
+
};
|
|
956
|
+
if (doc.has_glyph_rotate(id)) return {
|
|
957
|
+
kind: "refuse",
|
|
958
|
+
reason: "text-with-glyph-rotate"
|
|
959
|
+
};
|
|
960
|
+
if (doc.has_inline_css_transform(id)) return {
|
|
961
|
+
kind: "refuse",
|
|
962
|
+
reason: "css-property-transform"
|
|
963
|
+
};
|
|
964
|
+
if (doc.has_animate_transform_child(id)) return {
|
|
965
|
+
kind: "refuse",
|
|
966
|
+
reason: "animated-transform"
|
|
967
|
+
};
|
|
968
|
+
return { kind: "yes" };
|
|
969
|
+
}
|
|
970
|
+
_intent.is_rotatable = is_rotatable;
|
|
971
|
+
function find_op(ops, type) {
|
|
972
|
+
for (const op of ops) if (op.type === type) return op;
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
function capture_baseline(doc, id, pivot) {
|
|
976
|
+
const transform_str = doc.get_attr(id, "transform");
|
|
977
|
+
const ops = transform.parse(transform_str) ?? [];
|
|
978
|
+
const lead = find_op(ops, "translate");
|
|
979
|
+
const rot = find_op(ops, "rotate");
|
|
980
|
+
return {
|
|
981
|
+
transform: transform_str,
|
|
982
|
+
leading_translate: lead ? {
|
|
983
|
+
x: lead.tx,
|
|
984
|
+
y: lead.ty
|
|
985
|
+
} : null,
|
|
986
|
+
current_rotation_deg: rot?.angle ?? 0,
|
|
987
|
+
pivot
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
_intent.capture_baseline = capture_baseline;
|
|
991
|
+
function capture_baselines(doc, ids, pivot) {
|
|
992
|
+
const out = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const id of ids) out.set(id, capture_baseline(doc, id, pivot));
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
_intent.capture_baselines = capture_baselines;
|
|
997
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
998
|
+
/** Snap trailing FP noise off the 10th decimal place. The rad→deg
|
|
999
|
+
* conversion + addition produce e.g. `29.999999999999996` for
|
|
1000
|
+
* what the user dragged as "30°"; rounding to 1e-9 absorbs the
|
|
1001
|
+
* noise without losing any user-meaningful precision (SVG
|
|
1002
|
+
* coordinates rarely go past 4 decimals in authored content).
|
|
1003
|
+
*
|
|
1004
|
+
* Limits drift accumulation across a long gesture: each frame's
|
|
1005
|
+
* emit re-renders from `current_rotation_deg + delta_deg`, where
|
|
1006
|
+
* the delta is recomputed against the gesture-start anchor — not
|
|
1007
|
+
* accumulated. So noise stays bounded per-frame. */
|
|
1008
|
+
function fmt_angle(n) {
|
|
1009
|
+
return String(Math.round(n * 1e9) / 1e9);
|
|
1010
|
+
}
|
|
1011
|
+
function apply(doc, id, baseline, angle_radians) {
|
|
1012
|
+
const angle_deg = angle_radians * RAD_TO_DEG;
|
|
1013
|
+
const total = baseline.current_rotation_deg + angle_deg;
|
|
1014
|
+
if (total === 0 && angle_deg === 0) {
|
|
1015
|
+
doc.set_attr(id, "transform", baseline.transform);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const tx = baseline.leading_translate?.x ?? 0;
|
|
1019
|
+
const ty = baseline.leading_translate?.y ?? 0;
|
|
1020
|
+
const cx = baseline.pivot.x - tx;
|
|
1021
|
+
const cy = baseline.pivot.y - ty;
|
|
1022
|
+
const rotate_token = `rotate(${fmt_angle(total)} ${cx} ${cy})`;
|
|
1023
|
+
const str = baseline.leading_translate ? `translate(${tx} ${ty}) ${rotate_token}` : rotate_token;
|
|
1024
|
+
doc.set_attr(id, "transform", str);
|
|
1025
|
+
}
|
|
1026
|
+
_intent.apply = apply;
|
|
1027
|
+
})(intent || (intent = _rotate_pipeline.intent || (_rotate_pipeline.intent = {})));
|
|
1028
|
+
let stages;
|
|
1029
|
+
(function(_stages) {
|
|
1030
|
+
const angle_snap = _stages.angle_snap = {
|
|
1031
|
+
name: "angle_snap",
|
|
1032
|
+
run(plan, ctx) {
|
|
1033
|
+
if (ctx.modifiers.force_disable_snap) return { plan };
|
|
1034
|
+
if (ctx.modifiers.angle_snap !== "step") return { plan };
|
|
1035
|
+
const step = ctx.options.angle_snap_step_radians;
|
|
1036
|
+
if (step === null || step <= 0) return { plan };
|
|
1037
|
+
const snapped = Math.round(plan.angle_radians / step) * step;
|
|
1038
|
+
return { plan: {
|
|
1039
|
+
...plan,
|
|
1040
|
+
angle_radians: snapped
|
|
1041
|
+
} };
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
_stages.DEFAULT = Object.freeze([angle_snap]);
|
|
1045
|
+
_stages.RPC = Object.freeze([]);
|
|
1046
|
+
})(stages || (stages = _rotate_pipeline.stages || (_rotate_pipeline.stages = {})));
|
|
1047
|
+
function run(init, stages, ctx) {
|
|
1048
|
+
let plan = init;
|
|
1049
|
+
for (const stage of stages) plan = stage.run(plan, ctx).plan;
|
|
1050
|
+
return { plan };
|
|
1051
|
+
}
|
|
1052
|
+
_rotate_pipeline.run = run;
|
|
1053
|
+
function apply(doc, plan) {
|
|
1054
|
+
for (const m of plan.members) intent.apply(doc, m.id, m.baseline, plan.angle_radians);
|
|
1055
|
+
}
|
|
1056
|
+
_rotate_pipeline.apply = apply;
|
|
1057
|
+
function revert(doc, plan) {
|
|
1058
|
+
for (const m of plan.members) intent.apply(doc, m.id, m.baseline, 0);
|
|
1059
|
+
}
|
|
1060
|
+
_rotate_pipeline.revert = revert;
|
|
1061
|
+
function prepare_rpc(args) {
|
|
1062
|
+
const { doc, ids, pivot, angle_radians, options, emit, stages: stage_list = stages.RPC } = args;
|
|
1063
|
+
const filtered_ids = doc.prune_nested_nodes(ids);
|
|
1064
|
+
const baselines = intent.capture_baselines(doc, filtered_ids, pivot);
|
|
1065
|
+
const members = filtered_ids.map((id) => ({
|
|
1066
|
+
id,
|
|
1067
|
+
baseline: baselines.get(id)
|
|
1068
|
+
}));
|
|
1069
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
1070
|
+
for (const id of filtered_ids) verdicts.set(id, intent.is_rotatable(doc, id));
|
|
1071
|
+
const { plan } = run({
|
|
1072
|
+
members,
|
|
1073
|
+
pivot,
|
|
1074
|
+
angle_radians
|
|
1075
|
+
}, stage_list, {
|
|
1076
|
+
input: {
|
|
1077
|
+
ids: filtered_ids,
|
|
1078
|
+
angle_radians
|
|
1079
|
+
},
|
|
1080
|
+
modifiers: {
|
|
1081
|
+
angle_snap: "off",
|
|
1082
|
+
force_disable_snap: true
|
|
1083
|
+
},
|
|
1084
|
+
options
|
|
1085
|
+
});
|
|
1086
|
+
return {
|
|
1087
|
+
plan,
|
|
1088
|
+
verdicts,
|
|
1089
|
+
apply: () => {
|
|
1090
|
+
apply(doc, plan);
|
|
1091
|
+
emit();
|
|
1092
|
+
},
|
|
1093
|
+
revert: () => {
|
|
1094
|
+
revert(doc, plan);
|
|
1095
|
+
emit();
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
_rotate_pipeline.prepare_rpc = prepare_rpc;
|
|
1100
|
+
})(rotate_pipeline || (rotate_pipeline = {}));
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region src/core/rotate-pipeline/orchestrator.ts
|
|
1103
|
+
const PROVIDER_ID$1 = "svg-editor";
|
|
1104
|
+
function ids_key$1(ids) {
|
|
1105
|
+
return [...ids].sort().join("\0");
|
|
1106
|
+
}
|
|
1107
|
+
var RotateOrchestrator = class {
|
|
1108
|
+
constructor(deps) {
|
|
1109
|
+
this.deps = deps;
|
|
1110
|
+
this.active = null;
|
|
1111
|
+
}
|
|
1112
|
+
has_active_session() {
|
|
1113
|
+
return this.active !== null;
|
|
1114
|
+
}
|
|
1115
|
+
is_active_for(ids) {
|
|
1116
|
+
return this.active !== null && this.active.ids_key === ids_key$1(ids);
|
|
1117
|
+
}
|
|
1118
|
+
/** Per-frame drive. Opens a session lazily on the first call. Returns
|
|
1119
|
+
* `null` when `ids` is empty. On commit, returns a `RotateCommitOutcome`
|
|
1120
|
+
* the caller uses to surface refusal chips. */
|
|
1121
|
+
drive(input, modifiers, opts) {
|
|
1122
|
+
if (input.ids.length === 0) return null;
|
|
1123
|
+
const key = ids_key$1(input.ids);
|
|
1124
|
+
if (this.active && this.active.ids_key !== key) {
|
|
1125
|
+
this.active.preview.discard();
|
|
1126
|
+
this.dispose_session();
|
|
1127
|
+
}
|
|
1128
|
+
if (this.active === null) this.active = this.open(input.ids, opts.label ?? "rotate");
|
|
1129
|
+
const session = this.active;
|
|
1130
|
+
const stages = opts.stages ?? rotate_pipeline.stages.DEFAULT;
|
|
1131
|
+
const result = this.run_pass(session, input.angle_radians, modifiers, stages);
|
|
1132
|
+
session.last_angle = input.angle_radians;
|
|
1133
|
+
session.last_stages = stages;
|
|
1134
|
+
this.write_preview(session, result.plan);
|
|
1135
|
+
if (opts.phase === "commit") {
|
|
1136
|
+
let outcome;
|
|
1137
|
+
let refused = false;
|
|
1138
|
+
for (const v of session.verdicts.values()) if (v.kind === "refuse") {
|
|
1139
|
+
refused = true;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
if (refused) {
|
|
1143
|
+
session.preview.discard();
|
|
1144
|
+
outcome = {
|
|
1145
|
+
kind: "refused",
|
|
1146
|
+
verdicts: session.verdicts
|
|
1147
|
+
};
|
|
1148
|
+
} else {
|
|
1149
|
+
session.preview.commit();
|
|
1150
|
+
outcome = {
|
|
1151
|
+
kind: "committed",
|
|
1152
|
+
plan: result.plan
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
this.dispose_session();
|
|
1156
|
+
return {
|
|
1157
|
+
result,
|
|
1158
|
+
outcome
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
result,
|
|
1163
|
+
outcome: null
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
redrive_modifiers(modifiers) {
|
|
1167
|
+
if (!this.active) return null;
|
|
1168
|
+
const session = this.active;
|
|
1169
|
+
const result = this.run_pass(session, session.last_angle, modifiers, session.last_stages);
|
|
1170
|
+
this.write_preview(session, result.plan);
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
cancel() {
|
|
1174
|
+
if (!this.active) return;
|
|
1175
|
+
this.active.preview.discard();
|
|
1176
|
+
this.dispose_session();
|
|
1177
|
+
}
|
|
1178
|
+
run_pass(session, angle_radians, modifiers, stages) {
|
|
1179
|
+
const plan0 = {
|
|
1180
|
+
members: session.members,
|
|
1181
|
+
pivot: session.pivot,
|
|
1182
|
+
angle_radians
|
|
1183
|
+
};
|
|
1184
|
+
const ctx = {
|
|
1185
|
+
input: {
|
|
1186
|
+
ids: session.members.map((m) => m.id),
|
|
1187
|
+
angle_radians
|
|
1188
|
+
},
|
|
1189
|
+
modifiers,
|
|
1190
|
+
options: this.deps.options()
|
|
1191
|
+
};
|
|
1192
|
+
return rotate_pipeline.run(plan0, stages, ctx);
|
|
1193
|
+
}
|
|
1194
|
+
open(ids, label) {
|
|
1195
|
+
const doc = this.deps.get_doc();
|
|
1196
|
+
const filtered = doc.prune_nested_nodes(ids);
|
|
1197
|
+
const pivot = compute_union_center(filtered, this.deps.bbox_world);
|
|
1198
|
+
const baselines = rotate_pipeline.intent.capture_baselines(doc, filtered, pivot);
|
|
1199
|
+
const members = filtered.map((id) => ({
|
|
1200
|
+
id,
|
|
1201
|
+
baseline: baselines.get(id)
|
|
1202
|
+
}));
|
|
1203
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const id of filtered) verdicts.set(id, rotate_pipeline.intent.is_rotatable(doc, id));
|
|
1205
|
+
return {
|
|
1206
|
+
ids_key: ids_key$1(ids),
|
|
1207
|
+
members,
|
|
1208
|
+
pivot,
|
|
1209
|
+
verdicts,
|
|
1210
|
+
preview: this.deps.open_preview(label),
|
|
1211
|
+
last_angle: 0,
|
|
1212
|
+
last_stages: rotate_pipeline.stages.DEFAULT
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
write_preview(session, plan) {
|
|
1216
|
+
const doc = this.deps.get_doc();
|
|
1217
|
+
const emit = this.deps.emit;
|
|
1218
|
+
session.preview.set({
|
|
1219
|
+
providerId: PROVIDER_ID$1,
|
|
1220
|
+
apply: () => {
|
|
1221
|
+
rotate_pipeline.apply(doc, plan);
|
|
1222
|
+
emit();
|
|
1223
|
+
},
|
|
1224
|
+
revert: () => {
|
|
1225
|
+
rotate_pipeline.revert(doc, plan);
|
|
1226
|
+
emit();
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
dispose_session() {
|
|
1231
|
+
this.active = null;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function compute_union_center(ids, bbox_world) {
|
|
1235
|
+
const rects = ids.map(bbox_world);
|
|
1236
|
+
const u = _grida_cmath.default.rect.union(rects);
|
|
1237
|
+
return {
|
|
1238
|
+
x: u.x + u.width / 2,
|
|
1239
|
+
y: u.y + u.height / 2
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/core/hit-shape-svg.ts
|
|
1244
|
+
let hit_shape_svg;
|
|
1245
|
+
(function(_hit_shape_svg) {
|
|
1246
|
+
/** Tags that never participate in picking. Containers and non-
|
|
1247
|
+
* rendering metadata. The root `<svg>` is in this set — root
|
|
1248
|
+
* pickability is a host decision (measurement HUD wants it,
|
|
1249
|
+
* selection doesn't), gated by an explicit `allow_root` flag at the
|
|
1250
|
+
* caller. */
|
|
1251
|
+
const TRANSPARENT_TAGS = new Set([
|
|
1252
|
+
"g",
|
|
1253
|
+
"svg",
|
|
1254
|
+
"defs",
|
|
1255
|
+
"symbol",
|
|
1256
|
+
"clipPath",
|
|
1257
|
+
"mask",
|
|
1258
|
+
"marker",
|
|
1259
|
+
"pattern",
|
|
1260
|
+
"linearGradient",
|
|
1261
|
+
"radialGradient",
|
|
1262
|
+
"stop",
|
|
1263
|
+
"filter",
|
|
1264
|
+
"title",
|
|
1265
|
+
"desc",
|
|
1266
|
+
"metadata",
|
|
1267
|
+
"style",
|
|
1268
|
+
"script"
|
|
1269
|
+
]);
|
|
1270
|
+
function is_transparent_tag(tag) {
|
|
1271
|
+
return TRANSPARENT_TAGS.has(tag);
|
|
1272
|
+
}
|
|
1273
|
+
_hit_shape_svg.is_transparent_tag = is_transparent_tag;
|
|
1274
|
+
function num(doc, id, name, fallback = 0) {
|
|
1275
|
+
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
1276
|
+
}
|
|
1277
|
+
function of_doc(doc, id) {
|
|
1278
|
+
const tag = doc.tag_of(id);
|
|
1279
|
+
const ops = transform.parse(doc.get_attr(id, "transform"));
|
|
1280
|
+
if (ops === null) return null;
|
|
1281
|
+
if (transform.classify(ops) === "mixed") return null;
|
|
1282
|
+
const xform = pick_affine(ops);
|
|
1283
|
+
const has_rotation = xform.angle_rad !== 0;
|
|
1284
|
+
switch (tag) {
|
|
1285
|
+
case "rect": {
|
|
1286
|
+
const x = num(doc, id, "x");
|
|
1287
|
+
const y = num(doc, id, "y");
|
|
1288
|
+
const w = num(doc, id, "width");
|
|
1289
|
+
const h = num(doc, id, "height");
|
|
1290
|
+
if (!has_rotation) {
|
|
1291
|
+
const t = xform.translate;
|
|
1292
|
+
return {
|
|
1293
|
+
kind: "rect",
|
|
1294
|
+
x: x + t.x,
|
|
1295
|
+
y: y + t.y,
|
|
1296
|
+
width: w,
|
|
1297
|
+
height: h
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
kind: "polygon",
|
|
1302
|
+
pts: [
|
|
1303
|
+
{
|
|
1304
|
+
x,
|
|
1305
|
+
y
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
x: x + w,
|
|
1309
|
+
y
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
x: x + w,
|
|
1313
|
+
y: y + h
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
x,
|
|
1317
|
+
y: y + h
|
|
1318
|
+
}
|
|
1319
|
+
].map((p) => transform_point(p, xform)),
|
|
1320
|
+
closed: true
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
case "circle": {
|
|
1324
|
+
const cx = num(doc, id, "cx");
|
|
1325
|
+
const cy = num(doc, id, "cy");
|
|
1326
|
+
const r = num(doc, id, "r");
|
|
1327
|
+
const tc = transform_point({
|
|
1328
|
+
x: cx,
|
|
1329
|
+
y: cy
|
|
1330
|
+
}, xform);
|
|
1331
|
+
return {
|
|
1332
|
+
kind: "ellipse",
|
|
1333
|
+
cx: tc.x,
|
|
1334
|
+
cy: tc.y,
|
|
1335
|
+
rx: r,
|
|
1336
|
+
ry: r
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
case "ellipse": {
|
|
1340
|
+
const cx = num(doc, id, "cx");
|
|
1341
|
+
const cy = num(doc, id, "cy");
|
|
1342
|
+
const rx = num(doc, id, "rx");
|
|
1343
|
+
const ry = num(doc, id, "ry");
|
|
1344
|
+
if (has_rotation) return null;
|
|
1345
|
+
const tc = transform_point({
|
|
1346
|
+
x: cx,
|
|
1347
|
+
y: cy
|
|
1348
|
+
}, xform);
|
|
1349
|
+
return {
|
|
1350
|
+
kind: "ellipse",
|
|
1351
|
+
cx: tc.x,
|
|
1352
|
+
cy: tc.y,
|
|
1353
|
+
rx,
|
|
1354
|
+
ry
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
case "line": return {
|
|
1358
|
+
kind: "segment",
|
|
1359
|
+
a: transform_point({
|
|
1360
|
+
x: num(doc, id, "x1"),
|
|
1361
|
+
y: num(doc, id, "y1")
|
|
1362
|
+
}, xform),
|
|
1363
|
+
b: transform_point({
|
|
1364
|
+
x: num(doc, id, "x2"),
|
|
1365
|
+
y: num(doc, id, "y2")
|
|
1366
|
+
}, xform)
|
|
1367
|
+
};
|
|
1368
|
+
case "polyline": {
|
|
1369
|
+
const pts = _grida_svg_parse.svg_parse.parse_points(doc.get_attr(id, "points") ?? "");
|
|
1370
|
+
if (pts.length === 0) return null;
|
|
1371
|
+
return {
|
|
1372
|
+
kind: "polyline",
|
|
1373
|
+
pts: pts.map((q) => transform_point({
|
|
1374
|
+
x: q.x,
|
|
1375
|
+
y: q.y
|
|
1376
|
+
}, xform)),
|
|
1377
|
+
closed: false
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
case "polygon": {
|
|
1381
|
+
const pts = _grida_svg_parse.svg_parse.parse_points(doc.get_attr(id, "points") ?? "");
|
|
1382
|
+
if (pts.length === 0) return null;
|
|
1383
|
+
return {
|
|
1384
|
+
kind: "polygon",
|
|
1385
|
+
pts: pts.map((q) => transform_point({
|
|
1386
|
+
x: q.x,
|
|
1387
|
+
y: q.y
|
|
1388
|
+
}, xform)),
|
|
1389
|
+
closed: true
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
case "path": {
|
|
1393
|
+
const d = doc.get_attr(id, "d") ?? "";
|
|
1394
|
+
const pts = path_control_polyline(d);
|
|
1395
|
+
if (pts.length === 0) return null;
|
|
1396
|
+
return {
|
|
1397
|
+
kind: "path",
|
|
1398
|
+
pts: pts.map((p) => transform_point(p, xform)),
|
|
1399
|
+
closed: /[zZ]\s*$/.test(d)
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
default: return null;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
_hit_shape_svg.of_doc = of_doc;
|
|
1406
|
+
function pick_affine(ops) {
|
|
1407
|
+
let tx = 0;
|
|
1408
|
+
let ty = 0;
|
|
1409
|
+
let pivot = {
|
|
1410
|
+
x: 0,
|
|
1411
|
+
y: 0
|
|
1412
|
+
};
|
|
1413
|
+
let angle_rad = 0;
|
|
1414
|
+
for (const op of ops) if (op.type === "translate") {
|
|
1415
|
+
tx = op.tx;
|
|
1416
|
+
ty = op.ty;
|
|
1417
|
+
} else if (op.type === "rotate") {
|
|
1418
|
+
pivot = {
|
|
1419
|
+
x: op.cx,
|
|
1420
|
+
y: op.cy
|
|
1421
|
+
};
|
|
1422
|
+
angle_rad = op.angle * Math.PI / 180;
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
translate: {
|
|
1426
|
+
x: tx,
|
|
1427
|
+
y: ty
|
|
1428
|
+
},
|
|
1429
|
+
pivot,
|
|
1430
|
+
angle_rad
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
/** Apply (rotate around pivot by angle_rad, then translate). Mirrors
|
|
1434
|
+
* the SVG transform-list semantic order: `translate(...)
|
|
1435
|
+
* rotate(...)` means matrix product T·R applied to P → translation
|
|
1436
|
+
* moves the *rotated* point. */
|
|
1437
|
+
function transform_point(p, x) {
|
|
1438
|
+
let q = p;
|
|
1439
|
+
if (x.angle_rad !== 0) {
|
|
1440
|
+
const c = Math.cos(x.angle_rad);
|
|
1441
|
+
const s = Math.sin(x.angle_rad);
|
|
1442
|
+
const dx = q.x - x.pivot.x;
|
|
1443
|
+
const dy = q.y - x.pivot.y;
|
|
1444
|
+
q = {
|
|
1445
|
+
x: x.pivot.x + dx * c - dy * s,
|
|
1446
|
+
y: x.pivot.y + dx * s + dy * c
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
if (x.translate.x !== 0 || x.translate.y !== 0) q = {
|
|
1450
|
+
x: q.x + x.translate.x,
|
|
1451
|
+
y: q.y + x.translate.y
|
|
1452
|
+
};
|
|
1453
|
+
return q;
|
|
1454
|
+
}
|
|
1455
|
+
function path_control_polyline(d) {
|
|
1456
|
+
if (!d) return [];
|
|
1457
|
+
let path;
|
|
1458
|
+
try {
|
|
1459
|
+
path = new _grida_svg_pathdata.SVGPathData(d).toAbs();
|
|
1460
|
+
} catch {
|
|
1461
|
+
return [];
|
|
1462
|
+
}
|
|
1463
|
+
const out = [];
|
|
1464
|
+
for (const cmd of path.commands) {
|
|
1465
|
+
const c = cmd;
|
|
1466
|
+
if (typeof c.x1 === "number" && typeof c.y1 === "number") out.push({
|
|
1467
|
+
x: c.x1,
|
|
1468
|
+
y: c.y1
|
|
1469
|
+
});
|
|
1470
|
+
if (typeof c.x2 === "number" && typeof c.y2 === "number") out.push({
|
|
1471
|
+
x: c.x2,
|
|
1472
|
+
y: c.y2
|
|
1473
|
+
});
|
|
1474
|
+
if (typeof c.x === "number" && typeof c.y === "number") out.push({
|
|
1475
|
+
x: c.x,
|
|
1476
|
+
y: c.y
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return out;
|
|
1480
|
+
}
|
|
1481
|
+
_hit_shape_svg.path_control_polyline = path_control_polyline;
|
|
1482
|
+
})(hit_shape_svg || (hit_shape_svg = {}));
|
|
1483
|
+
//#endregion
|
|
1484
|
+
//#region src/core/policy-class/types.ts
|
|
1485
|
+
/**
|
|
1486
|
+
* The four top-level intents — distinguished as their own constant for
|
|
1487
|
+
* tables that only declare top-level intent cells.
|
|
1488
|
+
*/
|
|
1489
|
+
const TOP_LEVEL_INTENTS = [
|
|
1490
|
+
"resize",
|
|
1491
|
+
"translate",
|
|
1492
|
+
"rotate",
|
|
1493
|
+
"enter-vector-edit"
|
|
1494
|
+
];
|
|
1495
|
+
/**
|
|
1496
|
+
* The vector-edit sub-intents — the atomic operations exposed inside
|
|
1497
|
+
* vector-editing mode. See the v1 intent coverage section of the
|
|
1498
|
+
* glossary doc for the per-class table.
|
|
1499
|
+
*/
|
|
1500
|
+
const VECTOR_EDIT_SUB_INTENTS = [
|
|
1501
|
+
"translate-vertex",
|
|
1502
|
+
"insert-vertex",
|
|
1503
|
+
"delete-vertex",
|
|
1504
|
+
"close-shape",
|
|
1505
|
+
"open-shape",
|
|
1506
|
+
"insert-tangent",
|
|
1507
|
+
"adjust-tangent",
|
|
1508
|
+
"convert-segment-type",
|
|
1509
|
+
"adjust-arc-radii",
|
|
1510
|
+
"split-sub-path"
|
|
1511
|
+
];
|
|
1512
|
+
[...TOP_LEVEL_INTENTS, ...VECTOR_EDIT_SUB_INTENTS];
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/core/policy-class/index.ts
|
|
1515
|
+
let policy_class;
|
|
1516
|
+
(function(_policy_class) {
|
|
1517
|
+
function of(tag) {
|
|
1518
|
+
switch (tag) {
|
|
1519
|
+
case "line":
|
|
1520
|
+
case "polyline":
|
|
1521
|
+
case "polygon": return "vertex-chain";
|
|
1522
|
+
case "rect":
|
|
1523
|
+
case "image":
|
|
1524
|
+
case "use": return "vertex-box";
|
|
1525
|
+
case "circle": return "circle";
|
|
1526
|
+
case "ellipse": return "ellipse";
|
|
1527
|
+
case "path": return "path";
|
|
1528
|
+
case "text":
|
|
1529
|
+
case "tspan": return "text";
|
|
1530
|
+
case "g": return "group";
|
|
1531
|
+
default: return "none";
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
_policy_class.of = of;
|
|
1535
|
+
const SOLUTION_SPACE = {
|
|
1536
|
+
"vertex-chain": {
|
|
1537
|
+
resize: ["bake"],
|
|
1538
|
+
translate: ["bake", "via-transform"],
|
|
1539
|
+
rotate: ["via-transform"],
|
|
1540
|
+
"enter-vector-edit": ["bake"],
|
|
1541
|
+
"translate-vertex": ["bake"],
|
|
1542
|
+
"insert-vertex": ["bake"],
|
|
1543
|
+
"delete-vertex": ["bake", "restrict"],
|
|
1544
|
+
"close-shape": ["promote"],
|
|
1545
|
+
"open-shape": ["promote"]
|
|
1546
|
+
},
|
|
1547
|
+
"vertex-box": {
|
|
1548
|
+
resize: ["bake"],
|
|
1549
|
+
translate: ["bake", "via-transform"],
|
|
1550
|
+
rotate: ["via-transform"]
|
|
1551
|
+
},
|
|
1552
|
+
circle: {
|
|
1553
|
+
resize: [
|
|
1554
|
+
"restrict",
|
|
1555
|
+
"promote",
|
|
1556
|
+
"via-transform"
|
|
1557
|
+
],
|
|
1558
|
+
translate: ["bake"],
|
|
1559
|
+
rotate: ["via-transform"]
|
|
1560
|
+
},
|
|
1561
|
+
ellipse: {
|
|
1562
|
+
resize: ["bake", "via-transform"],
|
|
1563
|
+
translate: ["bake"],
|
|
1564
|
+
rotate: ["via-transform"]
|
|
1565
|
+
},
|
|
1566
|
+
path: {
|
|
1567
|
+
resize: ["bake", "via-transform"],
|
|
1568
|
+
translate: ["bake", "via-transform"],
|
|
1569
|
+
rotate: ["bake", "via-transform"],
|
|
1570
|
+
"enter-vector-edit": ["bake"],
|
|
1571
|
+
"translate-vertex": ["bake"],
|
|
1572
|
+
"insert-vertex": ["bake"],
|
|
1573
|
+
"delete-vertex": ["bake"],
|
|
1574
|
+
"close-shape": ["bake"],
|
|
1575
|
+
"open-shape": ["bake"],
|
|
1576
|
+
"insert-tangent": ["bake"],
|
|
1577
|
+
"adjust-tangent": ["bake"],
|
|
1578
|
+
"convert-segment-type": ["bake"],
|
|
1579
|
+
"adjust-arc-radii": ["bake"],
|
|
1580
|
+
"split-sub-path": ["bake"]
|
|
1581
|
+
},
|
|
1582
|
+
text: {},
|
|
1583
|
+
group: {
|
|
1584
|
+
translate: ["via-transform"],
|
|
1585
|
+
rotate: ["via-transform"]
|
|
1586
|
+
},
|
|
1587
|
+
none: {}
|
|
1588
|
+
};
|
|
1589
|
+
const CHOSEN_POLICY = {
|
|
1590
|
+
"vertex-chain": {
|
|
1591
|
+
resize: "bake",
|
|
1592
|
+
translate: "bake",
|
|
1593
|
+
rotate: "via-transform",
|
|
1594
|
+
"enter-vector-edit": "bake",
|
|
1595
|
+
"translate-vertex": "bake",
|
|
1596
|
+
"insert-vertex": "bake",
|
|
1597
|
+
"delete-vertex": "restrict",
|
|
1598
|
+
"close-shape": "promote",
|
|
1599
|
+
"open-shape": "promote"
|
|
1600
|
+
},
|
|
1601
|
+
"vertex-box": {
|
|
1602
|
+
resize: "bake",
|
|
1603
|
+
translate: "bake",
|
|
1604
|
+
rotate: "via-transform"
|
|
1605
|
+
},
|
|
1606
|
+
circle: {
|
|
1607
|
+
resize: "restrict",
|
|
1608
|
+
translate: "bake",
|
|
1609
|
+
rotate: "via-transform"
|
|
1610
|
+
},
|
|
1611
|
+
ellipse: {
|
|
1612
|
+
resize: "bake",
|
|
1613
|
+
translate: "bake",
|
|
1614
|
+
rotate: "via-transform"
|
|
1615
|
+
},
|
|
1616
|
+
path: {
|
|
1617
|
+
resize: "bake",
|
|
1618
|
+
translate: "bake",
|
|
1619
|
+
rotate: "via-transform"
|
|
1620
|
+
},
|
|
1621
|
+
text: {},
|
|
1622
|
+
group: {
|
|
1623
|
+
translate: "via-transform",
|
|
1624
|
+
rotate: "via-transform"
|
|
1625
|
+
},
|
|
1626
|
+
none: {}
|
|
1627
|
+
};
|
|
1628
|
+
/** Shared empty solution-space — returned on misses to avoid per-call
|
|
1629
|
+
* array allocation. */
|
|
1630
|
+
const EMPTY = Object.freeze([]);
|
|
1631
|
+
function legal_solutions(cls, intent) {
|
|
1632
|
+
return SOLUTION_SPACE[cls][intent] ?? EMPTY;
|
|
1633
|
+
}
|
|
1634
|
+
_policy_class.legal_solutions = legal_solutions;
|
|
1635
|
+
function chosen_policy(cls, intent) {
|
|
1636
|
+
return CHOSEN_POLICY[cls][intent];
|
|
1637
|
+
}
|
|
1638
|
+
_policy_class.chosen_policy = chosen_policy;
|
|
1639
|
+
function accepts(cls, intent) {
|
|
1640
|
+
return legal_solutions(cls, intent).length > 0;
|
|
1641
|
+
}
|
|
1642
|
+
_policy_class.accepts = accepts;
|
|
1643
|
+
function fork_count(cls, intent) {
|
|
1644
|
+
return legal_solutions(cls, intent).length;
|
|
1645
|
+
}
|
|
1646
|
+
_policy_class.fork_count = fork_count;
|
|
1647
|
+
_policy_class._internal_SOLUTION_SPACE = SOLUTION_SPACE;
|
|
1648
|
+
_policy_class._internal_CHOSEN_POLICY = CHOSEN_POLICY;
|
|
1649
|
+
})(policy_class || (policy_class = {}));
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/core/policy-class/handlers/resize.ts
|
|
1652
|
+
function scale_points_string(points, origin, sx, sy) {
|
|
1653
|
+
return _grida_svg_parse.svg_parse.parse_points(points).map((p) => {
|
|
1654
|
+
return `${origin.x + (p.x - origin.x) * sx},${origin.y + (p.y - origin.y) * sy}`;
|
|
1655
|
+
}).join(" ");
|
|
1656
|
+
}
|
|
1657
|
+
function scale_path_d(d, origin, sx, sy) {
|
|
1658
|
+
try {
|
|
1659
|
+
const e = origin.x * (1 - sx);
|
|
1660
|
+
const f = origin.y * (1 - sy);
|
|
1661
|
+
return new _grida_svg_pathdata.SVGPathData(d).transform(_grida_svg_pathdata.SVGPathDataTransformer.MATRIX(sx, 0, 0, sy, e, f)).encode();
|
|
1662
|
+
} catch {
|
|
1663
|
+
return d;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* VertexChain × resize — vertex transport in local space.
|
|
1668
|
+
* Line carries its own (x1, y1, x2, y2); polyline / polygon share `points`.
|
|
1669
|
+
* Result type is preserved.
|
|
1670
|
+
*/
|
|
1671
|
+
function resize_vertex_chain(doc, id, baseline, sx, sy, origin) {
|
|
1672
|
+
const a = baseline.attrs;
|
|
1673
|
+
if (a.kind === "line") {
|
|
1674
|
+
doc.set_attr(id, "x1", String(origin.x + (a.x1 - origin.x) * sx));
|
|
1675
|
+
doc.set_attr(id, "y1", String(origin.y + (a.y1 - origin.y) * sy));
|
|
1676
|
+
doc.set_attr(id, "x2", String(origin.x + (a.x2 - origin.x) * sx));
|
|
1677
|
+
doc.set_attr(id, "y2", String(origin.y + (a.y2 - origin.y) * sy));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (a.kind === "polyline" || a.kind === "polygon") {
|
|
1681
|
+
doc.set_attr(id, "points", scale_points_string(a.points, origin, sx, sy));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* VertexBox × resize — axis-aligned bounding-box transport (rect / image / use).
|
|
1687
|
+
* Width/height clamp at 0.001 prevents inverted boxes and degenerate writes.
|
|
1688
|
+
*/
|
|
1689
|
+
function resize_vertex_box(doc, id, baseline, sx, sy, origin) {
|
|
1690
|
+
const a = baseline.attrs;
|
|
1691
|
+
if (a.kind !== "rect" && a.kind !== "image" && a.kind !== "use") return;
|
|
1692
|
+
doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * sx));
|
|
1693
|
+
doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * sy));
|
|
1694
|
+
doc.set_attr(id, "width", String(Math.max(.001, a.w * sx)));
|
|
1695
|
+
doc.set_attr(id, "height", String(Math.max(.001, a.h * sy)));
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Circle × resize — the canonical Policy Class fork.
|
|
1699
|
+
*
|
|
1700
|
+
* v1 picks `restrict`: clamp the gesture onto the `rx = ry` constraint
|
|
1701
|
+
* surface (`s = min(sx, sy)`) and bake the projection. The circle stays
|
|
1702
|
+
* a circle. `promote` and `via-transform` are declared legal but
|
|
1703
|
+
* unimplemented; the exhaustive switch surfaces any future swap.
|
|
1704
|
+
*/
|
|
1705
|
+
function resize_circle(doc, id, baseline, sx, sy, origin) {
|
|
1706
|
+
const a = baseline.attrs;
|
|
1707
|
+
if (a.kind !== "circle") return;
|
|
1708
|
+
const policy = policy_class.chosen_policy("circle", "resize");
|
|
1709
|
+
switch (policy) {
|
|
1710
|
+
case "restrict": {
|
|
1711
|
+
const s = Math.min(sx, sy);
|
|
1712
|
+
doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * s));
|
|
1713
|
+
doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * s));
|
|
1714
|
+
doc.set_attr(id, "r", String(Math.max(.001, a.r * s)));
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
case "promote":
|
|
1718
|
+
case "via-transform":
|
|
1719
|
+
case "bake": throw new Error(`Circle resize policy '${policy}' is legal per Policy Class but not implemented in v1`);
|
|
1720
|
+
case void 0: throw new Error("Circle resize has no chosen policy declared in Policy Class");
|
|
1721
|
+
default: throw new Error("unreachable");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Ellipse × resize — independent rx, ry. No constraint to enforce
|
|
1726
|
+
* (ellipse is already the general axis-aligned-radii form).
|
|
1727
|
+
*/
|
|
1728
|
+
function resize_ellipse(doc, id, baseline, sx, sy, origin) {
|
|
1729
|
+
const a = baseline.attrs;
|
|
1730
|
+
if (a.kind !== "ellipse") return;
|
|
1731
|
+
doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * sx));
|
|
1732
|
+
doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * sy));
|
|
1733
|
+
doc.set_attr(id, "rx", String(Math.max(.001, a.rx * sx)));
|
|
1734
|
+
doc.set_attr(id, "ry", String(Math.max(.001, a.ry * sy)));
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Path × resize — bake the affine into every segment of `d` via
|
|
1738
|
+
* svg-pathdata's MATRIX transformer. Curve handles, arc radii, and
|
|
1739
|
+
* segment endpoints scale together; segment types are preserved.
|
|
1740
|
+
*/
|
|
1741
|
+
function resize_path(doc, id, baseline, sx, sy, origin) {
|
|
1742
|
+
const a = baseline.attrs;
|
|
1743
|
+
if (a.kind !== "path") return;
|
|
1744
|
+
doc.set_attr(id, "d", scale_path_d(a.d, origin, sx, sy));
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Text × resize.
|
|
1748
|
+
*
|
|
1749
|
+
* **Unnatural — see UNNATURAL.md.** Text class is "deferred" in Policy
|
|
1750
|
+
* Class (no resize cells declared) but accepted by the legacy
|
|
1751
|
+
* `is_resizable` capability gate. This handler preserves legacy
|
|
1752
|
+
* behavior verbatim:
|
|
1753
|
+
*
|
|
1754
|
+
* - Edge drags (one of sx, sy is 1) are refused.
|
|
1755
|
+
* - Corner drags scale (x, y, font-size) uniformly by min(sx, sy).
|
|
1756
|
+
*
|
|
1757
|
+
* When Text class is properly declared in Policy Class, the policy
|
|
1758
|
+
* switch above will gain a `text` branch and this handler's body will
|
|
1759
|
+
* either re-route through `chosen_policy("text", "resize")` or split
|
|
1760
|
+
* by sub-class (font-resize vs. container-box-resize is a design
|
|
1761
|
+
* question for the Text class work).
|
|
1762
|
+
*/
|
|
1763
|
+
function resize_text(doc, id, baseline, sx, sy, origin) {
|
|
1764
|
+
const a = baseline.attrs;
|
|
1765
|
+
if (a.kind !== "text") return;
|
|
1766
|
+
if (!(sx !== 1 && sy !== 1)) return;
|
|
1767
|
+
const s = Math.min(sx, sy);
|
|
1768
|
+
doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * s));
|
|
1769
|
+
doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * s));
|
|
1770
|
+
doc.set_attr(id, "font-size", String(Math.max(1, a.fontSize * s)));
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Resize dispatch through Policy Class.
|
|
1774
|
+
*
|
|
1775
|
+
* Replaces the nine-arm tag switch in `intents.ts:apply_resize`. The
|
|
1776
|
+
* caller (`apply_resize`) wraps this with the commit-phase pivot
|
|
1777
|
+
* recomposition; this function does **only** the geometry write.
|
|
1778
|
+
*
|
|
1779
|
+
* Classes rejected by Policy Class (`group`, `none`) noop silently.
|
|
1780
|
+
* Text is carved out as the one known gap (see Text handler doc).
|
|
1781
|
+
*/
|
|
1782
|
+
function dispatch_resize(doc, id, baseline, sx, sy, origin) {
|
|
1783
|
+
switch (policy_class.of(doc.tag_of(id))) {
|
|
1784
|
+
case "vertex-chain": return resize_vertex_chain(doc, id, baseline, sx, sy, origin);
|
|
1785
|
+
case "vertex-box": return resize_vertex_box(doc, id, baseline, sx, sy, origin);
|
|
1786
|
+
case "circle": return resize_circle(doc, id, baseline, sx, sy, origin);
|
|
1787
|
+
case "ellipse": return resize_ellipse(doc, id, baseline, sx, sy, origin);
|
|
1788
|
+
case "path": return resize_path(doc, id, baseline, sx, sy, origin);
|
|
1789
|
+
case "text": return resize_text(doc, id, baseline, sx, sy, origin);
|
|
1790
|
+
case "group":
|
|
1791
|
+
case "none": return;
|
|
1792
|
+
default: return;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region src/core/resize-capability.ts
|
|
1797
|
+
let resize_capability;
|
|
1798
|
+
(function(_resize_capability) {
|
|
1799
|
+
function direction_mask(dir) {
|
|
1800
|
+
const has_n = dir === "n" || dir === "ne" || dir === "nw";
|
|
1801
|
+
const has_s = dir === "s" || dir === "se" || dir === "sw";
|
|
1802
|
+
const has_e = dir === "e" || dir === "ne" || dir === "se";
|
|
1803
|
+
const has_w = dir === "w" || dir === "nw" || dir === "sw";
|
|
1804
|
+
return {
|
|
1805
|
+
affects_x: has_e || has_w,
|
|
1806
|
+
affects_y: has_n || has_s,
|
|
1807
|
+
x_edge: has_e ? "right" : has_w ? "left" : null,
|
|
1808
|
+
y_edge: has_n ? "top" : has_s ? "bottom" : null
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
_resize_capability.direction_mask = direction_mask;
|
|
1812
|
+
function is_corner(dir) {
|
|
1813
|
+
return dir === "nw" || dir === "ne" || dir === "se" || dir === "sw";
|
|
1814
|
+
}
|
|
1815
|
+
_resize_capability.is_corner = is_corner;
|
|
1816
|
+
function constraint(baseline, dir, sx_gesture, sy_gesture) {
|
|
1817
|
+
switch (baseline.attrs.kind) {
|
|
1818
|
+
case "rect":
|
|
1819
|
+
case "image":
|
|
1820
|
+
case "use":
|
|
1821
|
+
case "ellipse":
|
|
1822
|
+
case "line":
|
|
1823
|
+
case "polyline":
|
|
1824
|
+
case "polygon":
|
|
1825
|
+
case "path": return {
|
|
1826
|
+
sx: sx_gesture,
|
|
1827
|
+
sy: sy_gesture,
|
|
1828
|
+
no_op: false,
|
|
1829
|
+
uniform: false
|
|
1830
|
+
};
|
|
1831
|
+
case "circle": {
|
|
1832
|
+
const s = Math.min(sx_gesture, sy_gesture);
|
|
1833
|
+
return {
|
|
1834
|
+
sx: s,
|
|
1835
|
+
sy: s,
|
|
1836
|
+
no_op: false,
|
|
1837
|
+
uniform: true
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
case "text": {
|
|
1841
|
+
if (!is_corner(dir)) return {
|
|
1842
|
+
sx: 1,
|
|
1843
|
+
sy: 1,
|
|
1844
|
+
no_op: true,
|
|
1845
|
+
uniform: true
|
|
1846
|
+
};
|
|
1847
|
+
const s = Math.min(sx_gesture, sy_gesture);
|
|
1848
|
+
return {
|
|
1849
|
+
sx: s,
|
|
1850
|
+
sy: s,
|
|
1851
|
+
no_op: false,
|
|
1852
|
+
uniform: true
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
case "unsupported": return {
|
|
1856
|
+
sx: 1,
|
|
1857
|
+
sy: 1,
|
|
1858
|
+
no_op: true,
|
|
1859
|
+
uniform: false
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
_resize_capability.constraint = constraint;
|
|
1864
|
+
function corner_of_rect(r, dir) {
|
|
1865
|
+
switch (dir) {
|
|
1866
|
+
case "nw": return {
|
|
1867
|
+
x: r.x,
|
|
1868
|
+
y: r.y
|
|
1869
|
+
};
|
|
1870
|
+
case "n": return {
|
|
1871
|
+
x: r.x + r.width / 2,
|
|
1872
|
+
y: r.y
|
|
1873
|
+
};
|
|
1874
|
+
case "ne": return {
|
|
1875
|
+
x: r.x + r.width,
|
|
1876
|
+
y: r.y
|
|
1877
|
+
};
|
|
1878
|
+
case "e": return {
|
|
1879
|
+
x: r.x + r.width,
|
|
1880
|
+
y: r.y + r.height / 2
|
|
1881
|
+
};
|
|
1882
|
+
case "se": return {
|
|
1883
|
+
x: r.x + r.width,
|
|
1884
|
+
y: r.y + r.height
|
|
1885
|
+
};
|
|
1886
|
+
case "s": return {
|
|
1887
|
+
x: r.x + r.width / 2,
|
|
1888
|
+
y: r.y + r.height
|
|
1889
|
+
};
|
|
1890
|
+
case "sw": return {
|
|
1891
|
+
x: r.x,
|
|
1892
|
+
y: r.y + r.height
|
|
1893
|
+
};
|
|
1894
|
+
case "w": return {
|
|
1895
|
+
x: r.x,
|
|
1896
|
+
y: r.y + r.height / 2
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
_resize_capability.corner_of_rect = corner_of_rect;
|
|
1901
|
+
function origin_of_direction(r, dir) {
|
|
1902
|
+
switch (dir) {
|
|
1903
|
+
case "nw": return {
|
|
1904
|
+
x: r.x + r.width,
|
|
1905
|
+
y: r.y + r.height
|
|
1906
|
+
};
|
|
1907
|
+
case "n": return {
|
|
1908
|
+
x: r.x + r.width / 2,
|
|
1909
|
+
y: r.y + r.height
|
|
1910
|
+
};
|
|
1911
|
+
case "ne": return {
|
|
1912
|
+
x: r.x,
|
|
1913
|
+
y: r.y + r.height
|
|
1914
|
+
};
|
|
1915
|
+
case "e": return {
|
|
1916
|
+
x: r.x,
|
|
1917
|
+
y: r.y + r.height / 2
|
|
1918
|
+
};
|
|
1919
|
+
case "se": return {
|
|
1920
|
+
x: r.x,
|
|
1921
|
+
y: r.y
|
|
1922
|
+
};
|
|
1923
|
+
case "s": return {
|
|
1924
|
+
x: r.x + r.width / 2,
|
|
1925
|
+
y: r.y
|
|
1926
|
+
};
|
|
1927
|
+
case "sw": return {
|
|
1928
|
+
x: r.x + r.width,
|
|
1929
|
+
y: r.y
|
|
1930
|
+
};
|
|
1931
|
+
case "w": return {
|
|
1932
|
+
x: r.x + r.width,
|
|
1933
|
+
y: r.y + r.height / 2
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
_resize_capability.origin_of_direction = origin_of_direction;
|
|
1938
|
+
function effective(baseline, dir, sx_gesture, sy_gesture) {
|
|
1939
|
+
const bbox = baseline.bbox;
|
|
1940
|
+
const origin = origin_of_direction(bbox, dir);
|
|
1941
|
+
const c = constraint(baseline, dir, sx_gesture, sy_gesture);
|
|
1942
|
+
const mask = direction_mask(dir);
|
|
1943
|
+
const rect = {
|
|
1944
|
+
x: origin.x + (bbox.x - origin.x) * c.sx,
|
|
1945
|
+
y: origin.y + (bbox.y - origin.y) * c.sy,
|
|
1946
|
+
width: bbox.width * c.sx,
|
|
1947
|
+
height: bbox.height * c.sy
|
|
1948
|
+
};
|
|
1949
|
+
const moving_corner = corner_of_rect(rect, dir);
|
|
1950
|
+
return {
|
|
1951
|
+
rect,
|
|
1952
|
+
sx: c.sx,
|
|
1953
|
+
sy: c.sy,
|
|
1954
|
+
moving_corner,
|
|
1955
|
+
origin,
|
|
1956
|
+
no_op: c.no_op,
|
|
1957
|
+
uniform: c.uniform,
|
|
1958
|
+
mask
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
_resize_capability.effective = effective;
|
|
1962
|
+
})(resize_capability || (resize_capability = {}));
|
|
1963
|
+
//#endregion
|
|
1964
|
+
//#region src/core/resize-pipeline/resize-pipeline.ts
|
|
1965
|
+
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1966
|
+
let resize_pipeline;
|
|
1967
|
+
(function(_resize_pipeline) {
|
|
1968
|
+
let intent;
|
|
1969
|
+
(function(_intent) {
|
|
1970
|
+
function num(doc, id, name, fallback = 0) {
|
|
1971
|
+
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
1972
|
+
}
|
|
1973
|
+
function is_resizable(tag) {
|
|
1974
|
+
switch (tag) {
|
|
1975
|
+
case "rect":
|
|
1976
|
+
case "image":
|
|
1977
|
+
case "use":
|
|
1978
|
+
case "circle":
|
|
1979
|
+
case "ellipse":
|
|
1980
|
+
case "line":
|
|
1981
|
+
case "polyline":
|
|
1982
|
+
case "polygon":
|
|
1983
|
+
case "path":
|
|
1984
|
+
case "text": return true;
|
|
1985
|
+
default: return false;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
_intent.is_resizable = is_resizable;
|
|
1989
|
+
function capture_baseline(doc, id, bbox) {
|
|
1990
|
+
const tag = doc.tag_of(id);
|
|
1991
|
+
let attrs;
|
|
1992
|
+
switch (tag) {
|
|
1993
|
+
case "rect":
|
|
1994
|
+
attrs = {
|
|
1995
|
+
kind: "rect",
|
|
1996
|
+
x: num(doc, id, "x"),
|
|
1997
|
+
y: num(doc, id, "y"),
|
|
1998
|
+
w: num(doc, id, "width", bbox.width),
|
|
1999
|
+
h: num(doc, id, "height", bbox.height)
|
|
2000
|
+
};
|
|
2001
|
+
break;
|
|
2002
|
+
case "image":
|
|
2003
|
+
attrs = {
|
|
2004
|
+
kind: "image",
|
|
2005
|
+
x: num(doc, id, "x"),
|
|
2006
|
+
y: num(doc, id, "y"),
|
|
2007
|
+
w: num(doc, id, "width", bbox.width),
|
|
2008
|
+
h: num(doc, id, "height", bbox.height)
|
|
2009
|
+
};
|
|
2010
|
+
break;
|
|
2011
|
+
case "use":
|
|
2012
|
+
attrs = {
|
|
2013
|
+
kind: "use",
|
|
2014
|
+
x: num(doc, id, "x"),
|
|
2015
|
+
y: num(doc, id, "y"),
|
|
2016
|
+
w: num(doc, id, "width", bbox.width),
|
|
2017
|
+
h: num(doc, id, "height", bbox.height)
|
|
2018
|
+
};
|
|
2019
|
+
break;
|
|
2020
|
+
case "circle":
|
|
2021
|
+
attrs = {
|
|
2022
|
+
kind: "circle",
|
|
2023
|
+
cx: num(doc, id, "cx"),
|
|
2024
|
+
cy: num(doc, id, "cy"),
|
|
2025
|
+
r: num(doc, id, "r")
|
|
2026
|
+
};
|
|
2027
|
+
break;
|
|
2028
|
+
case "ellipse":
|
|
2029
|
+
attrs = {
|
|
2030
|
+
kind: "ellipse",
|
|
2031
|
+
cx: num(doc, id, "cx"),
|
|
2032
|
+
cy: num(doc, id, "cy"),
|
|
2033
|
+
rx: num(doc, id, "rx"),
|
|
2034
|
+
ry: num(doc, id, "ry")
|
|
2035
|
+
};
|
|
2036
|
+
break;
|
|
2037
|
+
case "line":
|
|
2038
|
+
attrs = {
|
|
2039
|
+
kind: "line",
|
|
2040
|
+
x1: num(doc, id, "x1"),
|
|
2041
|
+
y1: num(doc, id, "y1"),
|
|
2042
|
+
x2: num(doc, id, "x2"),
|
|
2043
|
+
y2: num(doc, id, "y2")
|
|
2044
|
+
};
|
|
2045
|
+
break;
|
|
2046
|
+
case "polyline":
|
|
2047
|
+
attrs = {
|
|
2048
|
+
kind: "polyline",
|
|
2049
|
+
points: doc.get_attr(id, "points") ?? ""
|
|
2050
|
+
};
|
|
2051
|
+
break;
|
|
2052
|
+
case "polygon":
|
|
2053
|
+
attrs = {
|
|
2054
|
+
kind: "polygon",
|
|
2055
|
+
points: doc.get_attr(id, "points") ?? ""
|
|
2056
|
+
};
|
|
2057
|
+
break;
|
|
2058
|
+
case "path":
|
|
2059
|
+
attrs = {
|
|
2060
|
+
kind: "path",
|
|
2061
|
+
d: doc.get_attr(id, "d") ?? ""
|
|
2062
|
+
};
|
|
2063
|
+
break;
|
|
2064
|
+
case "text":
|
|
2065
|
+
attrs = {
|
|
2066
|
+
kind: "text",
|
|
2067
|
+
x: num(doc, id, "x"),
|
|
2068
|
+
y: num(doc, id, "y"),
|
|
2069
|
+
fontSize: parseFloat(doc.get_attr(id, "font-size") ?? "16") || 16
|
|
2070
|
+
};
|
|
2071
|
+
break;
|
|
2072
|
+
default: attrs = { kind: "unsupported" };
|
|
2073
|
+
}
|
|
2074
|
+
return {
|
|
2075
|
+
bbox,
|
|
2076
|
+
attrs
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
_intent.capture_baseline = capture_baseline;
|
|
2080
|
+
function compute_factors(baseline, dir, dx, dy, shift) {
|
|
2081
|
+
const b = baseline.bbox;
|
|
2082
|
+
let anchorX = 0;
|
|
2083
|
+
let anchorY = 0;
|
|
2084
|
+
let baseHX = 0;
|
|
2085
|
+
let baseHY = 0;
|
|
2086
|
+
let affectsX = true;
|
|
2087
|
+
let affectsY = true;
|
|
2088
|
+
switch (dir) {
|
|
2089
|
+
case "nw":
|
|
2090
|
+
anchorX = b.x + b.width;
|
|
2091
|
+
anchorY = b.y + b.height;
|
|
2092
|
+
baseHX = b.x;
|
|
2093
|
+
baseHY = b.y;
|
|
2094
|
+
break;
|
|
2095
|
+
case "n":
|
|
2096
|
+
anchorX = b.x + b.width / 2;
|
|
2097
|
+
anchorY = b.y + b.height;
|
|
2098
|
+
baseHX = b.x + b.width / 2;
|
|
2099
|
+
baseHY = b.y;
|
|
2100
|
+
affectsX = false;
|
|
2101
|
+
break;
|
|
2102
|
+
case "ne":
|
|
2103
|
+
anchorX = b.x;
|
|
2104
|
+
anchorY = b.y + b.height;
|
|
2105
|
+
baseHX = b.x + b.width;
|
|
2106
|
+
baseHY = b.y;
|
|
2107
|
+
break;
|
|
2108
|
+
case "e":
|
|
2109
|
+
anchorX = b.x;
|
|
2110
|
+
anchorY = b.y + b.height / 2;
|
|
2111
|
+
baseHX = b.x + b.width;
|
|
2112
|
+
baseHY = b.y + b.height / 2;
|
|
2113
|
+
affectsY = false;
|
|
2114
|
+
break;
|
|
2115
|
+
case "se":
|
|
2116
|
+
anchorX = b.x;
|
|
2117
|
+
anchorY = b.y;
|
|
2118
|
+
baseHX = b.x + b.width;
|
|
2119
|
+
baseHY = b.y + b.height;
|
|
2120
|
+
break;
|
|
2121
|
+
case "s":
|
|
2122
|
+
anchorX = b.x + b.width / 2;
|
|
2123
|
+
anchorY = b.y;
|
|
2124
|
+
baseHX = b.x + b.width / 2;
|
|
2125
|
+
baseHY = b.y + b.height;
|
|
2126
|
+
affectsX = false;
|
|
2127
|
+
break;
|
|
2128
|
+
case "sw":
|
|
2129
|
+
anchorX = b.x + b.width;
|
|
2130
|
+
anchorY = b.y;
|
|
2131
|
+
baseHX = b.x;
|
|
2132
|
+
baseHY = b.y + b.height;
|
|
2133
|
+
break;
|
|
2134
|
+
case "w":
|
|
2135
|
+
anchorX = b.x + b.width;
|
|
2136
|
+
anchorY = b.y + b.height / 2;
|
|
2137
|
+
baseHX = b.x;
|
|
2138
|
+
baseHY = b.y + b.height / 2;
|
|
2139
|
+
affectsY = false;
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
const newHX = baseHX + (affectsX ? dx : 0);
|
|
2143
|
+
const newHY = baseHY + (affectsY ? dy : 0);
|
|
2144
|
+
const denomX = baseHX - anchorX;
|
|
2145
|
+
const denomY = baseHY - anchorY;
|
|
2146
|
+
let sx = affectsX && denomX !== 0 ? (newHX - anchorX) / denomX : 1;
|
|
2147
|
+
let sy = affectsY && denomY !== 0 ? (newHY - anchorY) / denomY : 1;
|
|
2148
|
+
if (shift && affectsX && affectsY) {
|
|
2149
|
+
const mag = Math.max(Math.abs(sx), Math.abs(sy));
|
|
2150
|
+
sx = sx >= 0 ? mag : -mag;
|
|
2151
|
+
sy = sy >= 0 ? mag : -mag;
|
|
2152
|
+
}
|
|
2153
|
+
sx = Math.max(.001, sx);
|
|
2154
|
+
sy = Math.max(.001, sy);
|
|
2155
|
+
return {
|
|
2156
|
+
sx,
|
|
2157
|
+
sy,
|
|
2158
|
+
origin: {
|
|
2159
|
+
x: anchorX,
|
|
2160
|
+
y: anchorY
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
_intent.compute_factors = compute_factors;
|
|
2165
|
+
function bbox_center(points) {
|
|
2166
|
+
if (points.length === 0) return null;
|
|
2167
|
+
const r = _grida_cmath.default.rect.fromPointsOrZero(points.map((p) => [p.x, p.y]));
|
|
2168
|
+
return {
|
|
2169
|
+
cx: r.x + r.width / 2,
|
|
2170
|
+
cy: r.y + r.height / 2
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function new_local_center(doc, id) {
|
|
2174
|
+
switch (doc.tag_of(id)) {
|
|
2175
|
+
case "rect":
|
|
2176
|
+
case "image":
|
|
2177
|
+
case "use": {
|
|
2178
|
+
const x = num(doc, id, "x");
|
|
2179
|
+
const y = num(doc, id, "y");
|
|
2180
|
+
return {
|
|
2181
|
+
cx: x + num(doc, id, "width") / 2,
|
|
2182
|
+
cy: y + num(doc, id, "height") / 2
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
case "circle":
|
|
2186
|
+
case "ellipse": return {
|
|
2187
|
+
cx: num(doc, id, "cx"),
|
|
2188
|
+
cy: num(doc, id, "cy")
|
|
2189
|
+
};
|
|
2190
|
+
case "line": {
|
|
2191
|
+
const x1 = num(doc, id, "x1");
|
|
2192
|
+
const y1 = num(doc, id, "y1");
|
|
2193
|
+
const x2 = num(doc, id, "x2");
|
|
2194
|
+
const y2 = num(doc, id, "y2");
|
|
2195
|
+
return {
|
|
2196
|
+
cx: (x1 + x2) / 2,
|
|
2197
|
+
cy: (y1 + y2) / 2
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
case "polyline":
|
|
2201
|
+
case "polygon": {
|
|
2202
|
+
const points = doc.get_attr(id, "points");
|
|
2203
|
+
if (!points) return null;
|
|
2204
|
+
return bbox_center(_grida_svg_parse.svg_parse.parse_points(points));
|
|
2205
|
+
}
|
|
2206
|
+
case "path": {
|
|
2207
|
+
const d = doc.get_attr(id, "d");
|
|
2208
|
+
if (!d) return null;
|
|
2209
|
+
return bbox_center(hit_shape_svg.path_control_polyline(d));
|
|
2210
|
+
}
|
|
2211
|
+
default: return null;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function shift_points_string(points, dx, dy) {
|
|
2215
|
+
if (dx === 0 && dy === 0) return points;
|
|
2216
|
+
return _grida_svg_parse.svg_parse.parse_points(points).map((p) => `${p.x + dx},${p.y + dy}`).join(" ");
|
|
2217
|
+
}
|
|
2218
|
+
function shift_path_d(d, dx, dy) {
|
|
2219
|
+
if (dx === 0 && dy === 0) return d;
|
|
2220
|
+
try {
|
|
2221
|
+
return new _grida_svg_pathdata.SVGPathData(d).transform(_grida_svg_pathdata.SVGPathDataTransformer.TRANSLATE(dx, dy)).encode();
|
|
2222
|
+
} catch {
|
|
2223
|
+
return d;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
/** Translate every local-frame coord of `id`'s geometry by (dx,
|
|
2227
|
+
* dy). The primitive arms write intrinsic attrs directly — we
|
|
2228
|
+
* can't route through `translate_pipeline.intent.apply` here
|
|
2229
|
+
* because `translate_pipeline.intent.capture_baseline` returns
|
|
2230
|
+
* `viaTransform` whenever the node has a `transform=`, and that
|
|
2231
|
+
* branch prepends a `translate()` to the transform string, which
|
|
2232
|
+
* would clobber the pivot rewrite the caller is about to do. */
|
|
2233
|
+
function shift_geometry(doc, id, dx, dy) {
|
|
2234
|
+
switch (doc.tag_of(id)) {
|
|
2235
|
+
case "rect":
|
|
2236
|
+
case "image":
|
|
2237
|
+
case "use":
|
|
2238
|
+
doc.set_attr(id, "x", String(num(doc, id, "x") + dx));
|
|
2239
|
+
doc.set_attr(id, "y", String(num(doc, id, "y") + dy));
|
|
2240
|
+
return;
|
|
2241
|
+
case "circle":
|
|
2242
|
+
case "ellipse":
|
|
2243
|
+
doc.set_attr(id, "cx", String(num(doc, id, "cx") + dx));
|
|
2244
|
+
doc.set_attr(id, "cy", String(num(doc, id, "cy") + dy));
|
|
2245
|
+
return;
|
|
2246
|
+
case "line":
|
|
2247
|
+
doc.set_attr(id, "x1", String(num(doc, id, "x1") + dx));
|
|
2248
|
+
doc.set_attr(id, "y1", String(num(doc, id, "y1") + dy));
|
|
2249
|
+
doc.set_attr(id, "x2", String(num(doc, id, "x2") + dx));
|
|
2250
|
+
doc.set_attr(id, "y2", String(num(doc, id, "y2") + dy));
|
|
2251
|
+
return;
|
|
2252
|
+
case "polyline":
|
|
2253
|
+
case "polygon": {
|
|
2254
|
+
const points = doc.get_attr(id, "points");
|
|
2255
|
+
if (points) doc.set_attr(id, "points", shift_points_string(points, dx, dy));
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
case "path": {
|
|
2259
|
+
const d = doc.get_attr(id, "d");
|
|
2260
|
+
if (d) doc.set_attr(id, "d", shift_path_d(d, dx, dy));
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
function find_rotate_op(ops) {
|
|
2266
|
+
for (const op of ops) if (op.type === "rotate") return op;
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Commit-only. Moves the rotate pivot to the new local center and
|
|
2271
|
+
* shifts geometry by δ = (R − I) · Δc so the doc-space rendering
|
|
2272
|
+
* is unchanged. Without the shift, changing the pivot offsets the
|
|
2273
|
+
* rect in doc space and the HUD's stable-matrix chrome falls out
|
|
2274
|
+
* of alignment with the element.
|
|
2275
|
+
*/
|
|
2276
|
+
function renormalize_rotate_pivot(doc, id) {
|
|
2277
|
+
const existing = doc.get_attr(id, "transform");
|
|
2278
|
+
if (existing === null || existing.indexOf("rotate") === -1) return;
|
|
2279
|
+
const ops = transform.parse(existing);
|
|
2280
|
+
if (ops === null) return;
|
|
2281
|
+
const cls = transform.classify(ops);
|
|
2282
|
+
if (cls !== "single_rotate_only" && cls !== "leading_translate_then_single_rotate") return;
|
|
2283
|
+
const rot = find_rotate_op(ops);
|
|
2284
|
+
if (!rot || rot.explicit_pivot !== true) return;
|
|
2285
|
+
const c_pre = new_local_center(doc, id);
|
|
2286
|
+
if (!c_pre) return;
|
|
2287
|
+
const dc_x = c_pre.cx - rot.cx;
|
|
2288
|
+
const dc_y = c_pre.cy - rot.cy;
|
|
2289
|
+
if (dc_x === 0 && dc_y === 0) return;
|
|
2290
|
+
const theta = rot.angle * Math.PI / 180;
|
|
2291
|
+
const cos = Math.cos(theta);
|
|
2292
|
+
const sin = Math.sin(theta);
|
|
2293
|
+
const dx = (cos - 1) * dc_x - sin * dc_y;
|
|
2294
|
+
const dy = sin * dc_x + (cos - 1) * dc_y;
|
|
2295
|
+
shift_geometry(doc, id, dx, dy);
|
|
2296
|
+
const next = transform.recompose(ops, c_pre.cx + dx, c_pre.cy + dy);
|
|
2297
|
+
if (next === existing) return;
|
|
2298
|
+
doc.set_attr(id, "transform", next);
|
|
2299
|
+
}
|
|
2300
|
+
function apply(doc, id, baseline, sx, sy, origin, phase = "commit") {
|
|
2301
|
+
dispatch_resize(doc, id, baseline, sx, sy, origin);
|
|
2302
|
+
if (phase === "commit") renormalize_rotate_pivot(doc, id);
|
|
2303
|
+
}
|
|
2304
|
+
_intent.apply = apply;
|
|
2305
|
+
function replace_href(doc, id, value) {
|
|
2306
|
+
const old_href = doc.get_attr(id, "href");
|
|
2307
|
+
const old_xlink = doc.get_attr(id, "href", XLINK_NS);
|
|
2308
|
+
const write_href = old_href !== null || old_xlink === null;
|
|
2309
|
+
const write_xlink = old_xlink !== null;
|
|
2310
|
+
if (write_href) doc.set_attr(id, "href", value);
|
|
2311
|
+
if (write_xlink) doc.set_attr(id, "href", value, XLINK_NS);
|
|
2312
|
+
return {
|
|
2313
|
+
old_href,
|
|
2314
|
+
old_xlink
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
_intent.replace_href = replace_href;
|
|
2318
|
+
function is_resizable_node(doc, id) {
|
|
2319
|
+
if (!is_resizable(doc.tag_of(id))) return false;
|
|
2320
|
+
const ops = transform.parse(doc.get_attr(id, "transform"));
|
|
2321
|
+
if (ops === null) return false;
|
|
2322
|
+
const cls = transform.classify(ops);
|
|
2323
|
+
if (cls === "identity" || cls === "leading_translate_only") return true;
|
|
2324
|
+
if (cls === "single_rotate_only" || cls === "leading_translate_then_single_rotate") return find_rotate_op(ops)?.explicit_pivot === true;
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
_intent.is_resizable_node = is_resizable_node;
|
|
2328
|
+
})(intent || (intent = _resize_pipeline.intent || (_resize_pipeline.intent = {})));
|
|
2329
|
+
let stages;
|
|
2330
|
+
(function(_stages) {
|
|
2331
|
+
function pipeline_baseline(plan) {
|
|
2332
|
+
return plan.baseline;
|
|
2333
|
+
}
|
|
2334
|
+
function corner_x_of(r, dir) {
|
|
2335
|
+
switch (dir) {
|
|
2336
|
+
case "nw":
|
|
2337
|
+
case "w":
|
|
2338
|
+
case "sw": return r.x;
|
|
2339
|
+
case "ne":
|
|
2340
|
+
case "e":
|
|
2341
|
+
case "se": return r.x + r.width;
|
|
2342
|
+
case "n":
|
|
2343
|
+
case "s": return r.x + r.width / 2;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
function corner_y_of(r, dir) {
|
|
2347
|
+
switch (dir) {
|
|
2348
|
+
case "nw":
|
|
2349
|
+
case "n":
|
|
2350
|
+
case "ne": return r.y;
|
|
2351
|
+
case "sw":
|
|
2352
|
+
case "s":
|
|
2353
|
+
case "se": return r.y + r.height;
|
|
2354
|
+
case "e":
|
|
2355
|
+
case "w": return r.y + r.height / 2;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
const aspect_lock = _stages.aspect_lock = {
|
|
2359
|
+
name: "aspect_lock",
|
|
2360
|
+
run(plan, ctx) {
|
|
2361
|
+
if (ctx.modifiers.aspect_lock !== "uniform") return { plan };
|
|
2362
|
+
if (!resize_capability.is_corner(plan.direction)) return { plan };
|
|
2363
|
+
const pbase = pipeline_baseline(plan);
|
|
2364
|
+
const locked = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, true);
|
|
2365
|
+
const bbox = pbase.bbox;
|
|
2366
|
+
const Hx_base = corner_x_of(bbox, plan.direction);
|
|
2367
|
+
const Hy_base = corner_y_of(bbox, plan.direction);
|
|
2368
|
+
const new_Hx = locked.origin.x + (Hx_base - locked.origin.x) * locked.sx;
|
|
2369
|
+
const new_Hy = locked.origin.y + (Hy_base - locked.origin.y) * locked.sy;
|
|
2370
|
+
return { plan: {
|
|
2371
|
+
...plan,
|
|
2372
|
+
dx: new_Hx - Hx_base,
|
|
2373
|
+
dy: new_Hy - Hy_base
|
|
2374
|
+
} };
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
const snap = _stages.snap = {
|
|
2378
|
+
name: "snap",
|
|
2379
|
+
run(plan, ctx) {
|
|
2380
|
+
if (ctx.modifiers.force_disable_snap) return { plan };
|
|
2381
|
+
if (!ctx.snap_session) return { plan };
|
|
2382
|
+
if (!ctx.options.snap_enabled) return { plan };
|
|
2383
|
+
const pbase = pipeline_baseline(plan);
|
|
2384
|
+
const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
|
|
2385
|
+
const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
|
|
2386
|
+
if (eff.no_op) return { plan };
|
|
2387
|
+
const r = ctx.snap_session.snap_resize(eff.rect, {
|
|
2388
|
+
x: eff.mask.x_edge,
|
|
2389
|
+
y: eff.mask.y_edge
|
|
2390
|
+
}, {
|
|
2391
|
+
enabled: true,
|
|
2392
|
+
threshold_px: ctx.options.snap_threshold_px
|
|
2393
|
+
});
|
|
2394
|
+
if (r.dx === 0 && r.dy === 0) return {
|
|
2395
|
+
plan,
|
|
2396
|
+
emit: r.guide ? { guide: r.guide } : void 0
|
|
2397
|
+
};
|
|
2398
|
+
if (eff.uniform) {
|
|
2399
|
+
const bbox = pbase.bbox;
|
|
2400
|
+
const new_Hx = eff.moving_corner.x + r.dx;
|
|
2401
|
+
const new_Hy = eff.moving_corner.y + r.dy;
|
|
2402
|
+
const sx_from_x = eff.mask.x_edge !== null && r.dx !== 0 && bbox.width !== 0 ? (new_Hx - eff.origin.x) / (eff.moving_corner.x - eff.origin.x) * eff.sx : null;
|
|
2403
|
+
const sy_from_y = eff.mask.y_edge !== null && r.dy !== 0 && bbox.height !== 0 ? (new_Hy - eff.origin.y) / (eff.moving_corner.y - eff.origin.y) * eff.sy : null;
|
|
2404
|
+
let s = eff.sx;
|
|
2405
|
+
if (sx_from_x !== null && sy_from_y !== null) s = Math.min(sx_from_x, sy_from_y);
|
|
2406
|
+
else if (sx_from_x !== null) s = sx_from_x;
|
|
2407
|
+
else if (sy_from_y !== null) s = sy_from_y;
|
|
2408
|
+
const Hx_base = corner_x_of(bbox, plan.direction);
|
|
2409
|
+
const Hy_base = corner_y_of(bbox, plan.direction);
|
|
2410
|
+
const target_Hx = eff.origin.x + (Hx_base - eff.origin.x) * s;
|
|
2411
|
+
const target_Hy = eff.origin.y + (Hy_base - eff.origin.y) * s;
|
|
2412
|
+
return {
|
|
2413
|
+
plan: {
|
|
2414
|
+
...plan,
|
|
2415
|
+
dx: eff.mask.affects_x ? target_Hx - Hx_base : 0,
|
|
2416
|
+
dy: eff.mask.affects_y ? target_Hy - Hy_base : 0
|
|
2417
|
+
},
|
|
2418
|
+
emit: r.guide ? { guide: r.guide } : void 0
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
plan: {
|
|
2423
|
+
...plan,
|
|
2424
|
+
dx: eff.mask.affects_x ? plan.dx + r.dx : plan.dx,
|
|
2425
|
+
dy: eff.mask.affects_y ? plan.dy + r.dy : plan.dy
|
|
2426
|
+
},
|
|
2427
|
+
emit: r.guide ? { guide: r.guide } : void 0
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
const pixel_grid = _stages.pixel_grid = {
|
|
2432
|
+
name: "pixel_grid",
|
|
2433
|
+
run(plan, ctx) {
|
|
2434
|
+
const q = ctx.options.pixel_grid_quantum;
|
|
2435
|
+
if (q === null || q <= 0) return { plan };
|
|
2436
|
+
const pbase = pipeline_baseline(plan);
|
|
2437
|
+
const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
|
|
2438
|
+
const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
|
|
2439
|
+
if (eff.no_op) return { plan };
|
|
2440
|
+
const target_Hx = eff.mask.affects_x ? Math.round(eff.moving_corner.x / q) * q : eff.moving_corner.x;
|
|
2441
|
+
const target_Hy = eff.mask.affects_y ? Math.round(eff.moving_corner.y / q) * q : eff.moving_corner.y;
|
|
2442
|
+
const bbox = pbase.bbox;
|
|
2443
|
+
const Hx_base = corner_x_of(bbox, plan.direction);
|
|
2444
|
+
const Hy_base = corner_y_of(bbox, plan.direction);
|
|
2445
|
+
return { plan: {
|
|
2446
|
+
...plan,
|
|
2447
|
+
dx: eff.mask.affects_x ? target_Hx - Hx_base : 0,
|
|
2448
|
+
dy: eff.mask.affects_y ? target_Hy - Hy_base : 0
|
|
2449
|
+
} };
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
_stages.DEFAULT = Object.freeze([
|
|
2453
|
+
aspect_lock,
|
|
2454
|
+
snap,
|
|
2455
|
+
pixel_grid
|
|
2456
|
+
]);
|
|
2457
|
+
})(stages || (stages = _resize_pipeline.stages || (_resize_pipeline.stages = {})));
|
|
2458
|
+
function run(init, stages, ctx) {
|
|
2459
|
+
let plan = init;
|
|
2460
|
+
const guides = [];
|
|
2461
|
+
for (const stage of stages) {
|
|
2462
|
+
const out = stage.run(plan, ctx);
|
|
2463
|
+
plan = out.plan;
|
|
2464
|
+
if (out.emit?.guide) guides.push(out.emit.guide);
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
plan,
|
|
2468
|
+
guides
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
_resize_pipeline.run = run;
|
|
2472
|
+
function apply(doc, plan, phase = "commit") {
|
|
2473
|
+
const f = intent.compute_factors(plan.baseline, plan.direction, plan.dx, plan.dy, false);
|
|
2474
|
+
const members = plan.members ?? [{
|
|
2475
|
+
id: plan.id,
|
|
2476
|
+
baseline: plan.baseline
|
|
2477
|
+
}];
|
|
2478
|
+
for (const m of members) intent.apply(doc, m.id, m.baseline, f.sx, f.sy, f.origin, phase);
|
|
2479
|
+
}
|
|
2480
|
+
_resize_pipeline.apply = apply;
|
|
2481
|
+
function revert(doc, plan) {
|
|
2482
|
+
const f = intent.compute_factors(plan.baseline, plan.direction, 0, 0, false);
|
|
2483
|
+
const members = plan.members ?? [{
|
|
2484
|
+
id: plan.id,
|
|
2485
|
+
baseline: plan.baseline
|
|
2486
|
+
}];
|
|
2487
|
+
for (const m of members) intent.apply(doc, m.id, m.baseline, 1, 1, f.origin, "preview");
|
|
2488
|
+
}
|
|
2489
|
+
_resize_pipeline.revert = revert;
|
|
2490
|
+
function synthesize_group_baseline(union) {
|
|
2491
|
+
return {
|
|
2492
|
+
bbox: {
|
|
2493
|
+
x: union.x,
|
|
2494
|
+
y: union.y,
|
|
2495
|
+
width: union.width,
|
|
2496
|
+
height: union.height
|
|
2497
|
+
},
|
|
2498
|
+
attrs: {
|
|
2499
|
+
kind: "rect",
|
|
2500
|
+
x: union.x,
|
|
2501
|
+
y: union.y,
|
|
2502
|
+
w: union.width,
|
|
2503
|
+
h: union.height
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
_resize_pipeline.synthesize_group_baseline = synthesize_group_baseline;
|
|
2508
|
+
})(resize_pipeline || (resize_pipeline = {}));
|
|
2509
|
+
//#endregion
|
|
2510
|
+
//#region src/core/resize-pipeline/orchestrator.ts
|
|
2511
|
+
const PROVIDER_ID = "svg-editor";
|
|
2512
|
+
/** West/north-anchor flips invert the corresponding world delta so a
|
|
2513
|
+
* positive value always grows the moving edge outward — the convention
|
|
2514
|
+
* `compute_resize_factors` consumes. */
|
|
2515
|
+
function sign_adjust(dir, dx_world, dy_world) {
|
|
2516
|
+
return {
|
|
2517
|
+
dx: dir === "w" || dir === "nw" || dir === "sw" ? -dx_world : dx_world,
|
|
2518
|
+
dy: dir === "n" || dir === "ne" || dir === "nw" ? -dy_world : dy_world
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
/** Stable, order-independent key for an id set — used by `is_active_for`
|
|
2522
|
+
* to decide whether the current session targets the same group. */
|
|
2523
|
+
function ids_key(ids) {
|
|
2524
|
+
return [...ids].sort().join("\0");
|
|
2525
|
+
}
|
|
2526
|
+
var ResizeOrchestrator = class {
|
|
2527
|
+
constructor(deps) {
|
|
2528
|
+
this.deps = deps;
|
|
2529
|
+
this.active = null;
|
|
2530
|
+
this._last_guides = [];
|
|
2531
|
+
}
|
|
2532
|
+
/** Guides emitted by the most recent pipeline run. Cleared on
|
|
2533
|
+
* cancel/dispose. */
|
|
2534
|
+
get last_guides() {
|
|
2535
|
+
return this._last_guides;
|
|
2536
|
+
}
|
|
2537
|
+
has_active_session() {
|
|
2538
|
+
return this.active !== null;
|
|
2539
|
+
}
|
|
2540
|
+
/** Is the gesture currently targeting `ids` with `direction`? Used by
|
|
2541
|
+
* the HUD dispatch to decide whether to reset the session on a new
|
|
2542
|
+
* handle / target. Order-independent. */
|
|
2543
|
+
is_active_for(ids, direction) {
|
|
2544
|
+
return this.active !== null && this.active.direction === direction && this.active.ids_key === ids_key(ids);
|
|
2545
|
+
}
|
|
2546
|
+
/** Per-frame drive. Opens a session lazily on the first call. The
|
|
2547
|
+
* HUD passes its gesture-target rect dimensions in **world space**;
|
|
2548
|
+
* the orchestrator derives the signed world-frame delta against its
|
|
2549
|
+
* captured `baseline.bbox`. The DOM adapter is responsible for the
|
|
2550
|
+
* CSS-px → world conversion at the intent boundary. */
|
|
2551
|
+
drive(input, modifiers, opts) {
|
|
2552
|
+
if (input.ids.length === 0) return null;
|
|
2553
|
+
const doc = this.deps.get_doc();
|
|
2554
|
+
for (const id of input.ids) if (!resize_pipeline.intent.is_resizable_node(doc, id)) return null;
|
|
2555
|
+
const key = ids_key(input.ids);
|
|
2556
|
+
if (this.active && (this.active.ids_key !== key || this.active.direction !== input.direction)) {
|
|
2557
|
+
this.active.preview.discard();
|
|
2558
|
+
this.dispose_session();
|
|
2559
|
+
}
|
|
2560
|
+
if (this.active === null) this.active = this.open(input.ids, input.direction, opts.snap, opts.label ?? "resize");
|
|
2561
|
+
const session = this.active;
|
|
2562
|
+
const bbox = session.baseline.bbox;
|
|
2563
|
+
const dx_world = input.target_width - bbox.width;
|
|
2564
|
+
const dy_world = input.target_height - bbox.height;
|
|
2565
|
+
const d = sign_adjust(input.direction, dx_world, dy_world);
|
|
2566
|
+
const stages = opts.stages ?? resize_pipeline.stages.DEFAULT;
|
|
2567
|
+
const result = this.run_pass(session, d.dx, d.dy, modifiers, stages);
|
|
2568
|
+
session.last_dx = d.dx;
|
|
2569
|
+
session.last_dy = d.dy;
|
|
2570
|
+
session.last_stages = stages;
|
|
2571
|
+
this.write_preview(session, result.plan, opts.phase);
|
|
2572
|
+
if (opts.phase === "commit") {
|
|
2573
|
+
session.preview.commit();
|
|
2574
|
+
this.dispose_session();
|
|
2575
|
+
}
|
|
2576
|
+
return result;
|
|
2577
|
+
}
|
|
2578
|
+
/** Re-run the current preview frame with new modifiers. */
|
|
2579
|
+
redrive_modifiers(modifiers) {
|
|
2580
|
+
if (!this.active) return null;
|
|
2581
|
+
const session = this.active;
|
|
2582
|
+
const result = this.run_pass(session, session.last_dx, session.last_dy, modifiers, session.last_stages);
|
|
2583
|
+
this.write_preview(session, result.plan, "preview");
|
|
2584
|
+
return result;
|
|
2585
|
+
}
|
|
2586
|
+
cancel() {
|
|
2587
|
+
if (!this.active) return;
|
|
2588
|
+
this.active.preview.discard();
|
|
2589
|
+
this.dispose_session();
|
|
2590
|
+
}
|
|
2591
|
+
run_pass(session, dx, dy, modifiers, stages) {
|
|
2592
|
+
const plan0 = {
|
|
2593
|
+
id: session.primary_id,
|
|
2594
|
+
baseline: session.baseline,
|
|
2595
|
+
members: session.members,
|
|
2596
|
+
direction: session.direction,
|
|
2597
|
+
dx,
|
|
2598
|
+
dy
|
|
2599
|
+
};
|
|
2600
|
+
const ctx = {
|
|
2601
|
+
input: {
|
|
2602
|
+
id: session.primary_id,
|
|
2603
|
+
direction: session.direction,
|
|
2604
|
+
dx,
|
|
2605
|
+
dy
|
|
2606
|
+
},
|
|
2607
|
+
modifiers,
|
|
2608
|
+
options: this.deps.options(),
|
|
2609
|
+
snap_session: session.snap
|
|
2610
|
+
};
|
|
2611
|
+
const result = resize_pipeline.run(plan0, stages, ctx);
|
|
2612
|
+
this._last_guides = result.guides;
|
|
2613
|
+
return result;
|
|
2614
|
+
}
|
|
2615
|
+
open(ids, direction, snap, label) {
|
|
2616
|
+
const doc = this.deps.get_doc();
|
|
2617
|
+
const members = ids.map((id) => ({
|
|
2618
|
+
id,
|
|
2619
|
+
baseline: resize_pipeline.intent.capture_baseline(doc, id, this.deps.bbox_world(id))
|
|
2620
|
+
}));
|
|
2621
|
+
const baseline = members.length === 1 ? members[0].baseline : resize_pipeline.synthesize_group_baseline(_grida_cmath.default.rect.union(members.map((m) => m.baseline.bbox)));
|
|
2622
|
+
return {
|
|
2623
|
+
ids_key: ids_key(ids),
|
|
2624
|
+
primary_id: members[0].id,
|
|
2625
|
+
direction,
|
|
2626
|
+
members,
|
|
2627
|
+
baseline,
|
|
2628
|
+
snap: snap ? this.deps.open_snap(ids) : null,
|
|
2629
|
+
preview: this.deps.open_preview(label),
|
|
2630
|
+
last_dx: 0,
|
|
2631
|
+
last_dy: 0,
|
|
2632
|
+
last_stages: resize_pipeline.stages.DEFAULT
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
write_preview(session, plan, phase) {
|
|
2636
|
+
const doc = this.deps.get_doc();
|
|
2637
|
+
const emit = this.deps.emit;
|
|
2638
|
+
session.preview.set({
|
|
2639
|
+
providerId: PROVIDER_ID,
|
|
2640
|
+
apply: () => {
|
|
2641
|
+
resize_pipeline.apply(doc, plan, phase);
|
|
2642
|
+
emit();
|
|
2643
|
+
},
|
|
2644
|
+
revert: () => {
|
|
2645
|
+
resize_pipeline.revert(doc, plan);
|
|
2646
|
+
emit();
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
dispose_session() {
|
|
2651
|
+
if (!this.active) return;
|
|
2652
|
+
this.active.snap?.dispose();
|
|
2653
|
+
this.active = null;
|
|
2654
|
+
this._last_guides = [];
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
//#endregion
|
|
2658
|
+
//#region src/core/paint.ts
|
|
2659
|
+
let paint;
|
|
2660
|
+
(function(_paint) {
|
|
2661
|
+
function parse(declared) {
|
|
2662
|
+
if (declared === null || declared === "") return null;
|
|
2663
|
+
const trimmed = declared.trim();
|
|
2664
|
+
if (trimmed === "") return null;
|
|
2665
|
+
if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
|
|
2666
|
+
if (/^var\s*\(/i.test(trimmed)) return {
|
|
2667
|
+
error: "invalid_at_computed_value_time",
|
|
2668
|
+
reason: "var() substitution requires a cascade engine (not implemented)"
|
|
2669
|
+
};
|
|
2670
|
+
if (trimmed === "none") return { kind: "none" };
|
|
2671
|
+
if (trimmed === "context-fill" || trimmed === "contextFill") return { kind: "context_fill" };
|
|
2672
|
+
if (trimmed === "context-stroke" || trimmed === "contextStroke") return { kind: "context_stroke" };
|
|
2673
|
+
const url_match = trimmed.match(/^url\(\s*(["']?)#([^)"']+)\1\s*\)\s*(.*)$/i);
|
|
2674
|
+
if (url_match) {
|
|
2675
|
+
const id = url_match[2];
|
|
2676
|
+
const rest = url_match[3].trim();
|
|
2677
|
+
let fallback;
|
|
2678
|
+
if (rest !== "") {
|
|
2679
|
+
const f = parse(rest);
|
|
2680
|
+
if (f && f.kind === "none") fallback = { kind: "none" };
|
|
2681
|
+
else if (f && f.kind === "color") fallback = {
|
|
2682
|
+
kind: "color",
|
|
2683
|
+
value: f.value
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
return fallback ? {
|
|
2687
|
+
kind: "ref",
|
|
2688
|
+
id,
|
|
2689
|
+
fallback
|
|
2690
|
+
} : {
|
|
2691
|
+
kind: "ref",
|
|
2692
|
+
id
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
if (/^currentcolor$/i.test(trimmed)) return {
|
|
2696
|
+
kind: "color",
|
|
2697
|
+
value: { kind: "current_color" }
|
|
2698
|
+
};
|
|
2699
|
+
return {
|
|
2700
|
+
kind: "color",
|
|
2701
|
+
value: {
|
|
2702
|
+
kind: "rgb",
|
|
2703
|
+
value: trimmed
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
_paint.parse = parse;
|
|
2708
|
+
function serialize(p) {
|
|
2709
|
+
switch (p.kind) {
|
|
2710
|
+
case "none": return "none";
|
|
2711
|
+
case "context_fill": return "context-fill";
|
|
2712
|
+
case "context_stroke": return "context-stroke";
|
|
2713
|
+
case "color": return p.value.kind === "current_color" ? "currentColor" : p.value.value;
|
|
2714
|
+
case "ref":
|
|
2715
|
+
if (p.fallback) {
|
|
2716
|
+
const f = p.fallback.kind === "none" ? "none" : p.fallback.value.kind === "current_color" ? "currentColor" : p.fallback.value.value;
|
|
2717
|
+
return `url(#${p.id}) ${f}`;
|
|
2718
|
+
}
|
|
2719
|
+
return `url(#${p.id})`;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
_paint.serialize = serialize;
|
|
2723
|
+
function value_equals(a, b) {
|
|
2724
|
+
if (a === b) return true;
|
|
2725
|
+
if (a.declared !== b.declared) return false;
|
|
2726
|
+
if (a.provenance.carrier !== b.provenance.carrier) return false;
|
|
2727
|
+
if (a.provenance.origin !== b.provenance.origin) return false;
|
|
2728
|
+
return computed_equals(a.computed, b.computed);
|
|
2729
|
+
}
|
|
2730
|
+
_paint.value_equals = value_equals;
|
|
2731
|
+
function computed_equals(a, b) {
|
|
2732
|
+
if (a === b) return true;
|
|
2733
|
+
if (a == null || b == null) return false;
|
|
2734
|
+
if ("error" in a || "error" in b) return "error" in a && "error" in b && a.error === b.error && a.reason === b.reason;
|
|
2735
|
+
if (a.kind !== b.kind) return false;
|
|
2736
|
+
if (a.kind === "color" && b.kind === "color") {
|
|
2737
|
+
if (a.value.kind !== b.value.kind) return false;
|
|
2738
|
+
if (a.value.kind === "rgb" && b.value.kind === "rgb") return a.value.value === b.value.value;
|
|
2739
|
+
return true;
|
|
2740
|
+
}
|
|
2741
|
+
if (a.kind === "ref" && b.kind === "ref") return a.id === b.id;
|
|
2742
|
+
if (a.kind === "none" && b.kind === "none") return true;
|
|
2743
|
+
if (a.kind === "context_fill" && b.kind === "context_fill") return true;
|
|
2744
|
+
if (a.kind === "context_stroke" && b.kind === "context_stroke") return true;
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
})(paint || (paint = {}));
|
|
2748
|
+
//#endregion
|
|
2749
|
+
//#region src/types.ts
|
|
2750
|
+
const TOOL_CURSOR = { type: "cursor" };
|
|
2751
|
+
const DEFAULT_STYLE = {
|
|
2752
|
+
chrome_color: "#2563eb",
|
|
2753
|
+
handle_size: 8,
|
|
2754
|
+
handle_fill: "#ffffff",
|
|
2755
|
+
handle_stroke: "#2563eb",
|
|
2756
|
+
endpoint_dot_radius: 5,
|
|
2757
|
+
selection_outline_width: 2,
|
|
2758
|
+
measurement_color: "#ff3a30",
|
|
2759
|
+
show_size_meter: true,
|
|
2760
|
+
snap_enabled: true,
|
|
2761
|
+
snap_threshold_px: 6,
|
|
2762
|
+
hit_tolerance_px: 0,
|
|
2763
|
+
snap_to_pixel_grid: false,
|
|
2764
|
+
pixel_grid_size: 1,
|
|
2765
|
+
pixel_grid: true,
|
|
2766
|
+
angle_snap_step_radians: Math.PI / 12
|
|
2767
|
+
};
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region src/core/insertions.ts
|
|
2770
|
+
let insertions;
|
|
2771
|
+
(function(_insertions) {
|
|
2772
|
+
const DEFAULT_SIZE = _insertions.DEFAULT_SIZE = 100;
|
|
2773
|
+
const DEFAULT_FILL = _insertions.DEFAULT_FILL = "#D9D9D9";
|
|
2774
|
+
function initial_attrs(tag, point) {
|
|
2775
|
+
switch (tag) {
|
|
2776
|
+
case "rect": return {
|
|
2777
|
+
x: fmt(point.x),
|
|
2778
|
+
y: fmt(point.y),
|
|
2779
|
+
width: "0",
|
|
2780
|
+
height: "0"
|
|
2781
|
+
};
|
|
2782
|
+
case "ellipse": return {
|
|
2783
|
+
cx: fmt(point.x),
|
|
2784
|
+
cy: fmt(point.y),
|
|
2785
|
+
rx: "0",
|
|
2786
|
+
ry: "0"
|
|
2787
|
+
};
|
|
2788
|
+
case "line": return {
|
|
2789
|
+
x1: fmt(point.x),
|
|
2790
|
+
y1: fmt(point.y),
|
|
2791
|
+
x2: fmt(point.x),
|
|
2792
|
+
y2: fmt(point.y)
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
_insertions.initial_attrs = initial_attrs;
|
|
2797
|
+
function default_attrs(tag, point, size = DEFAULT_SIZE) {
|
|
2798
|
+
switch (tag) {
|
|
2799
|
+
case "rect": return {
|
|
2800
|
+
x: fmt(point.x - size / 2),
|
|
2801
|
+
y: fmt(point.y - size / 2),
|
|
2802
|
+
width: fmt(size),
|
|
2803
|
+
height: fmt(size)
|
|
2804
|
+
};
|
|
2805
|
+
case "ellipse": return {
|
|
2806
|
+
cx: fmt(point.x),
|
|
2807
|
+
cy: fmt(point.y),
|
|
2808
|
+
rx: fmt(size / 2),
|
|
2809
|
+
ry: fmt(size / 2)
|
|
2810
|
+
};
|
|
2811
|
+
case "line": return {
|
|
2812
|
+
x1: fmt(point.x - size / 2),
|
|
2813
|
+
y1: fmt(point.y),
|
|
2814
|
+
x2: fmt(point.x + size / 2),
|
|
2815
|
+
y2: fmt(point.y)
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
_insertions.default_attrs = default_attrs;
|
|
2820
|
+
function compute_drag_attrs(tag, anchor, current, modifiers) {
|
|
2821
|
+
switch (tag) {
|
|
2822
|
+
case "rect": return rect_attrs(anchor, current, modifiers);
|
|
2823
|
+
case "ellipse": return ellipse_attrs(anchor, current, modifiers);
|
|
2824
|
+
case "line": return line_attrs(anchor, current, modifiers);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
_insertions.compute_drag_attrs = compute_drag_attrs;
|
|
2828
|
+
function default_paint_attrs(tag) {
|
|
2829
|
+
switch (tag) {
|
|
2830
|
+
case "rect":
|
|
2831
|
+
case "ellipse": return { fill: DEFAULT_FILL };
|
|
2832
|
+
case "line": return {
|
|
2833
|
+
stroke: "#000000",
|
|
2834
|
+
"stroke-width": "1"
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
_insertions.default_paint_attrs = default_paint_attrs;
|
|
2839
|
+
const DEFAULT_TEXT_FONT_SIZE = _insertions.DEFAULT_TEXT_FONT_SIZE = 16;
|
|
2840
|
+
const DEFAULT_TEXT_FONT_FAMILY = _insertions.DEFAULT_TEXT_FONT_FAMILY = "sans-serif";
|
|
2841
|
+
const DEFAULT_TEXT_FILL = _insertions.DEFAULT_TEXT_FILL = "#000000";
|
|
2842
|
+
function default_text_attrs(point) {
|
|
2843
|
+
return {
|
|
2844
|
+
x: fmt(point.x),
|
|
2845
|
+
y: fmt(point.y),
|
|
2846
|
+
"font-size": String(DEFAULT_TEXT_FONT_SIZE),
|
|
2847
|
+
"font-family": DEFAULT_TEXT_FONT_FAMILY,
|
|
2848
|
+
fill: DEFAULT_TEXT_FILL
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
_insertions.default_text_attrs = default_text_attrs;
|
|
2852
|
+
function rect_attrs(anchor, current, mods) {
|
|
2853
|
+
let dx = current.x - anchor.x;
|
|
2854
|
+
let dy = current.y - anchor.y;
|
|
2855
|
+
if (mods.shift) {
|
|
2856
|
+
const m = Math.max(Math.abs(dx), Math.abs(dy));
|
|
2857
|
+
dx = dx < 0 ? -m : m;
|
|
2858
|
+
dy = dy < 0 ? -m : m;
|
|
2859
|
+
}
|
|
2860
|
+
let x;
|
|
2861
|
+
let y;
|
|
2862
|
+
let w;
|
|
2863
|
+
let h;
|
|
2864
|
+
if (mods.alt) {
|
|
2865
|
+
x = anchor.x - Math.abs(dx);
|
|
2866
|
+
y = anchor.y - Math.abs(dy);
|
|
2867
|
+
w = Math.abs(dx) * 2;
|
|
2868
|
+
h = Math.abs(dy) * 2;
|
|
2869
|
+
} else {
|
|
2870
|
+
x = Math.min(anchor.x, anchor.x + dx);
|
|
2871
|
+
y = Math.min(anchor.y, anchor.y + dy);
|
|
2872
|
+
w = Math.abs(dx);
|
|
2873
|
+
h = Math.abs(dy);
|
|
2874
|
+
}
|
|
2875
|
+
return {
|
|
2876
|
+
x: fmt(x),
|
|
2877
|
+
y: fmt(y),
|
|
2878
|
+
width: fmt(w),
|
|
2879
|
+
height: fmt(h)
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
function ellipse_attrs(anchor, current, mods) {
|
|
2883
|
+
let dx = current.x - anchor.x;
|
|
2884
|
+
let dy = current.y - anchor.y;
|
|
2885
|
+
if (mods.shift) {
|
|
2886
|
+
const m = Math.max(Math.abs(dx), Math.abs(dy));
|
|
2887
|
+
dx = dx < 0 ? -m : m;
|
|
2888
|
+
dy = dy < 0 ? -m : m;
|
|
2889
|
+
}
|
|
2890
|
+
let cx;
|
|
2891
|
+
let cy;
|
|
2892
|
+
let rx;
|
|
2893
|
+
let ry;
|
|
2894
|
+
if (mods.alt) {
|
|
2895
|
+
cx = anchor.x;
|
|
2896
|
+
cy = anchor.y;
|
|
2897
|
+
rx = Math.abs(dx);
|
|
2898
|
+
ry = Math.abs(dy);
|
|
2899
|
+
} else {
|
|
2900
|
+
cx = anchor.x + dx / 2;
|
|
2901
|
+
cy = anchor.y + dy / 2;
|
|
2902
|
+
rx = Math.abs(dx) / 2;
|
|
2903
|
+
ry = Math.abs(dy) / 2;
|
|
2904
|
+
}
|
|
2905
|
+
return {
|
|
2906
|
+
cx: fmt(cx),
|
|
2907
|
+
cy: fmt(cy),
|
|
2908
|
+
rx: fmt(rx),
|
|
2909
|
+
ry: fmt(ry)
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function line_attrs(anchor, current, mods) {
|
|
2913
|
+
let dx = current.x - anchor.x;
|
|
2914
|
+
let dy = current.y - anchor.y;
|
|
2915
|
+
if (mods.shift) {
|
|
2916
|
+
const len = Math.hypot(dx, dy);
|
|
2917
|
+
if (len > 0) {
|
|
2918
|
+
const angle = Math.atan2(dy, dx);
|
|
2919
|
+
const step = Math.PI / 4;
|
|
2920
|
+
const quantized = Math.round(angle / step) * step;
|
|
2921
|
+
dx = Math.cos(quantized) * len;
|
|
2922
|
+
dy = Math.sin(quantized) * len;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let x1;
|
|
2926
|
+
let y1;
|
|
2927
|
+
let x2;
|
|
2928
|
+
let y2;
|
|
2929
|
+
if (mods.alt) {
|
|
2930
|
+
x1 = anchor.x - dx;
|
|
2931
|
+
y1 = anchor.y - dy;
|
|
2932
|
+
x2 = anchor.x + dx;
|
|
2933
|
+
y2 = anchor.y + dy;
|
|
2934
|
+
} else {
|
|
2935
|
+
x1 = anchor.x;
|
|
2936
|
+
y1 = anchor.y;
|
|
2937
|
+
x2 = anchor.x + dx;
|
|
2938
|
+
y2 = anchor.y + dy;
|
|
2939
|
+
}
|
|
2940
|
+
return {
|
|
2941
|
+
x1: fmt(x1),
|
|
2942
|
+
y1: fmt(y1),
|
|
2943
|
+
x2: fmt(x2),
|
|
2944
|
+
y2: fmt(y2)
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
/** Format a numeric value for SVG attr output. Rounds to 4 decimals
|
|
2948
|
+
* to suppress IEEE-754 noise (`0.30000000000000004` → `0.3`);
|
|
2949
|
+
* `String()` drops trailing zeros and the decimal point for
|
|
2950
|
+
* integers. */
|
|
2951
|
+
function fmt(n) {
|
|
2952
|
+
return String(Math.round(n * 1e4) / 1e4);
|
|
2953
|
+
}
|
|
2954
|
+
})(insertions || (insertions = {}));
|
|
2955
|
+
//#endregion
|
|
2956
|
+
//#region src/core/vector-edit/model.ts
|
|
2957
|
+
/**
|
|
2958
|
+
* Canonical vector-network model for a single SVG path's `d` string.
|
|
2959
|
+
*
|
|
2960
|
+
* `PathModel` is a self-contained geometry primitive — it parses an SVG
|
|
2961
|
+
* path `d` into a vertex/segment graph (with verb hints preserved for
|
|
2962
|
+
* round-trip honesty), exposes POJO observers, and serializes back to
|
|
2963
|
+
* `d`. It does not hold or reference an `SvgDocument`, an editor
|
|
2964
|
+
* instance, the DOM, or any host. It is safe to construct in any
|
|
2965
|
+
* environment that can run the package.
|
|
2966
|
+
*
|
|
2967
|
+
* Public re-exported as a top-level Layer-A primitive from
|
|
2968
|
+
* `@grida/svg-editor` for callers that want canonical path geometry
|
|
2969
|
+
* without mounting an editor. The full mutation surface (translate /
|
|
2970
|
+
* bend / set-tangent / split, etc.) is package-internal and may shift;
|
|
2971
|
+
* the publicly-stable contract for external callers is the construction
|
|
2972
|
+
* + serialization + observation methods documented at the entry point.
|
|
2973
|
+
*
|
|
2974
|
+
* @experimental Surface shape is v0; signatures may change before the
|
|
2975
|
+
* package reaches semver stability.
|
|
2976
|
+
*/
|
|
2977
|
+
var PathModel = class PathModel {
|
|
2978
|
+
constructor(network, meta) {
|
|
2979
|
+
if (network.segments.length !== meta.length) throw new Error(`PathModel invariant violated: segments(${network.segments.length}) !== meta(${meta.length})`);
|
|
2980
|
+
this._network = network;
|
|
2981
|
+
this._meta = meta;
|
|
2982
|
+
}
|
|
2983
|
+
static fromSvgPathD(d) {
|
|
2984
|
+
const { network, meta } = parseWithVerbs(d);
|
|
2985
|
+
return new PathModel(network, meta);
|
|
2986
|
+
}
|
|
2987
|
+
/** Construct from a vn network with no verb info (every segment defaults to undefined verb). */
|
|
2988
|
+
static fromVectorNetwork(network) {
|
|
2989
|
+
const meta = network.segments.map(() => ({}));
|
|
2990
|
+
return new PathModel(cloneNetwork(network), meta);
|
|
2991
|
+
}
|
|
2992
|
+
toSvgPathD() {
|
|
2993
|
+
return emitWithVerbs(this._network, this._meta);
|
|
2994
|
+
}
|
|
2995
|
+
snapshot() {
|
|
2996
|
+
return {
|
|
2997
|
+
vertices: this._network.vertices,
|
|
2998
|
+
segments: this._network.segments.map((seg, i) => ({
|
|
2999
|
+
a: seg.a,
|
|
3000
|
+
b: seg.b,
|
|
3001
|
+
ta: seg.ta,
|
|
3002
|
+
tb: seg.tb,
|
|
3003
|
+
source_verb: this._meta[i]?.source_verb
|
|
3004
|
+
}))
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
bbox() {
|
|
3008
|
+
return new _grida_vn.default.VectorNetworkEditor(this._network).getBBox();
|
|
3009
|
+
}
|
|
3010
|
+
vertexCount() {
|
|
3011
|
+
return this._network.vertices.length;
|
|
3012
|
+
}
|
|
3013
|
+
segmentCount() {
|
|
3014
|
+
return this._network.segments.length;
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* If the model's current geometry is still expressible in the source
|
|
3018
|
+
* SVG tag's native attribute form, return the equivalent
|
|
3019
|
+
* `VectorEditSource` (which is also the writeable shape) — else `null`.
|
|
3020
|
+
*
|
|
3021
|
+
* This is the decider that gates per-gesture native-attrs writeback in
|
|
3022
|
+
* `VectorEditSession.apply_d`. `null` means "the user's edit cannot be
|
|
3023
|
+
* faithfully written back to the source tag" — in v1 with no
|
|
3024
|
+
* promotion, the gesture is refused; in v1.1+ with promotion, the
|
|
3025
|
+
* element is rewritten to `<path d="…">`.
|
|
3026
|
+
*
|
|
3027
|
+
* v1 expressibility (all source kinds require every segment's `ta` and
|
|
3028
|
+
* `tb` to be exactly zero — any tangent edit forces promotion):
|
|
3029
|
+
*
|
|
3030
|
+
* - **path** — always `null` (no native fallback; the canonical form
|
|
3031
|
+
* IS `<path d>`, so callers should just write `d` directly).
|
|
3032
|
+
* - **line** — exactly two vertices joined by one straight segment
|
|
3033
|
+
* `0→1`. (Topology after a 2-point `vn.fromPolyline` and any sequence
|
|
3034
|
+
* of endpoint translates.)
|
|
3035
|
+
* - **polyline** — segments form the canonical open chain
|
|
3036
|
+
* `0→1, 1→2, …, (n-2)→(n-1)`. (Topology after `vn.fromPolyline` and
|
|
3037
|
+
* any sequence of vertex translates.)
|
|
3038
|
+
* - **polygon** — segments form the canonical closed chain
|
|
3039
|
+
* `0→1, 1→2, …, (n-1)→0`. (Topology after `vn.fromPolygon` and any
|
|
3040
|
+
* sequence of vertex translates.)
|
|
3041
|
+
* - **rect / circle / ellipse** — always `null`. These geometry
|
|
3042
|
+
* primitives have no native writeback target; any vector gesture on
|
|
3043
|
+
* them re-types the element to `<path>` (see `vector_apply` /
|
|
3044
|
+
* `SvgDocument.retype_to_path`), so they never round-trip through here.
|
|
3045
|
+
*
|
|
3046
|
+
* Anything that changes segment topology (insert-vertex, delete-vertex,
|
|
3047
|
+
* close/open shape) or introduces a curve leaves the canonical chain and
|
|
3048
|
+
* returns `null` here; the caller re-types the element to `<path>`.
|
|
3049
|
+
*/
|
|
3050
|
+
toNativeAttrs(source_tag) {
|
|
3051
|
+
if (source_tag !== "line" && source_tag !== "polyline" && source_tag !== "polygon") return null;
|
|
3052
|
+
const { vertices, segments } = this._network;
|
|
3053
|
+
for (const s of segments) {
|
|
3054
|
+
if (s.ta[0] !== 0 || s.ta[1] !== 0) return null;
|
|
3055
|
+
if (s.tb[0] !== 0 || s.tb[1] !== 0) return null;
|
|
3056
|
+
}
|
|
3057
|
+
const n = vertices.length;
|
|
3058
|
+
if (source_tag === "line") {
|
|
3059
|
+
if (n !== 2 || segments.length !== 1) return null;
|
|
3060
|
+
const s = segments[0];
|
|
3061
|
+
if (s.a !== 0 || s.b !== 1) return null;
|
|
3062
|
+
const [x1, y1] = vertices[0];
|
|
3063
|
+
const [x2, y2] = vertices[1];
|
|
3064
|
+
return {
|
|
3065
|
+
kind: "line",
|
|
3066
|
+
x1,
|
|
3067
|
+
y1,
|
|
3068
|
+
x2,
|
|
3069
|
+
y2
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
if (source_tag === "polyline") {
|
|
3073
|
+
if (segments.length !== n - 1 || n < 2) return null;
|
|
3074
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3075
|
+
const s = segments[i];
|
|
3076
|
+
if (s.a !== i || s.b !== i + 1) return null;
|
|
3077
|
+
}
|
|
3078
|
+
return {
|
|
3079
|
+
kind: "polyline",
|
|
3080
|
+
points: vertices.map((v) => [v[0], v[1]])
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
if (source_tag === "polygon") {
|
|
3084
|
+
if (segments.length !== n || n < 2) return null;
|
|
3085
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
3086
|
+
const s = segments[i];
|
|
3087
|
+
if (s.a !== i || s.b !== i + 1) return null;
|
|
3088
|
+
}
|
|
3089
|
+
const closer = segments[segments.length - 1];
|
|
3090
|
+
if (closer.a !== n - 1 || closer.b !== 0) return null;
|
|
3091
|
+
return {
|
|
3092
|
+
kind: "polygon",
|
|
3093
|
+
points: vertices.map((v) => [v[0], v[1]])
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
/** Translate one vertex by `delta`. Connected segments follow because
|
|
3099
|
+
* tangents are stored relative to vertices. Verb metadata is preserved
|
|
3100
|
+
* as-is; emit-time honesty handles cases where the shape no longer
|
|
3101
|
+
* matches the recorded verb (e.g. an H whose endpoint y-coord drifts). */
|
|
3102
|
+
translateVertex(v, delta) {
|
|
3103
|
+
if (v < 0 || v >= this._network.vertices.length) throw new Error(`PathModel.translateVertex: invalid vertex ${v}`);
|
|
3104
|
+
const next_network = cloneNetwork(this._network);
|
|
3105
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(next_network);
|
|
3106
|
+
vne.translateVertex(v, [delta[0], delta[1]]);
|
|
3107
|
+
return new PathModel(vne.value, this._meta);
|
|
3108
|
+
}
|
|
3109
|
+
/** Bulk-translate a set of vertices by the same delta. Atomic — either
|
|
3110
|
+
* every move succeeds or none (input is validated up-front). */
|
|
3111
|
+
translateVertices(indices, delta) {
|
|
3112
|
+
if (indices.length === 0) return this;
|
|
3113
|
+
for (const v of indices) if (v < 0 || v >= this._network.vertices.length) throw new Error(`PathModel.translateVertices: invalid vertex ${v}`);
|
|
3114
|
+
const next_network = cloneNetwork(this._network);
|
|
3115
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(next_network);
|
|
3116
|
+
for (const v of indices) vne.translateVertex(v, [delta[0], delta[1]]);
|
|
3117
|
+
return new PathModel(vne.value, this._meta);
|
|
3118
|
+
}
|
|
3119
|
+
/** Translate one segment by `delta` — moves both endpoints, dragging
|
|
3120
|
+
* their tangents along (tangents are stored relative to vertices, so
|
|
3121
|
+
* this is automatic). Other segments connected to the moved endpoints
|
|
3122
|
+
* also follow at the shared vertex. */
|
|
3123
|
+
translateSegment(seg, delta) {
|
|
3124
|
+
if (seg < 0 || seg >= this._network.segments.length) throw new Error(`PathModel.translateSegment: invalid segment ${seg}`);
|
|
3125
|
+
const s = this._network.segments[seg];
|
|
3126
|
+
const unique = s.a === s.b ? [s.a] : [s.a, s.b];
|
|
3127
|
+
return this.translateVertices(unique, delta);
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Bend a curve segment by dragging a point at parameter `ca` to `cb`
|
|
3131
|
+
* (cb is in absolute doc-space). Delegates to vn's `bendSegment` —
|
|
3132
|
+
* which solves for the new ta/tb that put `B(ca) === cb`, holding the
|
|
3133
|
+
* endpoints fixed.
|
|
3134
|
+
*
|
|
3135
|
+
* The "frozen" snapshot of the segment at gesture start is the caller's
|
|
3136
|
+
* responsibility. Convention: call this from a preview session where
|
|
3137
|
+
* each frame replays from the baseline (same pattern as translate).
|
|
3138
|
+
*/
|
|
3139
|
+
bendSegment(seg, ca, cb, frozen) {
|
|
3140
|
+
if (seg < 0 || seg >= this._network.segments.length) throw new Error(`PathModel.bendSegment: invalid segment ${seg}`);
|
|
3141
|
+
const next_network = cloneNetwork(this._network);
|
|
3142
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(next_network);
|
|
3143
|
+
vne.bendSegment(seg, ca, [cb[0], cb[1]], {
|
|
3144
|
+
a: [frozen.a[0], frozen.a[1]],
|
|
3145
|
+
b: [frozen.b[0], frozen.b[1]],
|
|
3146
|
+
ta: [frozen.ta[0], frozen.ta[1]],
|
|
3147
|
+
tb: [frozen.tb[0], frozen.tb[1]]
|
|
3148
|
+
});
|
|
3149
|
+
return new PathModel(vne.value, this._meta);
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Move one tangent control point to a new absolute position. Mirror
|
|
3153
|
+
* policy follows vn's `updateTangent`. The other tangent at the same
|
|
3154
|
+
* vertex is updated according to the policy.
|
|
3155
|
+
*
|
|
3156
|
+
* Returns a new PathModel; verb metadata is preserved verbatim.
|
|
3157
|
+
* `toSvgPathD` will demote (e.g. L → C) if the new tangents make the
|
|
3158
|
+
* recorded verb no longer match the geometry.
|
|
3159
|
+
*/
|
|
3160
|
+
setTangent(t, abs_pos, mirror = "auto") {
|
|
3161
|
+
const located = this._locateTangent(t);
|
|
3162
|
+
if (!located) throw new Error(`PathModel.setTangent: no segment found for tangent [${t[0]}, ${t[1]}]`);
|
|
3163
|
+
const { seg_index, control } = located;
|
|
3164
|
+
const seg = this._network.segments[seg_index];
|
|
3165
|
+
const anchor_idx = control === "ta" ? seg.a : seg.b;
|
|
3166
|
+
const anchor = this._network.vertices[anchor_idx];
|
|
3167
|
+
const value = [abs_pos[0] - anchor[0], abs_pos[1] - anchor[1]];
|
|
3168
|
+
const next_network = cloneNetwork(this._network);
|
|
3169
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(next_network);
|
|
3170
|
+
vne.updateTangent(seg_index, control, value, mirror);
|
|
3171
|
+
return new PathModel(vne.value, this._meta);
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Split segment `seg` at parametric position `t ∈ [0,1]`, inserting a
|
|
3175
|
+
* new vertex. Returns the new model and the **canonical (path-order)**
|
|
3176
|
+
* index of the inserted vertex.
|
|
3177
|
+
*
|
|
3178
|
+
* Verb metadata for the split: the original segment's verb propagates
|
|
3179
|
+
* to BOTH halves if it was a curve type (`C`/`S`/`Q`/`T`/`A`); for
|
|
3180
|
+
* straight verbs (`L`/`H`/`V`), the split halves stay straight (their
|
|
3181
|
+
* tangents are zero from vn's `preserveZero` path when both originals
|
|
3182
|
+
* were zero). Arc-group identity is dropped from the halves — the
|
|
3183
|
+
* arc is broken once split (the emitter will fall back to `C`/`L`).
|
|
3184
|
+
*
|
|
3185
|
+
* **Index space contract.** `VectorNetworkEditor.splitSegment` APPENDS
|
|
3186
|
+
* the new vertex at the end of the network's vertices array — its
|
|
3187
|
+
* index is the in-memory insertion order. But `toSvgPathD` / `fromSvgPathD`
|
|
3188
|
+
* canonicalize vertices in path order, so the same vertex gets a
|
|
3189
|
+
* DIFFERENT index in the d-derived model that consumers re-parse each
|
|
3190
|
+
* frame (e.g., the host's `handle_translate_vertices`). Returning the
|
|
3191
|
+
* insertion-order index causes the classic split-and-drag bug: the
|
|
3192
|
+
* surface holds index N (insertion-order) but the live model has
|
|
3193
|
+
* index M (path-order) at that position — drag moves the wrong vertex
|
|
3194
|
+
* and the user sees "split happened but the new vertex doesn't move".
|
|
3195
|
+
*
|
|
3196
|
+
* To prevent that, we round-trip the post-split model through
|
|
3197
|
+
* `toSvgPathD` → `fromSvgPathD` and return the canonical (path-order)
|
|
3198
|
+
* index of the new vertex. The returned `model` is the canonical
|
|
3199
|
+
* one, so any subsequent op on it uses the same index space the d
|
|
3200
|
+
* roundtrip exposes. See `__tests__/README.md` §"index identity
|
|
3201
|
+
* across the `d` round-trip" for the test pattern that pins this.
|
|
3202
|
+
*/
|
|
3203
|
+
splitSegment(seg, t) {
|
|
3204
|
+
if (seg < 0 || seg >= this._network.segments.length) throw new Error(`PathModel.splitSegment: invalid segment ${seg}`);
|
|
3205
|
+
const next_network = cloneNetwork(this._network);
|
|
3206
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(next_network);
|
|
3207
|
+
const in_memory_new_vertex = vne.splitSegment({
|
|
3208
|
+
segment: seg,
|
|
3209
|
+
t
|
|
3210
|
+
});
|
|
3211
|
+
const orig = this._meta[seg];
|
|
3212
|
+
const half = { source_verb: orig?.source_verb };
|
|
3213
|
+
const half_first = { ...half };
|
|
3214
|
+
const half_second = {
|
|
3215
|
+
...half,
|
|
3216
|
+
is_close_segment: orig?.is_close_segment
|
|
3217
|
+
};
|
|
3218
|
+
const next_meta = [
|
|
3219
|
+
...this._meta.slice(0, seg),
|
|
3220
|
+
half_first,
|
|
3221
|
+
half_second,
|
|
3222
|
+
...this._meta.slice(seg + 1)
|
|
3223
|
+
];
|
|
3224
|
+
const new_vertex_pos = vne.value.vertices[in_memory_new_vertex];
|
|
3225
|
+
const target_d = new PathModel(vne.value, next_meta).toSvgPathD();
|
|
3226
|
+
const canonical_model = PathModel.fromSvgPathD(target_d);
|
|
3227
|
+
const canonical_vertices = canonical_model._network.vertices;
|
|
3228
|
+
let canonical_new_vertex = -1;
|
|
3229
|
+
for (let i = 0; i < canonical_vertices.length; i++) {
|
|
3230
|
+
const v = canonical_vertices[i];
|
|
3231
|
+
if (Math.abs(v[0] - new_vertex_pos[0]) < 1e-9 && Math.abs(v[1] - new_vertex_pos[1]) < 1e-9) {
|
|
3232
|
+
canonical_new_vertex = i;
|
|
3233
|
+
break;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
if (canonical_new_vertex < 0) canonical_new_vertex = in_memory_new_vertex;
|
|
3237
|
+
return {
|
|
3238
|
+
model: canonical_model,
|
|
3239
|
+
new_vertex: canonical_new_vertex
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Doc-space position of a tangent control point. `t` references a
|
|
3244
|
+
* segment and which end (`a` or `b`) the tangent belongs to; the
|
|
3245
|
+
* result is `vertex + tangent_value + origin`. Returns null if no
|
|
3246
|
+
* segment has this tangent (e.g. the vertex is isolated).
|
|
3247
|
+
*/
|
|
3248
|
+
tangentAbsolute(t, origin) {
|
|
3249
|
+
const located = this._locateTangent(t);
|
|
3250
|
+
if (!located) return null;
|
|
3251
|
+
const { seg_index, control } = located;
|
|
3252
|
+
const seg = this._network.segments[seg_index];
|
|
3253
|
+
const anchor_idx = control === "ta" ? seg.a : seg.b;
|
|
3254
|
+
const anchor = this._network.vertices[anchor_idx];
|
|
3255
|
+
const value = control === "ta" ? seg.ta : seg.tb;
|
|
3256
|
+
return [anchor[0] + value[0] + origin[0], anchor[1] + value[1] + origin[1]];
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Vertices "neighbouring" the current selection — these are the
|
|
3260
|
+
* vertices whose tangent handles should render in chrome.
|
|
3261
|
+
*
|
|
3262
|
+
* Two-phase, mirrors `editor/grida-canvas/reducers/methods/vector.ts`
|
|
3263
|
+
* `getUXNeighbouringVertices`:
|
|
3264
|
+
*
|
|
3265
|
+
* 1. Collect "active" vertices:
|
|
3266
|
+
* - every selected vertex
|
|
3267
|
+
* - every tangent-owning vertex
|
|
3268
|
+
* - both endpoints of every selected segment
|
|
3269
|
+
* 2. Expand uniformly to 1-hop neighbours (vertices sharing a segment
|
|
3270
|
+
* with any active vertex).
|
|
3271
|
+
*
|
|
3272
|
+
* Without phase 2 for tangent / segment selections, selecting only a
|
|
3273
|
+
* tangent would hide neighbouring-vertex tangents — the user loses
|
|
3274
|
+
* spatial context. Phase 2 makes the affordance symmetric: whatever
|
|
3275
|
+
* triggered selection, the 1-hop ring of tangent handles is visible.
|
|
3276
|
+
*
|
|
3277
|
+
* Sorted ascending; deduped.
|
|
3278
|
+
*/
|
|
3279
|
+
neighbouringVertices(sel) {
|
|
3280
|
+
const { vertices, segments } = this._network;
|
|
3281
|
+
const active = /* @__PURE__ */ new Set();
|
|
3282
|
+
const add_if_valid = (v) => {
|
|
3283
|
+
if (v >= 0 && v < vertices.length) active.add(v);
|
|
3284
|
+
};
|
|
3285
|
+
for (const v of sel.vertices) add_if_valid(v);
|
|
3286
|
+
for (const seg_idx of sel.segments) {
|
|
3287
|
+
if (seg_idx < 0 || seg_idx >= segments.length) continue;
|
|
3288
|
+
const s = segments[seg_idx];
|
|
3289
|
+
add_if_valid(s.a);
|
|
3290
|
+
add_if_valid(s.b);
|
|
3291
|
+
}
|
|
3292
|
+
for (const t of sel.tangents) {
|
|
3293
|
+
const located = this._locateTangent(t);
|
|
3294
|
+
if (!located) continue;
|
|
3295
|
+
add_if_valid(t[0]);
|
|
3296
|
+
const s = segments[located.seg_index];
|
|
3297
|
+
add_if_valid(s.a);
|
|
3298
|
+
add_if_valid(s.b);
|
|
3299
|
+
}
|
|
3300
|
+
const out = new Set(active);
|
|
3301
|
+
if (active.size > 0) {
|
|
3302
|
+
const vne = new _grida_vn.default.VectorNetworkEditor(this._network);
|
|
3303
|
+
for (const v of active) for (const n of vne.getNeighboringVerticies(v)) if (n >= 0 && n < vertices.length) out.add(n);
|
|
3304
|
+
}
|
|
3305
|
+
return Array.from(out).sort((x, y) => x - y);
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* True iff segment `seg`'s curve is entirely contained in the rect.
|
|
3309
|
+
* Delegates to `cmath.bezier.containedByRect`.
|
|
3310
|
+
*/
|
|
3311
|
+
segmentContainedByRect(seg, rect, origin = [0, 0]) {
|
|
3312
|
+
if (seg < 0 || seg >= this._network.segments.length) return false;
|
|
3313
|
+
const s = this._network.segments[seg];
|
|
3314
|
+
const a = this._network.vertices[s.a];
|
|
3315
|
+
const b = this._network.vertices[s.b];
|
|
3316
|
+
const local_rect = {
|
|
3317
|
+
x: rect.x - origin[0],
|
|
3318
|
+
y: rect.y - origin[1],
|
|
3319
|
+
width: rect.width,
|
|
3320
|
+
height: rect.height
|
|
3321
|
+
};
|
|
3322
|
+
return _grida_cmath.default.bezier.containedByRect([a[0], a[1]], [b[0], b[1]], [s.ta[0], s.ta[1]], [s.tb[0], s.tb[1]], local_rect);
|
|
3323
|
+
}
|
|
3324
|
+
/** @internal */
|
|
3325
|
+
_rawNetwork() {
|
|
3326
|
+
return this._network;
|
|
3327
|
+
}
|
|
3328
|
+
/** @internal */
|
|
3329
|
+
_rawMeta() {
|
|
3330
|
+
return this._meta;
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Map a `TangentRef` to a concrete `(segment_index, control)` pair.
|
|
3334
|
+
*
|
|
3335
|
+
* `[v, 0]` → first segment whose `a === v` (its `ta`).
|
|
3336
|
+
* `[v, 1]` → first segment whose `b === v` (its `tb`).
|
|
3337
|
+
*
|
|
3338
|
+
* Y-junctions (multi-outgoing or multi-incoming) are uncommon for SVG
|
|
3339
|
+
* `<path>` content; v1 picks the first match. If we ever support those
|
|
3340
|
+
* cleanly, extend `TangentRef` to carry the segment id explicitly.
|
|
3341
|
+
*/
|
|
3342
|
+
_locateTangent(t) {
|
|
3343
|
+
const [vertex_idx, end] = t;
|
|
3344
|
+
const segs = this._network.segments;
|
|
3345
|
+
for (let i = 0; i < segs.length; i++) {
|
|
3346
|
+
const s = segs[i];
|
|
3347
|
+
if (end === 0 && s.a === vertex_idx) return {
|
|
3348
|
+
seg_index: i,
|
|
3349
|
+
control: "ta"
|
|
3350
|
+
};
|
|
3351
|
+
if (end === 1 && s.b === vertex_idx) return {
|
|
3352
|
+
seg_index: i,
|
|
3353
|
+
control: "tb"
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
return null;
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
function cloneNetwork(net) {
|
|
3360
|
+
return {
|
|
3361
|
+
vertices: net.vertices.map((v) => [v[0], v[1]]),
|
|
3362
|
+
segments: net.segments.map((s) => ({
|
|
3363
|
+
a: s.a,
|
|
3364
|
+
b: s.b,
|
|
3365
|
+
ta: [s.ta[0], s.ta[1]],
|
|
3366
|
+
tb: [s.tb[0], s.tb[1]]
|
|
3367
|
+
}))
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Walks the SVG path commands once, building the vn network AND a parallel
|
|
3372
|
+
* `meta` array. Mirrors the structure of `vn.fromSVGPathData` but tags each
|
|
3373
|
+
* emitted segment with its originating verb. The two stay logically in lock-
|
|
3374
|
+
* step: every segment vn would push, we push one meta entry for.
|
|
3375
|
+
*/
|
|
3376
|
+
function parseWithVerbs(d) {
|
|
3377
|
+
const commands = new _grida_svg_pathdata.SVGPathData(d).toAbs().commands;
|
|
3378
|
+
const vne = new _grida_vn.default.VectorNetworkEditor();
|
|
3379
|
+
const meta = [];
|
|
3380
|
+
let last_point = null;
|
|
3381
|
+
let last_quadratic_control = null;
|
|
3382
|
+
/** Vertex index of the current point. Tracked explicitly because
|
|
3383
|
+
* `vne.addVertex` reuses an existing vertex when the coordinate matches,
|
|
3384
|
+
* so `vne.vertices.length - 1` is NOT reliable for "current vertex." */
|
|
3385
|
+
let current_idx = -1;
|
|
3386
|
+
/** Vertex index of the current subpath's start (set on M). */
|
|
3387
|
+
let subpath_start_idx = -1;
|
|
3388
|
+
/** Monotonic counter for arc groups. */
|
|
3389
|
+
let arc_group_seq = 0;
|
|
3390
|
+
const pushSegmentMeta = (entry) => {
|
|
3391
|
+
meta.push(entry);
|
|
3392
|
+
};
|
|
3393
|
+
for (const command of commands) {
|
|
3394
|
+
const { type } = command;
|
|
3395
|
+
switch (type) {
|
|
3396
|
+
case _grida_svg_pathdata.SVGPathData.MOVE_TO: {
|
|
3397
|
+
const { x, y } = command;
|
|
3398
|
+
current_idx = vne.addVertex([x, y]);
|
|
3399
|
+
subpath_start_idx = current_idx;
|
|
3400
|
+
last_point = [x, y];
|
|
3401
|
+
last_quadratic_control = null;
|
|
3402
|
+
break;
|
|
3403
|
+
}
|
|
3404
|
+
case _grida_svg_pathdata.SVGPathData.LINE_TO: {
|
|
3405
|
+
const { x, y } = command;
|
|
3406
|
+
if (last_point) {
|
|
3407
|
+
current_idx = vne.addVertex([x, y], current_idx);
|
|
3408
|
+
pushSegmentMeta({ source_verb: "L" });
|
|
3409
|
+
}
|
|
3410
|
+
last_point = [x, y];
|
|
3411
|
+
last_quadratic_control = null;
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
case _grida_svg_pathdata.SVGPathData.HORIZ_LINE_TO: {
|
|
3415
|
+
const { x } = command;
|
|
3416
|
+
if (last_point) {
|
|
3417
|
+
current_idx = vne.addVertex([x, last_point[1]], current_idx);
|
|
3418
|
+
pushSegmentMeta({ source_verb: "H" });
|
|
3419
|
+
}
|
|
3420
|
+
last_point = [x, last_point ? last_point[1] : 0];
|
|
3421
|
+
last_quadratic_control = null;
|
|
3422
|
+
break;
|
|
3423
|
+
}
|
|
3424
|
+
case _grida_svg_pathdata.SVGPathData.VERT_LINE_TO: {
|
|
3425
|
+
const { y } = command;
|
|
3426
|
+
if (last_point) {
|
|
3427
|
+
current_idx = vne.addVertex([last_point[0], y], current_idx);
|
|
3428
|
+
pushSegmentMeta({ source_verb: "V" });
|
|
3429
|
+
}
|
|
3430
|
+
last_point = [last_point ? last_point[0] : 0, y];
|
|
3431
|
+
last_quadratic_control = null;
|
|
3432
|
+
break;
|
|
3433
|
+
}
|
|
3434
|
+
case _grida_svg_pathdata.SVGPathData.CURVE_TO: {
|
|
3435
|
+
const { x, y } = command;
|
|
3436
|
+
if (last_point) {
|
|
3437
|
+
const ta = [command.x1 - last_point[0], command.y1 - last_point[1]];
|
|
3438
|
+
const tb = [command.x2 - x, command.y2 - y];
|
|
3439
|
+
current_idx = vne.addVertex([x, y], current_idx, ta, tb);
|
|
3440
|
+
pushSegmentMeta({ source_verb: "C" });
|
|
3441
|
+
}
|
|
3442
|
+
last_point = [x, y];
|
|
3443
|
+
last_quadratic_control = null;
|
|
3444
|
+
break;
|
|
3445
|
+
}
|
|
3446
|
+
case _grida_svg_pathdata.SVGPathData.SMOOTH_CURVE_TO: {
|
|
3447
|
+
const { x, y, x2, y2 } = command;
|
|
3448
|
+
if (last_point) {
|
|
3449
|
+
const ta = vne.getNextMirroredTangent(current_idx);
|
|
3450
|
+
const tb = [x2 - x, y2 - y];
|
|
3451
|
+
current_idx = vne.addVertex([x, y], current_idx, ta, tb);
|
|
3452
|
+
pushSegmentMeta({ source_verb: "S" });
|
|
3453
|
+
}
|
|
3454
|
+
last_point = [x, y];
|
|
3455
|
+
last_quadratic_control = null;
|
|
3456
|
+
break;
|
|
3457
|
+
}
|
|
3458
|
+
case _grida_svg_pathdata.SVGPathData.QUAD_TO:
|
|
3459
|
+
if (last_point) {
|
|
3460
|
+
const control = [command.x1, command.y1];
|
|
3461
|
+
const end = [command.x, command.y];
|
|
3462
|
+
const ta = [2 / 3 * (control[0] - last_point[0]), 2 / 3 * (control[1] - last_point[1])];
|
|
3463
|
+
const tb = [2 / 3 * (control[0] - end[0]), 2 / 3 * (control[1] - end[1])];
|
|
3464
|
+
current_idx = vne.addVertex(end, current_idx, ta, tb);
|
|
3465
|
+
pushSegmentMeta({ source_verb: "Q" });
|
|
3466
|
+
last_point = end;
|
|
3467
|
+
last_quadratic_control = control;
|
|
3468
|
+
}
|
|
3469
|
+
break;
|
|
3470
|
+
case _grida_svg_pathdata.SVGPathData.SMOOTH_QUAD_TO:
|
|
3471
|
+
if (last_point) {
|
|
3472
|
+
const end = [command.x, command.y];
|
|
3473
|
+
const control = last_quadratic_control ? [2 * last_point[0] - last_quadratic_control[0], 2 * last_point[1] - last_quadratic_control[1]] : [last_point[0], last_point[1]];
|
|
3474
|
+
const ta = [2 / 3 * (control[0] - last_point[0]), 2 / 3 * (control[1] - last_point[1])];
|
|
3475
|
+
const tb = [2 / 3 * (control[0] - end[0]), 2 / 3 * (control[1] - end[1])];
|
|
3476
|
+
current_idx = vne.addVertex(end, current_idx, ta, tb);
|
|
3477
|
+
pushSegmentMeta({ source_verb: "T" });
|
|
3478
|
+
last_point = end;
|
|
3479
|
+
last_quadratic_control = control;
|
|
3480
|
+
}
|
|
3481
|
+
break;
|
|
3482
|
+
case _grida_svg_pathdata.SVGPathData.ARC: {
|
|
3483
|
+
const { rX, rY, xRot, lArcFlag, sweepFlag, x, y } = command;
|
|
3484
|
+
if (last_point) {
|
|
3485
|
+
const [x1, y1] = last_point;
|
|
3486
|
+
const curves = _grida_cmath.default.bezier.a2c(x1, y1, rX, rY, xRot, lArcFlag, sweepFlag, x, y);
|
|
3487
|
+
const seg_count = curves.length / 6;
|
|
3488
|
+
const group_id = ++arc_group_seq;
|
|
3489
|
+
let current_point = last_point;
|
|
3490
|
+
let seq = 0;
|
|
3491
|
+
for (let i = 0; i < curves.length; i += 6) {
|
|
3492
|
+
const [cx1, cy1, cx2, cy2, ex, ey] = curves.slice(i, i + 6);
|
|
3493
|
+
const end_point = [ex, ey];
|
|
3494
|
+
const ta = [cx1 - current_point[0], cy1 - current_point[1]];
|
|
3495
|
+
const tb = [cx2 - end_point[0], cy2 - end_point[1]];
|
|
3496
|
+
current_idx = vne.addVertex(end_point, current_idx, ta, tb);
|
|
3497
|
+
const is_last = seq === seg_count - 1;
|
|
3498
|
+
pushSegmentMeta({
|
|
3499
|
+
source_verb: "A",
|
|
3500
|
+
arc: {
|
|
3501
|
+
group_id,
|
|
3502
|
+
rx: rX,
|
|
3503
|
+
ry: rY,
|
|
3504
|
+
x_rot: xRot,
|
|
3505
|
+
large_arc_flag: lArcFlag,
|
|
3506
|
+
sweep_flag: sweepFlag,
|
|
3507
|
+
baseline_ta: [ta[0], ta[1]],
|
|
3508
|
+
baseline_tb: [tb[0], tb[1]],
|
|
3509
|
+
baseline_b_abs: [end_point[0], end_point[1]],
|
|
3510
|
+
seq,
|
|
3511
|
+
count: seg_count,
|
|
3512
|
+
original_end: is_last ? [x, y] : void 0
|
|
3513
|
+
}
|
|
3514
|
+
});
|
|
3515
|
+
current_point = end_point;
|
|
3516
|
+
seq++;
|
|
3517
|
+
}
|
|
3518
|
+
last_point = current_point;
|
|
3519
|
+
}
|
|
3520
|
+
last_quadratic_control = null;
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
case _grida_svg_pathdata.SVGPathData.CLOSE_PATH:
|
|
3524
|
+
if (current_idx !== -1 && subpath_start_idx !== -1 && current_idx !== subpath_start_idx) {
|
|
3525
|
+
vne.addSegment(current_idx, subpath_start_idx);
|
|
3526
|
+
pushSegmentMeta({
|
|
3527
|
+
source_verb: "Z",
|
|
3528
|
+
is_close_segment: true
|
|
3529
|
+
});
|
|
3530
|
+
current_idx = subpath_start_idx;
|
|
3531
|
+
last_point = vne.vertices[subpath_start_idx];
|
|
3532
|
+
} else if (current_idx !== -1 && current_idx === subpath_start_idx && meta.length > 0) {
|
|
3533
|
+
const last_seg = vne.segments[vne.segments.length - 1];
|
|
3534
|
+
if (last_seg && last_seg.b === subpath_start_idx) meta[meta.length - 1] = {
|
|
3535
|
+
...meta[meta.length - 1],
|
|
3536
|
+
is_close_segment: true
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
last_quadratic_control = null;
|
|
3540
|
+
break;
|
|
3541
|
+
default: throw new Error(`Unsupported path command type: ${type}`);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
return {
|
|
3545
|
+
network: vne.value,
|
|
3546
|
+
meta
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
const EPSILON = 1e-9;
|
|
3550
|
+
function approxEqual(a, b, eps = EPSILON) {
|
|
3551
|
+
return Math.abs(a - b) <= eps;
|
|
3552
|
+
}
|
|
3553
|
+
function vec2Equal(a, b, eps = EPSILON) {
|
|
3554
|
+
return approxEqual(a[0], b[0], eps) && approxEqual(a[1], b[1], eps);
|
|
3555
|
+
}
|
|
3556
|
+
function isZeroTangent(t) {
|
|
3557
|
+
return approxEqual(t[0], 0) && approxEqual(t[1], 0);
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Returns true iff the segment's tangents are consistent with a single
|
|
3561
|
+
* quadratic Bézier control point at ratio 2/3 (i.e. the segment was emitted
|
|
3562
|
+
* from a `Q` or `T` command and has not been edited).
|
|
3563
|
+
*
|
|
3564
|
+
* For a quadratic with start=A, end=B, control=C:
|
|
3565
|
+
* ta = 2/3 * (C - A) => C = A + (3/2) * ta
|
|
3566
|
+
* tb = 2/3 * (C - B) => C = B + (3/2) * tb
|
|
3567
|
+
* Both must yield the same C.
|
|
3568
|
+
*/
|
|
3569
|
+
function tangentsRepresentQuadratic(a, b, ta, tb) {
|
|
3570
|
+
const c_from_a = [a[0] + 1.5 * ta[0], a[1] + 1.5 * ta[1]];
|
|
3571
|
+
if (vec2Equal(c_from_a, [b[0] + 1.5 * tb[0], b[1] + 1.5 * tb[1]], 1e-6)) return {
|
|
3572
|
+
ok: true,
|
|
3573
|
+
control: c_from_a
|
|
3574
|
+
};
|
|
3575
|
+
return { ok: false };
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* Returns true iff `ta` mirrors the previous segment's `tb` (i.e. the
|
|
3579
|
+
* vertex is a smooth join — what `S`/`T` commands require).
|
|
3580
|
+
*
|
|
3581
|
+
* For `ta` at vertex V (segment a=V) to mirror previous segment's `tb`
|
|
3582
|
+
* (whose b=V), we need: ta == -prev.tb (both expressed relative to V).
|
|
3583
|
+
*/
|
|
3584
|
+
function isSmoothJoin(prev_tb, curr_ta) {
|
|
3585
|
+
return vec2Equal([curr_ta[0], curr_ta[1]], [-prev_tb[0], -prev_tb[1]], 1e-6);
|
|
3586
|
+
}
|
|
3587
|
+
/**
|
|
3588
|
+
* Determines whether an arc-group's segments are byte-equal to their parse-
|
|
3589
|
+
* time baselines. If any segment in the group has been edited (vertex moved,
|
|
3590
|
+
* tangent changed), the entire arc must be promoted to C.
|
|
3591
|
+
*/
|
|
3592
|
+
function isArcGroupUnchanged(segments, meta, vertices, group_id) {
|
|
3593
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3594
|
+
const m = meta[i];
|
|
3595
|
+
if (m?.arc?.group_id !== group_id) continue;
|
|
3596
|
+
const seg = segments[i];
|
|
3597
|
+
if (!vec2Equal(seg.ta, m.arc.baseline_ta)) return false;
|
|
3598
|
+
if (!vec2Equal(seg.tb, m.arc.baseline_tb)) return false;
|
|
3599
|
+
if (!vec2Equal(vertices[seg.b], m.arc.baseline_b_abs)) return false;
|
|
3600
|
+
}
|
|
3601
|
+
return true;
|
|
3602
|
+
}
|
|
3603
|
+
function emitWithVerbs(network, meta) {
|
|
3604
|
+
const { vertices, segments } = network;
|
|
3605
|
+
if (segments.length === 0) return "";
|
|
3606
|
+
const commands = [];
|
|
3607
|
+
const arc_unchanged = /* @__PURE__ */ new Map();
|
|
3608
|
+
const arcStillValid = (group_id) => {
|
|
3609
|
+
const cached = arc_unchanged.get(group_id);
|
|
3610
|
+
if (cached !== void 0) return cached;
|
|
3611
|
+
const ok = isArcGroupUnchanged(segments, meta, vertices, group_id);
|
|
3612
|
+
arc_unchanged.set(group_id, ok);
|
|
3613
|
+
return ok;
|
|
3614
|
+
};
|
|
3615
|
+
let current_start = null;
|
|
3616
|
+
let previous_end = null;
|
|
3617
|
+
let prev_segment_tb = null;
|
|
3618
|
+
/** When true, the previous segment was emitted as a quadratic (Q or T) and `prev_quad_control` is valid. */
|
|
3619
|
+
let prev_quad_control = null;
|
|
3620
|
+
/** Skip the next K iterations because we already emitted an arc for them. */
|
|
3621
|
+
let skip_to_index = -1;
|
|
3622
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3623
|
+
if (i < skip_to_index) continue;
|
|
3624
|
+
const segment = segments[i];
|
|
3625
|
+
const m = meta[i] ?? {};
|
|
3626
|
+
const { a, b, ta, tb } = segment;
|
|
3627
|
+
const start = vertices[a];
|
|
3628
|
+
const end = vertices[b];
|
|
3629
|
+
if (previous_end !== a) {
|
|
3630
|
+
commands.push({
|
|
3631
|
+
type: _grida_svg_pathdata.SVGPathData.MOVE_TO,
|
|
3632
|
+
x: start[0],
|
|
3633
|
+
y: start[1],
|
|
3634
|
+
relative: false
|
|
3635
|
+
});
|
|
3636
|
+
current_start = a;
|
|
3637
|
+
prev_segment_tb = null;
|
|
3638
|
+
prev_quad_control = null;
|
|
3639
|
+
}
|
|
3640
|
+
const is_straight = isZeroTangent(ta) && isZeroTangent(tb);
|
|
3641
|
+
const is_closing = m.is_close_segment === true && current_start !== null && b === current_start;
|
|
3642
|
+
if (m.arc && m.source_verb === "A" && arcStillValid(m.arc.group_id)) {
|
|
3643
|
+
let last_idx = i;
|
|
3644
|
+
while (last_idx + 1 < segments.length && meta[last_idx + 1]?.arc?.group_id === m.arc.group_id) last_idx++;
|
|
3645
|
+
const last_seg = segments[last_idx];
|
|
3646
|
+
const last_end = meta[last_idx]?.arc?.original_end ?? vertices[last_seg.b];
|
|
3647
|
+
commands.push({
|
|
3648
|
+
type: _grida_svg_pathdata.SVGPathData.ARC,
|
|
3649
|
+
rX: m.arc.rx,
|
|
3650
|
+
rY: m.arc.ry,
|
|
3651
|
+
xRot: m.arc.x_rot,
|
|
3652
|
+
lArcFlag: m.arc.large_arc_flag,
|
|
3653
|
+
sweepFlag: m.arc.sweep_flag,
|
|
3654
|
+
x: last_end[0],
|
|
3655
|
+
y: last_end[1],
|
|
3656
|
+
relative: false
|
|
3657
|
+
});
|
|
3658
|
+
previous_end = last_seg.b;
|
|
3659
|
+
prev_segment_tb = last_seg.tb;
|
|
3660
|
+
prev_quad_control = null;
|
|
3661
|
+
skip_to_index = last_idx + 1;
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
if (is_closing && is_straight) {
|
|
3665
|
+
commands.push({ type: _grida_svg_pathdata.SVGPathData.CLOSE_PATH });
|
|
3666
|
+
previous_end = null;
|
|
3667
|
+
current_start = null;
|
|
3668
|
+
prev_segment_tb = null;
|
|
3669
|
+
prev_quad_control = null;
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
if (is_straight) {
|
|
3673
|
+
if (m.source_verb === "H" && approxEqual(end[1], start[1])) commands.push({
|
|
3674
|
+
type: _grida_svg_pathdata.SVGPathData.HORIZ_LINE_TO,
|
|
3675
|
+
x: end[0],
|
|
3676
|
+
relative: false
|
|
3677
|
+
});
|
|
3678
|
+
else if (m.source_verb === "V" && approxEqual(end[0], start[0])) commands.push({
|
|
3679
|
+
type: _grida_svg_pathdata.SVGPathData.VERT_LINE_TO,
|
|
3680
|
+
y: end[1],
|
|
3681
|
+
relative: false
|
|
3682
|
+
});
|
|
3683
|
+
else commands.push({
|
|
3684
|
+
type: _grida_svg_pathdata.SVGPathData.LINE_TO,
|
|
3685
|
+
x: end[0],
|
|
3686
|
+
y: end[1],
|
|
3687
|
+
relative: false
|
|
3688
|
+
});
|
|
3689
|
+
previous_end = b;
|
|
3690
|
+
prev_segment_tb = tb;
|
|
3691
|
+
prev_quad_control = null;
|
|
3692
|
+
if (current_start !== null && b === current_start) {}
|
|
3693
|
+
continue;
|
|
3694
|
+
}
|
|
3695
|
+
let emitted = false;
|
|
3696
|
+
if (!emitted && (m.source_verb === "Q" || m.source_verb === "T")) {
|
|
3697
|
+
const quad = tangentsRepresentQuadratic(start, end, ta, tb);
|
|
3698
|
+
if (quad.ok) {
|
|
3699
|
+
if (m.source_verb === "T" && prev_quad_control !== null && vec2Equal([start[0] - prev_quad_control[0], start[1] - prev_quad_control[1]], [quad.control[0] - start[0], quad.control[1] - start[1]], 1e-6)) commands.push({
|
|
3700
|
+
type: _grida_svg_pathdata.SVGPathData.SMOOTH_QUAD_TO,
|
|
3701
|
+
x: end[0],
|
|
3702
|
+
y: end[1],
|
|
3703
|
+
relative: false
|
|
3704
|
+
});
|
|
3705
|
+
else commands.push({
|
|
3706
|
+
type: _grida_svg_pathdata.SVGPathData.QUAD_TO,
|
|
3707
|
+
x1: quad.control[0],
|
|
3708
|
+
y1: quad.control[1],
|
|
3709
|
+
x: end[0],
|
|
3710
|
+
y: end[1],
|
|
3711
|
+
relative: false
|
|
3712
|
+
});
|
|
3713
|
+
previous_end = b;
|
|
3714
|
+
prev_segment_tb = tb;
|
|
3715
|
+
prev_quad_control = quad.control;
|
|
3716
|
+
emitted = true;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
if (!emitted && m.source_verb === "S" && prev_segment_tb !== null && isSmoothJoin(prev_segment_tb, ta)) {
|
|
3720
|
+
const c2 = [end[0] + tb[0], end[1] + tb[1]];
|
|
3721
|
+
commands.push({
|
|
3722
|
+
type: _grida_svg_pathdata.SVGPathData.SMOOTH_CURVE_TO,
|
|
3723
|
+
x2: c2[0],
|
|
3724
|
+
y2: c2[1],
|
|
3725
|
+
x: end[0],
|
|
3726
|
+
y: end[1],
|
|
3727
|
+
relative: false
|
|
3728
|
+
});
|
|
3729
|
+
previous_end = b;
|
|
3730
|
+
prev_segment_tb = tb;
|
|
3731
|
+
prev_quad_control = null;
|
|
3732
|
+
emitted = true;
|
|
3733
|
+
}
|
|
3734
|
+
if (!emitted) {
|
|
3735
|
+
const c1 = [start[0] + ta[0], start[1] + ta[1]];
|
|
3736
|
+
const c2 = [end[0] + tb[0], end[1] + tb[1]];
|
|
3737
|
+
commands.push({
|
|
3738
|
+
type: _grida_svg_pathdata.SVGPathData.CURVE_TO,
|
|
3739
|
+
x1: c1[0],
|
|
3740
|
+
y1: c1[1],
|
|
3741
|
+
x2: c2[0],
|
|
3742
|
+
y2: c2[1],
|
|
3743
|
+
x: end[0],
|
|
3744
|
+
y: end[1],
|
|
3745
|
+
relative: false
|
|
3746
|
+
});
|
|
3747
|
+
previous_end = b;
|
|
3748
|
+
prev_segment_tb = tb;
|
|
3749
|
+
prev_quad_control = null;
|
|
3750
|
+
}
|
|
3751
|
+
if (is_closing && !is_straight) {
|
|
3752
|
+
commands.push({ type: _grida_svg_pathdata.SVGPathData.CLOSE_PATH });
|
|
3753
|
+
previous_end = null;
|
|
3754
|
+
current_start = null;
|
|
3755
|
+
prev_segment_tb = null;
|
|
3756
|
+
prev_quad_control = null;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
return (0, _grida_svg_pathdata.encodeSVGPath)(commands);
|
|
3760
|
+
}
|
|
3761
|
+
//#endregion
|
|
3762
|
+
Object.defineProperty(exports, "DEFAULT_STYLE", {
|
|
3763
|
+
enumerable: true,
|
|
3764
|
+
get: function() {
|
|
3765
|
+
return DEFAULT_STYLE;
|
|
3766
|
+
}
|
|
3767
|
+
});
|
|
3768
|
+
Object.defineProperty(exports, "NudgeDwellWatcher", {
|
|
3769
|
+
enumerable: true,
|
|
3770
|
+
get: function() {
|
|
3771
|
+
return NudgeDwellWatcher;
|
|
3772
|
+
}
|
|
3773
|
+
});
|
|
3774
|
+
Object.defineProperty(exports, "PathModel", {
|
|
3775
|
+
enumerable: true,
|
|
3776
|
+
get: function() {
|
|
3777
|
+
return PathModel;
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
Object.defineProperty(exports, "ResizeOrchestrator", {
|
|
3781
|
+
enumerable: true,
|
|
3782
|
+
get: function() {
|
|
3783
|
+
return ResizeOrchestrator;
|
|
3784
|
+
}
|
|
3785
|
+
});
|
|
3786
|
+
Object.defineProperty(exports, "RotateOrchestrator", {
|
|
3787
|
+
enumerable: true,
|
|
3788
|
+
get: function() {
|
|
3789
|
+
return RotateOrchestrator;
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
Object.defineProperty(exports, "TOOL_CURSOR", {
|
|
3793
|
+
enumerable: true,
|
|
3794
|
+
get: function() {
|
|
3795
|
+
return TOOL_CURSOR;
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
Object.defineProperty(exports, "TranslateOrchestrator", {
|
|
3799
|
+
enumerable: true,
|
|
3800
|
+
get: function() {
|
|
3801
|
+
return TranslateOrchestrator;
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
3804
|
+
Object.defineProperty(exports, "__exportAll", {
|
|
3805
|
+
enumerable: true,
|
|
3806
|
+
get: function() {
|
|
3807
|
+
return __exportAll;
|
|
3808
|
+
}
|
|
3809
|
+
});
|
|
3810
|
+
Object.defineProperty(exports, "__toESM", {
|
|
3811
|
+
enumerable: true,
|
|
3812
|
+
get: function() {
|
|
3813
|
+
return __toESM;
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
Object.defineProperty(exports, "array_shallow_equal", {
|
|
3817
|
+
enumerable: true,
|
|
3818
|
+
get: function() {
|
|
3819
|
+
return array_shallow_equal;
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
Object.defineProperty(exports, "group", {
|
|
3823
|
+
enumerable: true,
|
|
3824
|
+
get: function() {
|
|
3825
|
+
return group;
|
|
3826
|
+
}
|
|
3827
|
+
});
|
|
3828
|
+
Object.defineProperty(exports, "hit_shape_svg", {
|
|
3829
|
+
enumerable: true,
|
|
3830
|
+
get: function() {
|
|
3831
|
+
return hit_shape_svg;
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
Object.defineProperty(exports, "insertions", {
|
|
3835
|
+
enumerable: true,
|
|
3836
|
+
get: function() {
|
|
3837
|
+
return insertions;
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
Object.defineProperty(exports, "is_text_input_focused", {
|
|
3841
|
+
enumerable: true,
|
|
3842
|
+
get: function() {
|
|
3843
|
+
return is_text_input_focused;
|
|
3844
|
+
}
|
|
3845
|
+
});
|
|
3846
|
+
Object.defineProperty(exports, "paint", {
|
|
3847
|
+
enumerable: true,
|
|
3848
|
+
get: function() {
|
|
3849
|
+
return paint;
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3852
|
+
Object.defineProperty(exports, "resize_pipeline", {
|
|
3853
|
+
enumerable: true,
|
|
3854
|
+
get: function() {
|
|
3855
|
+
return resize_pipeline;
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
Object.defineProperty(exports, "rotate_pipeline", {
|
|
3859
|
+
enumerable: true,
|
|
3860
|
+
get: function() {
|
|
3861
|
+
return rotate_pipeline;
|
|
3862
|
+
}
|
|
3863
|
+
});
|
|
3864
|
+
Object.defineProperty(exports, "transform", {
|
|
3865
|
+
enumerable: true,
|
|
3866
|
+
get: function() {
|
|
3867
|
+
return transform;
|
|
3868
|
+
}
|
|
3869
|
+
});
|
|
3870
|
+
Object.defineProperty(exports, "translate_pipeline", {
|
|
3871
|
+
enumerable: true,
|
|
3872
|
+
get: function() {
|
|
3873
|
+
return translate_pipeline;
|
|
3874
|
+
}
|
|
3875
|
+
});
|