@grida/svg-editor 1.0.0-alpha.16 → 1.0.0-alpha.18
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 +33 -0
- package/dist/{dom-BO2-E9oK.d.ts → dom-BMzX1CXZ.d.ts} +13 -1
- package/dist/{dom-U6ae5fQF.js → dom-DKQ4Vt3z.js} +105 -12
- package/dist/{dom-DOvcMvl4.mjs → dom-OP-kmK8k.mjs} +105 -12
- package/dist/{dom-98AUOfsP.d.mts → dom-TctdgRnn.d.mts} +13 -1
- package/dist/dom.d.mts +2 -2
- package/dist/dom.d.ts +2 -2
- package/dist/dom.js +1 -1
- package/dist/dom.mjs +1 -1
- package/dist/{editor-CYoGJ3Hf.d.ts → editor-BSxTUsW_.d.ts} +246 -4
- package/dist/{editor-C6Lj1In-.js → editor-Be6UrMeV.js} +384 -36
- package/dist/{editor-DKQOIKuU.mjs → editor-BkCbYCz2.mjs} +385 -36
- package/dist/{editor-D2eQe8lB.d.mts → editor-KqpIW1qm.d.mts} +246 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{model-D0nU_EkL.js → model-BLhMJZKJ.js} +373 -27
- package/dist/{model-L3t9ixT_.mjs → model-DU0GOMwM.mjs} +362 -28
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +2 -2
- package/dist/presets.mjs +1 -1
- package/dist/react.d.mts +10 -3
- package/dist/react.d.ts +10 -3
- package/dist/react.js +6 -4
- package/dist/react.mjs +6 -4
- package/package.json +5 -5
|
@@ -154,10 +154,20 @@ const GEOMETRY_ATTRS = new Set([
|
|
|
154
154
|
]);
|
|
155
155
|
/** `transform:` CSS property at the start of a declaration list or after `;`. */
|
|
156
156
|
const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
|
|
157
|
+
/** Trivia shape for editor-synthesized attribute tokens — parser-authored
|
|
158
|
+
* tokens carry their source trivia verbatim; tokens the editor appends get
|
|
159
|
+
* this one canonical shape (` name="value"`). */
|
|
160
|
+
const SYNTH_ATTR_TRIVIA = {
|
|
161
|
+
pre: " ",
|
|
162
|
+
eq_trivia: "",
|
|
163
|
+
eq_trailing: "",
|
|
164
|
+
quote: "\""
|
|
165
|
+
};
|
|
157
166
|
var SvgDocument = class SvgDocument {
|
|
158
167
|
constructor(svg) {
|
|
159
168
|
this.listeners = /* @__PURE__ */ new Set();
|
|
160
169
|
this._structure_version = 0;
|
|
170
|
+
this._revision = 0;
|
|
161
171
|
this._geometry_version = 0;
|
|
162
172
|
if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
163
173
|
this.source = svg;
|
|
@@ -205,6 +215,20 @@ var SvgDocument = class SvgDocument {
|
|
|
205
215
|
get structure_version() {
|
|
206
216
|
return this._structure_version;
|
|
207
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Total mutation counter — advances on EVERY listener-visible mutation
|
|
220
|
+
* (attribute, style, text, topology, load/reset), unlike the selective
|
|
221
|
+
* `structure_version` / `geometry_version` channels. The single
|
|
222
|
+
* edit-version source: anything derived from this document — the
|
|
223
|
+
* editor's `content_version` / `dirty`, memoized reads, a rendered
|
|
224
|
+
* projection — answers "am I current?" by comparing values, with no
|
|
225
|
+
* event-ordering dependence. Advances BEFORE listeners fire, so a
|
|
226
|
+
* read issued from inside a change listener already sees the new
|
|
227
|
+
* value.
|
|
228
|
+
*/
|
|
229
|
+
get revision() {
|
|
230
|
+
return this._revision;
|
|
231
|
+
}
|
|
208
232
|
/** See `_geometry_version` for what this counter signals. */
|
|
209
233
|
get geometry_version() {
|
|
210
234
|
return this._geometry_version;
|
|
@@ -221,15 +245,15 @@ var SvgDocument = class SvgDocument {
|
|
|
221
245
|
* settled glyph metrics. See ../../docs/geometry.md §Limitations.
|
|
222
246
|
*
|
|
223
247
|
* Deliberately does NOT call `emit()`: this is not a document edit, so
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* `
|
|
227
|
-
* out the geometry listeners itself.
|
|
248
|
+
* `revision` must not advance — no dirty flag, no undo, no render
|
|
249
|
+
* flush. The editor's `_internal.bump_geometry` advances
|
|
250
|
+
* `geometry_version` here and fans out the geometry listeners itself.
|
|
228
251
|
*/
|
|
229
252
|
bump_geometry() {
|
|
230
253
|
this._geometry_version++;
|
|
231
254
|
}
|
|
232
255
|
emit() {
|
|
256
|
+
this._revision++;
|
|
233
257
|
for (const fn of this.listeners) fn();
|
|
234
258
|
}
|
|
235
259
|
/** Notify subscribers — for callers that mutate directly via setAttr/etc. */
|
|
@@ -380,9 +404,7 @@ var SvgDocument = class SvgDocument {
|
|
|
380
404
|
local: name,
|
|
381
405
|
ns,
|
|
382
406
|
value,
|
|
383
|
-
|
|
384
|
-
eq_trivia: "",
|
|
385
|
-
quote: "\""
|
|
407
|
+
...SYNTH_ATTR_TRIVIA
|
|
386
408
|
});
|
|
387
409
|
if (structural) this._structure_version++;
|
|
388
410
|
if (geometry) this._geometry_version++;
|
|
@@ -638,9 +660,7 @@ var SvgDocument = class SvgDocument {
|
|
|
638
660
|
local: "d",
|
|
639
661
|
ns: null,
|
|
640
662
|
value: d,
|
|
641
|
-
|
|
642
|
-
eq_trivia: "",
|
|
643
|
-
quote: "\""
|
|
663
|
+
...SYNTH_ATTR_TRIVIA
|
|
644
664
|
});
|
|
645
665
|
let added_fill_none = false;
|
|
646
666
|
if (prev_local === "line" && this.get_attr(id, "fill") === null && this.get_style(id, "fill") === null) {
|
|
@@ -650,9 +670,7 @@ var SvgDocument = class SvgDocument {
|
|
|
650
670
|
local: "fill",
|
|
651
671
|
ns: null,
|
|
652
672
|
value: "none",
|
|
653
|
-
|
|
654
|
-
eq_trivia: "",
|
|
655
|
-
quote: "\""
|
|
673
|
+
...SYNTH_ATTR_TRIVIA
|
|
656
674
|
});
|
|
657
675
|
added_fill_none = true;
|
|
658
676
|
}
|
|
@@ -1019,7 +1037,7 @@ var SvgDocument = class SvgDocument {
|
|
|
1019
1037
|
}
|
|
1020
1038
|
}
|
|
1021
1039
|
emit_attr(a) {
|
|
1022
|
-
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.quote}${(0, _grida_svg_parser.encode_attr_value)(a.value, a.quote)}${a.quote}`;
|
|
1040
|
+
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.eq_trailing}${a.quote}${(0, _grida_svg_parser.encode_attr_value)(a.value, a.quote)}${a.quote}`;
|
|
1023
1041
|
}
|
|
1024
1042
|
};
|
|
1025
1043
|
function parse_inline_style(s) {
|
|
@@ -1037,6 +1055,131 @@ function parse_inline_style(s) {
|
|
|
1037
1055
|
}
|
|
1038
1056
|
return out;
|
|
1039
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Namespace prefixes resolvable without a source declaration. ONE table,
|
|
1060
|
+
* shared by the paste-side hoist (`insert_fragment`'s xmlns plan) and the
|
|
1061
|
+
* copy-side shell repair (clipboard payload extraction) — the two sides
|
|
1062
|
+
* form a round-trip and must agree: a prefix only one side knows would
|
|
1063
|
+
* produce payloads the other can't honor.
|
|
1064
|
+
*/
|
|
1065
|
+
const WELL_KNOWN_NS_PREFIXES = new Map([["xlink", _grida_svg_parser.XLINK_NS]]);
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/core/subtree.ts
|
|
1068
|
+
/**
|
|
1069
|
+
* The selection → subtree algebra the two extraction operations share,
|
|
1070
|
+
* plus the clone-specific member (`clone_plan`).
|
|
1071
|
+
*
|
|
1072
|
+
* The clipboard FRD
|
|
1073
|
+
* ([docs/wg/feat-svg-editor/clipboard.md](../../../../docs/wg/feat-svg-editor/clipboard.md)
|
|
1074
|
+
* §Two extraction operations) names two operations over a normalized
|
|
1075
|
+
* selection: **payload extraction** (copy — `core/clipboard.ts`) and
|
|
1076
|
+
* **subtree clone** (duplicate / clone-drag — this module; design note:
|
|
1077
|
+
* [docs/wg/feat-svg-editor/subtree-clone.md](../../../../docs/wg/feat-svg-editor/subtree-clone.md)).
|
|
1078
|
+
* They share exactly two things — selection normalization
|
|
1079
|
+
* ({@link subtree.normalize_roots}) and verbatim subtree serialization
|
|
1080
|
+
* (`SvgDocument.serialize_node`) — and nothing else.
|
|
1081
|
+
*
|
|
1082
|
+
* Unlike copy's payload, a clone carries **no reference closure and no
|
|
1083
|
+
* namespace shell**: the destination is the source document, every
|
|
1084
|
+
* `url(#…)` / `href` reference still resolves against it, and carrying
|
|
1085
|
+
* definitions would deposit duplicate defs on every duplicate.
|
|
1086
|
+
*
|
|
1087
|
+
* Consumers: `commands.duplicate` (⌘D) and the translate orchestrator's
|
|
1088
|
+
* Alt-drag clone session (gridaco/grida#817).
|
|
1089
|
+
*
|
|
1090
|
+
* Verbatim-id policy: authored `id=""` attributes are cloned verbatim,
|
|
1091
|
+
* NEVER rewritten — same stance as `insert_fragment`. A clone of a node
|
|
1092
|
+
* carrying `id="x"` yields a second `id="x"`; reference resolution follows
|
|
1093
|
+
* the host renderer's first-in-document-order rule (a cloned subtree's
|
|
1094
|
+
* internal self-reference resolves to the ORIGINAL), and deduplication is
|
|
1095
|
+
* the explicit Tidy command's job.
|
|
1096
|
+
*/
|
|
1097
|
+
let subtree;
|
|
1098
|
+
(function(_subtree) {
|
|
1099
|
+
function by_document_order(doc) {
|
|
1100
|
+
const index = /* @__PURE__ */ new Map();
|
|
1101
|
+
let i = 0;
|
|
1102
|
+
for (const id of doc.all_nodes()) index.set(id, i++);
|
|
1103
|
+
return (a, b) => (index.get(a) ?? 0) - (index.get(b) ?? 0);
|
|
1104
|
+
}
|
|
1105
|
+
_subtree.by_document_order = by_document_order;
|
|
1106
|
+
function normalize_roots(doc, selection, order) {
|
|
1107
|
+
const live = [...new Set(selection)].filter((id) => doc.is_element(id) && doc.contains(doc.root, id));
|
|
1108
|
+
const roots = doc.prune_nested_nodes(live);
|
|
1109
|
+
if (roots.length > 1) roots.sort(order ?? by_document_order(doc));
|
|
1110
|
+
return roots;
|
|
1111
|
+
}
|
|
1112
|
+
_subtree.normalize_roots = normalize_roots;
|
|
1113
|
+
function clone_plan(doc, selection) {
|
|
1114
|
+
const out = [];
|
|
1115
|
+
for (const origin of normalize_roots(doc, selection)) {
|
|
1116
|
+
const parent = doc.parent_of(origin);
|
|
1117
|
+
if (parent === null) continue;
|
|
1118
|
+
if (doc.tag_of(origin) === "svg") continue;
|
|
1119
|
+
const { roots } = doc.create_fragment(doc.serialize_node(origin));
|
|
1120
|
+
if (roots.length !== 1) throw new Error(`subtree.clone_plan: cloning ${JSON.stringify(origin)} yielded ${roots.length} roots`);
|
|
1121
|
+
out.push({
|
|
1122
|
+
origin,
|
|
1123
|
+
clone: roots[0],
|
|
1124
|
+
parent,
|
|
1125
|
+
before: doc.next_sibling_of(origin)
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
1130
|
+
_subtree.clone_plan = clone_plan;
|
|
1131
|
+
function insert_plan(doc, plan) {
|
|
1132
|
+
for (const p of plan) doc.insert(p.clone, p.parent, p.before);
|
|
1133
|
+
}
|
|
1134
|
+
_subtree.insert_plan = insert_plan;
|
|
1135
|
+
function remove_plan(doc, plan) {
|
|
1136
|
+
for (const p of plan) doc.remove(p.clone);
|
|
1137
|
+
}
|
|
1138
|
+
_subtree.remove_plan = remove_plan;
|
|
1139
|
+
/** Per-member rigidity tolerance for the rigid-translate witness in
|
|
1140
|
+
* {@link repeat_delta} — position drift from the shared delta, and
|
|
1141
|
+
* size drift from the origin. Generous against float noise from
|
|
1142
|
+
* re-encoded path data / `getBBox`, far below anything a user would
|
|
1143
|
+
* call a move or a resize. */
|
|
1144
|
+
const REPEAT_RIGID_EPSILON = .01;
|
|
1145
|
+
function repeat_delta(record, targets, bounds_of) {
|
|
1146
|
+
if (record === null) return null;
|
|
1147
|
+
if (record.origins.length === 0) return null;
|
|
1148
|
+
if (record.origins.length !== record.clones.length) return null;
|
|
1149
|
+
if (!array_shallow_equal(record.clones, targets)) return null;
|
|
1150
|
+
const origin_bounds = collect_bounds(record.origins, bounds_of);
|
|
1151
|
+
if (origin_bounds === null) return null;
|
|
1152
|
+
const clone_bounds = collect_bounds(record.clones, bounds_of);
|
|
1153
|
+
if (clone_bounds === null) return null;
|
|
1154
|
+
const a = _grida_cmath.default.rect.union(origin_bounds);
|
|
1155
|
+
const b = _grida_cmath.default.rect.union(clone_bounds);
|
|
1156
|
+
const dx = b.x - a.x;
|
|
1157
|
+
const dy = b.y - a.y;
|
|
1158
|
+
for (let i = 0; i < origin_bounds.length; i++) {
|
|
1159
|
+
const o = origin_bounds[i];
|
|
1160
|
+
const c = clone_bounds[i];
|
|
1161
|
+
if (Math.abs(c.x - o.x - dx) > REPEAT_RIGID_EPSILON || Math.abs(c.y - o.y - dy) > REPEAT_RIGID_EPSILON || Math.abs(c.width - o.width) > REPEAT_RIGID_EPSILON || Math.abs(c.height - o.height) > REPEAT_RIGID_EPSILON) return null;
|
|
1162
|
+
}
|
|
1163
|
+
if (Math.abs(dx) <= REPEAT_RIGID_EPSILON && Math.abs(dy) <= REPEAT_RIGID_EPSILON) return null;
|
|
1164
|
+
return {
|
|
1165
|
+
x: dx,
|
|
1166
|
+
y: dy
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
_subtree.repeat_delta = repeat_delta;
|
|
1170
|
+
/** All-or-nothing bounds gather for {@link repeat_delta} — one
|
|
1171
|
+
* unmeasurable member and the record can't witness a rigid
|
|
1172
|
+
* translate. */
|
|
1173
|
+
function collect_bounds(ids, bounds_of) {
|
|
1174
|
+
const out = [];
|
|
1175
|
+
for (const id of ids) {
|
|
1176
|
+
const r = bounds_of(id);
|
|
1177
|
+
if (r === null) return null;
|
|
1178
|
+
out.push(r);
|
|
1179
|
+
}
|
|
1180
|
+
return out;
|
|
1181
|
+
}
|
|
1182
|
+
})(subtree || (subtree = {}));
|
|
1040
1183
|
//#endregion
|
|
1041
1184
|
//#region src/core/transform.ts
|
|
1042
1185
|
let transform;
|
|
@@ -1804,6 +1947,8 @@ let translate_pipeline;
|
|
|
1804
1947
|
})(translate_pipeline || (translate_pipeline = {}));
|
|
1805
1948
|
//#endregion
|
|
1806
1949
|
//#region src/core/translate-pipeline/orchestrator.ts
|
|
1950
|
+
const movers = (s) => s.clone?.ids ?? s.ids;
|
|
1951
|
+
const mover_baselines = (s) => s.clone?.baselines ?? s.baselines;
|
|
1807
1952
|
const PROVIDER_ID$2 = "svg-editor";
|
|
1808
1953
|
var TranslateOrchestrator = class {
|
|
1809
1954
|
constructor(deps) {
|
|
@@ -1826,25 +1971,33 @@ var TranslateOrchestrator = class {
|
|
|
1826
1971
|
drive(input, modifiers, opts) {
|
|
1827
1972
|
if (this.active === null) this.active = this.open(input.ids, opts.snap, opts.label ?? "move");
|
|
1828
1973
|
const session = this.active;
|
|
1974
|
+
this.reconcile_clone(session, modifiers);
|
|
1829
1975
|
const stages = opts.stages ?? translate_pipeline.stages.DEFAULT;
|
|
1830
1976
|
const result = this.run_pass(session, input.movement, modifiers, opts.policy, stages);
|
|
1831
1977
|
session.last_movement = input.movement;
|
|
1832
1978
|
session.last_policy = opts.policy;
|
|
1833
1979
|
session.last_stages = stages;
|
|
1834
|
-
this.
|
|
1980
|
+
if (opts.phase === "commit" && session.clone !== null) this.write_commit_composite(session, result.plan);
|
|
1981
|
+
else this.write_preview_delta(session, result.plan);
|
|
1835
1982
|
if (opts.phase === "commit") {
|
|
1983
|
+
const cloned = session.clone;
|
|
1836
1984
|
session.preview.commit();
|
|
1837
1985
|
this.dispose_session();
|
|
1986
|
+
if (cloned !== null) this.deps.on_clone_commit?.({
|
|
1987
|
+
origins: cloned.plan.map((p) => p.origin),
|
|
1988
|
+
clones: cloned.ids
|
|
1989
|
+
});
|
|
1838
1990
|
}
|
|
1839
1991
|
return result;
|
|
1840
1992
|
}
|
|
1841
1993
|
/** Re-run the current preview frame with new modifiers, reusing the
|
|
1842
1994
|
* last-known movement / policy / stages. Used when a modifier key
|
|
1843
|
-
* changes between pointer-move events (Shift down/up mid-drag
|
|
1844
|
-
* No-op when no session is active. */
|
|
1995
|
+
* changes between pointer-move events (Shift down/up mid-drag,
|
|
1996
|
+
* Alt down/up → clone toggle). No-op when no session is active. */
|
|
1845
1997
|
redrive_modifiers(modifiers) {
|
|
1846
1998
|
if (!this.active) return null;
|
|
1847
1999
|
const session = this.active;
|
|
2000
|
+
this.reconcile_clone(session, modifiers);
|
|
1848
2001
|
const result = this.run_pass(session, session.last_movement, modifiers, session.last_policy, session.last_stages);
|
|
1849
2002
|
this.write_preview_delta(session, result.plan);
|
|
1850
2003
|
return result;
|
|
@@ -1852,15 +2005,22 @@ var TranslateOrchestrator = class {
|
|
|
1852
2005
|
/** Cancel an in-flight gesture (Escape, programmatic abort). */
|
|
1853
2006
|
cancel() {
|
|
1854
2007
|
if (!this.active) return;
|
|
1855
|
-
this.active
|
|
2008
|
+
const session = this.active;
|
|
2009
|
+
session.preview.discard();
|
|
2010
|
+
if (session.clone !== null) {
|
|
2011
|
+
subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
|
|
2012
|
+
this.deps.set_selection?.(session.ids);
|
|
2013
|
+
this.deps.emit();
|
|
2014
|
+
}
|
|
1856
2015
|
this.dispose_session();
|
|
1857
2016
|
}
|
|
1858
2017
|
/** Build a plan + context, run the pipeline, stash guides. Pure
|
|
1859
|
-
* computation — does not touch the preview.
|
|
2018
|
+
* computation — does not touch the preview. The context's modifiers
|
|
2019
|
+
* are the pipeline's own vocabulary: stages never see `clone`. */
|
|
1860
2020
|
run_pass(session, movement, modifiers, policy, stages) {
|
|
1861
2021
|
const plan0 = {
|
|
1862
|
-
ids: session
|
|
1863
|
-
baselines: session
|
|
2022
|
+
ids: movers(session),
|
|
2023
|
+
baselines: mover_baselines(session),
|
|
1864
2024
|
delta: {
|
|
1865
2025
|
x: 0,
|
|
1866
2026
|
y: 0
|
|
@@ -1868,7 +2028,7 @@ var TranslateOrchestrator = class {
|
|
|
1868
2028
|
};
|
|
1869
2029
|
const ctx = {
|
|
1870
2030
|
input: {
|
|
1871
|
-
ids:
|
|
2031
|
+
ids: plan0.ids,
|
|
1872
2032
|
movement
|
|
1873
2033
|
},
|
|
1874
2034
|
modifiers,
|
|
@@ -1880,6 +2040,69 @@ var TranslateOrchestrator = class {
|
|
|
1880
2040
|
this._last_guides = result.guides;
|
|
1881
2041
|
return result;
|
|
1882
2042
|
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Clone-modifier state machine (Alt-drag translate-with-clone). Runs
|
|
2045
|
+
* before each pipeline pass so a toggle takes effect on the very frame
|
|
2046
|
+
* it happens — lazy clone on the first frame with the modifier held.
|
|
2047
|
+
*
|
|
2048
|
+
* Both edges and the composite commit below depend on one invariant:
|
|
2049
|
+
* `translate_pipeline.*.revert` writes ABSOLUTE baseline values (incl.
|
|
2050
|
+
* tspan's exact-attr restore), so re-reverting an already-reverted set
|
|
2051
|
+
* is a no-op. A future relative-write baseline kind would break this.
|
|
2052
|
+
*/
|
|
2053
|
+
reconcile_clone(session, modifiers) {
|
|
2054
|
+
const want = modifiers.clone === true;
|
|
2055
|
+
const cloned = session.clone !== null;
|
|
2056
|
+
if (want && !cloned) this.enter_clone(session);
|
|
2057
|
+
else if (!want && cloned) this.exit_clone(session);
|
|
2058
|
+
}
|
|
2059
|
+
/** Enter the cloned state: origins back to rest, verbatim clones
|
|
2060
|
+
* inserted next to them, gesture + selection + snap retargeted to the
|
|
2061
|
+
* clones. The origin set stays untouched for the rest of the cloned
|
|
2062
|
+
* gesture — the CLONE moves, the origin stays (Figma convention). */
|
|
2063
|
+
enter_clone(session) {
|
|
2064
|
+
const doc = this.deps.get_doc();
|
|
2065
|
+
translate_pipeline.revert(doc, {
|
|
2066
|
+
ids: session.ids,
|
|
2067
|
+
baselines: session.baselines,
|
|
2068
|
+
delta: {
|
|
2069
|
+
x: 0,
|
|
2070
|
+
y: 0
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
const plan = subtree.clone_plan(doc, session.ids);
|
|
2074
|
+
if (plan.length === 0) return;
|
|
2075
|
+
subtree.insert_plan(doc, plan);
|
|
2076
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
2077
|
+
for (const p of plan) baselines.set(p.clone, session.baselines.get(p.origin));
|
|
2078
|
+
session.clone = {
|
|
2079
|
+
plan,
|
|
2080
|
+
ids: plan.map((p) => p.clone),
|
|
2081
|
+
baselines
|
|
2082
|
+
};
|
|
2083
|
+
this.retarget(session, session.clone.ids);
|
|
2084
|
+
}
|
|
2085
|
+
/** Exit the cloned state: clones removed, gesture + selection + snap
|
|
2086
|
+
* retargeted back to the origins, which resume following the cursor
|
|
2087
|
+
* on the next pass. Removed clones stay in the document's id map
|
|
2088
|
+
* (standard removed-node policy) and are never serialized; a stale
|
|
2089
|
+
* preview delta auto-reverting onto them later is harmless. Re-press
|
|
2090
|
+
* = fresh enter with new clones. */
|
|
2091
|
+
exit_clone(session) {
|
|
2092
|
+
subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
|
|
2093
|
+
session.clone = null;
|
|
2094
|
+
this.retarget(session, session.ids);
|
|
2095
|
+
}
|
|
2096
|
+
/** Point selection + snap at the new mover set. Selection + emit run
|
|
2097
|
+
* BEFORE the snap reopen: the surface re-renders on notify, so freshly
|
|
2098
|
+
* inserted clones are measurable when the new snap session captures
|
|
2099
|
+
* its neighborhood (where the origins are legitimate snap targets). */
|
|
2100
|
+
retarget(session, ids) {
|
|
2101
|
+
this.deps.set_selection?.(ids);
|
|
2102
|
+
this.deps.emit();
|
|
2103
|
+
session.snap?.dispose();
|
|
2104
|
+
session.snap = session.snap_requested ? this.deps.open_snap(ids) : null;
|
|
2105
|
+
}
|
|
1883
2106
|
open(ids, snap, label) {
|
|
1884
2107
|
const doc = this.deps.get_doc();
|
|
1885
2108
|
const filtered = doc.prune_nested_nodes(ids);
|
|
@@ -1887,10 +2110,12 @@ var TranslateOrchestrator = class {
|
|
|
1887
2110
|
ids: filtered,
|
|
1888
2111
|
baselines: translate_pipeline.intent.capture_baselines(doc, filtered),
|
|
1889
2112
|
snap: snap ? this.deps.open_snap(filtered) : null,
|
|
2113
|
+
snap_requested: snap,
|
|
1890
2114
|
preview: this.deps.open_preview(label),
|
|
1891
2115
|
last_movement: [0, 0],
|
|
1892
2116
|
last_policy: "engine",
|
|
1893
|
-
last_stages: translate_pipeline.stages.DEFAULT
|
|
2117
|
+
last_stages: translate_pipeline.stages.DEFAULT,
|
|
2118
|
+
clone: null
|
|
1894
2119
|
};
|
|
1895
2120
|
}
|
|
1896
2121
|
/** Bind a fresh apply/revert pair (closure over `plan`) into the
|
|
@@ -1912,6 +2137,43 @@ var TranslateOrchestrator = class {
|
|
|
1912
2137
|
}
|
|
1913
2138
|
});
|
|
1914
2139
|
}
|
|
2140
|
+
/** Commit-time delta for a CLONED gesture. Per-frame deltas stay
|
|
2141
|
+
* translate-only; the structural insert happened at the toggle edge,
|
|
2142
|
+
* outside the preview. The one delta that gets committed must own the
|
|
2143
|
+
* whole outcome, so a single undo removes clone + move and a redo
|
|
2144
|
+
* restores both:
|
|
2145
|
+
* apply = insert clones (idempotent reposition when live) +
|
|
2146
|
+
* translate + select clones
|
|
2147
|
+
* revert = un-translate + remove clones + select origins
|
|
2148
|
+
* `preview.set` reverts the previous translate-only delta first
|
|
2149
|
+
* (clones back to rest), then runs this apply — the document passes
|
|
2150
|
+
* through exactly the states the closures describe.
|
|
2151
|
+
* Captures locals only (not the session), so the committed delta
|
|
2152
|
+
* retains the minimum undo/redo needs. */
|
|
2153
|
+
write_commit_composite(session, plan) {
|
|
2154
|
+
const doc = this.deps.get_doc();
|
|
2155
|
+
const emit = this.deps.emit;
|
|
2156
|
+
const project = this.deps.project_delta;
|
|
2157
|
+
const set_selection = this.deps.set_selection;
|
|
2158
|
+
const clone_plan = session.clone.plan;
|
|
2159
|
+
const clone_ids = session.clone.ids;
|
|
2160
|
+
const origin_ids = session.ids;
|
|
2161
|
+
session.preview.set({
|
|
2162
|
+
providerId: PROVIDER_ID$2,
|
|
2163
|
+
apply: () => {
|
|
2164
|
+
subtree.insert_plan(doc, clone_plan);
|
|
2165
|
+
translate_pipeline.apply(doc, plan, project);
|
|
2166
|
+
set_selection?.(clone_ids);
|
|
2167
|
+
emit();
|
|
2168
|
+
},
|
|
2169
|
+
revert: () => {
|
|
2170
|
+
translate_pipeline.revert(doc, plan);
|
|
2171
|
+
subtree.remove_plan(doc, clone_plan);
|
|
2172
|
+
set_selection?.(origin_ids);
|
|
2173
|
+
emit();
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
1915
2177
|
dispose_session() {
|
|
1916
2178
|
if (!this.active) return;
|
|
1917
2179
|
this.active.snap?.dispose();
|
|
@@ -2772,6 +3034,60 @@ function scale_path_d(d, origin, sx, sy) {
|
|
|
2772
3034
|
}
|
|
2773
3035
|
}
|
|
2774
3036
|
/**
|
|
3037
|
+
* `dispatch_resize`'s write surface as data: the attribute names each
|
|
3038
|
+
* handler may write, by tag. Capture-side snapshots (the resize
|
|
3039
|
+
* pipeline's `baseline.raw`) are built from this list so revert can
|
|
3040
|
+
* restore exactly what a handler may touch — when a handler starts
|
|
3041
|
+
* writing a new attribute, extend this table or undo will silently
|
|
3042
|
+
* miss it. Cross-checked against the live handlers by
|
|
3043
|
+
* `__tests__/resize-snapshot-coverage.test.ts`.
|
|
3044
|
+
*/
|
|
3045
|
+
const RESIZE_WRITE_ATTRS = {
|
|
3046
|
+
rect: [
|
|
3047
|
+
"x",
|
|
3048
|
+
"y",
|
|
3049
|
+
"width",
|
|
3050
|
+
"height"
|
|
3051
|
+
],
|
|
3052
|
+
image: [
|
|
3053
|
+
"x",
|
|
3054
|
+
"y",
|
|
3055
|
+
"width",
|
|
3056
|
+
"height"
|
|
3057
|
+
],
|
|
3058
|
+
use: [
|
|
3059
|
+
"x",
|
|
3060
|
+
"y",
|
|
3061
|
+
"width",
|
|
3062
|
+
"height"
|
|
3063
|
+
],
|
|
3064
|
+
circle: [
|
|
3065
|
+
"cx",
|
|
3066
|
+
"cy",
|
|
3067
|
+
"r"
|
|
3068
|
+
],
|
|
3069
|
+
ellipse: [
|
|
3070
|
+
"cx",
|
|
3071
|
+
"cy",
|
|
3072
|
+
"rx",
|
|
3073
|
+
"ry"
|
|
3074
|
+
],
|
|
3075
|
+
line: [
|
|
3076
|
+
"x1",
|
|
3077
|
+
"y1",
|
|
3078
|
+
"x2",
|
|
3079
|
+
"y2"
|
|
3080
|
+
],
|
|
3081
|
+
polyline: ["points"],
|
|
3082
|
+
polygon: ["points"],
|
|
3083
|
+
path: ["d"],
|
|
3084
|
+
text: [
|
|
3085
|
+
"x",
|
|
3086
|
+
"y",
|
|
3087
|
+
"font-size"
|
|
3088
|
+
]
|
|
3089
|
+
};
|
|
3090
|
+
/**
|
|
2775
3091
|
* VertexChain × resize — vertex transport in local space.
|
|
2776
3092
|
* Line carries its own (x1, y1, x2, y2); polyline / polygon share `points`.
|
|
2777
3093
|
* Result type is preserved.
|
|
@@ -3078,6 +3394,15 @@ let resize_pipeline;
|
|
|
3078
3394
|
function num(doc, id, name, fallback = 0) {
|
|
3079
3395
|
return _grida_svg_parse.svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
3080
3396
|
}
|
|
3397
|
+
/** Attribute names a resize gesture may write for `tag`: the
|
|
3398
|
+
* handler write surface (`RESIZE_WRITE_ATTRS`, owned next to the
|
|
3399
|
+
* handlers) plus `transform` — the pipeline's own write
|
|
3400
|
+
* (commit-phase `renormalize_rotate_pivot`). Drives the
|
|
3401
|
+
* `baseline.raw` snapshot that `restore` writes back. */
|
|
3402
|
+
function writable_attrs(tag) {
|
|
3403
|
+
const handler_writes = RESIZE_WRITE_ATTRS[tag];
|
|
3404
|
+
return handler_writes ? [...handler_writes, "transform"] : [];
|
|
3405
|
+
}
|
|
3081
3406
|
function is_resizable(tag) {
|
|
3082
3407
|
switch (tag) {
|
|
3083
3408
|
case "rect":
|
|
@@ -3179,12 +3504,21 @@ let resize_pipeline;
|
|
|
3179
3504
|
break;
|
|
3180
3505
|
default: attrs = { kind: "unsupported" };
|
|
3181
3506
|
}
|
|
3507
|
+
const raw = writable_attrs(tag).map((name) => ({
|
|
3508
|
+
name,
|
|
3509
|
+
value: doc.get_attr(id, name)
|
|
3510
|
+
}));
|
|
3182
3511
|
return {
|
|
3183
3512
|
bbox,
|
|
3184
|
-
attrs
|
|
3513
|
+
attrs,
|
|
3514
|
+
raw
|
|
3185
3515
|
};
|
|
3186
3516
|
}
|
|
3187
3517
|
_intent.capture_baseline = capture_baseline;
|
|
3518
|
+
function restore(doc, id, baseline) {
|
|
3519
|
+
for (const a of baseline.raw) doc.set_attr(id, a.name, a.value);
|
|
3520
|
+
}
|
|
3521
|
+
_intent.restore = restore;
|
|
3188
3522
|
function compute_factors(baseline, dir, dx, dy, shift) {
|
|
3189
3523
|
const b = baseline.bbox;
|
|
3190
3524
|
let anchorX = 0;
|
|
@@ -3587,12 +3921,11 @@ let resize_pipeline;
|
|
|
3587
3921
|
}
|
|
3588
3922
|
_resize_pipeline.apply = apply;
|
|
3589
3923
|
function revert(doc, plan) {
|
|
3590
|
-
const f = intent.compute_factors(plan.baseline, plan.direction, 0, 0, false);
|
|
3591
3924
|
const members = plan.members ?? [{
|
|
3592
3925
|
id: plan.id,
|
|
3593
3926
|
baseline: plan.baseline
|
|
3594
3927
|
}];
|
|
3595
|
-
for (const m of members) intent.
|
|
3928
|
+
for (const m of members) intent.restore(doc, m.id, m.baseline);
|
|
3596
3929
|
}
|
|
3597
3930
|
_resize_pipeline.revert = revert;
|
|
3598
3931
|
function synthesize_group_baseline(union) {
|
|
@@ -3609,7 +3942,8 @@ let resize_pipeline;
|
|
|
3609
3942
|
y: union.y,
|
|
3610
3943
|
w: union.width,
|
|
3611
3944
|
h: union.height
|
|
3612
|
-
}
|
|
3945
|
+
},
|
|
3946
|
+
raw: []
|
|
3613
3947
|
};
|
|
3614
3948
|
}
|
|
3615
3949
|
_resize_pipeline.synthesize_group_baseline = synthesize_group_baseline;
|
|
@@ -4915,6 +5249,12 @@ Object.defineProperty(exports, "TranslateOrchestrator", {
|
|
|
4915
5249
|
return TranslateOrchestrator;
|
|
4916
5250
|
}
|
|
4917
5251
|
});
|
|
5252
|
+
Object.defineProperty(exports, "WELL_KNOWN_NS_PREFIXES", {
|
|
5253
|
+
enumerable: true,
|
|
5254
|
+
get: function() {
|
|
5255
|
+
return WELL_KNOWN_NS_PREFIXES;
|
|
5256
|
+
}
|
|
5257
|
+
});
|
|
4918
5258
|
Object.defineProperty(exports, "__exportAll", {
|
|
4919
5259
|
enumerable: true,
|
|
4920
5260
|
get: function() {
|
|
@@ -4975,6 +5315,12 @@ Object.defineProperty(exports, "rotate_pipeline", {
|
|
|
4975
5315
|
return rotate_pipeline;
|
|
4976
5316
|
}
|
|
4977
5317
|
});
|
|
5318
|
+
Object.defineProperty(exports, "subtree", {
|
|
5319
|
+
enumerable: true,
|
|
5320
|
+
get: function() {
|
|
5321
|
+
return subtree;
|
|
5322
|
+
}
|
|
5323
|
+
});
|
|
4978
5324
|
Object.defineProperty(exports, "transform", {
|
|
4979
5325
|
enumerable: true,
|
|
4980
5326
|
get: function() {
|