@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
|
@@ -121,10 +121,20 @@ const GEOMETRY_ATTRS = new Set([
|
|
|
121
121
|
]);
|
|
122
122
|
/** `transform:` CSS property at the start of a declaration list or after `;`. */
|
|
123
123
|
const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
|
|
124
|
+
/** Trivia shape for editor-synthesized attribute tokens — parser-authored
|
|
125
|
+
* tokens carry their source trivia verbatim; tokens the editor appends get
|
|
126
|
+
* this one canonical shape (` name="value"`). */
|
|
127
|
+
const SYNTH_ATTR_TRIVIA = {
|
|
128
|
+
pre: " ",
|
|
129
|
+
eq_trivia: "",
|
|
130
|
+
eq_trailing: "",
|
|
131
|
+
quote: "\""
|
|
132
|
+
};
|
|
124
133
|
var SvgDocument = class SvgDocument {
|
|
125
134
|
constructor(svg) {
|
|
126
135
|
this.listeners = /* @__PURE__ */ new Set();
|
|
127
136
|
this._structure_version = 0;
|
|
137
|
+
this._revision = 0;
|
|
128
138
|
this._geometry_version = 0;
|
|
129
139
|
if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
|
|
130
140
|
this.source = svg;
|
|
@@ -172,6 +182,20 @@ var SvgDocument = class SvgDocument {
|
|
|
172
182
|
get structure_version() {
|
|
173
183
|
return this._structure_version;
|
|
174
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Total mutation counter — advances on EVERY listener-visible mutation
|
|
187
|
+
* (attribute, style, text, topology, load/reset), unlike the selective
|
|
188
|
+
* `structure_version` / `geometry_version` channels. The single
|
|
189
|
+
* edit-version source: anything derived from this document — the
|
|
190
|
+
* editor's `content_version` / `dirty`, memoized reads, a rendered
|
|
191
|
+
* projection — answers "am I current?" by comparing values, with no
|
|
192
|
+
* event-ordering dependence. Advances BEFORE listeners fire, so a
|
|
193
|
+
* read issued from inside a change listener already sees the new
|
|
194
|
+
* value.
|
|
195
|
+
*/
|
|
196
|
+
get revision() {
|
|
197
|
+
return this._revision;
|
|
198
|
+
}
|
|
175
199
|
/** See `_geometry_version` for what this counter signals. */
|
|
176
200
|
get geometry_version() {
|
|
177
201
|
return this._geometry_version;
|
|
@@ -188,15 +212,15 @@ var SvgDocument = class SvgDocument {
|
|
|
188
212
|
* settled glyph metrics. See ../../docs/geometry.md §Limitations.
|
|
189
213
|
*
|
|
190
214
|
* Deliberately does NOT call `emit()`: this is not a document edit, so
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* `
|
|
194
|
-
* out the geometry listeners itself.
|
|
215
|
+
* `revision` must not advance — no dirty flag, no undo, no render
|
|
216
|
+
* flush. The editor's `_internal.bump_geometry` advances
|
|
217
|
+
* `geometry_version` here and fans out the geometry listeners itself.
|
|
195
218
|
*/
|
|
196
219
|
bump_geometry() {
|
|
197
220
|
this._geometry_version++;
|
|
198
221
|
}
|
|
199
222
|
emit() {
|
|
223
|
+
this._revision++;
|
|
200
224
|
for (const fn of this.listeners) fn();
|
|
201
225
|
}
|
|
202
226
|
/** Notify subscribers — for callers that mutate directly via setAttr/etc. */
|
|
@@ -347,9 +371,7 @@ var SvgDocument = class SvgDocument {
|
|
|
347
371
|
local: name,
|
|
348
372
|
ns,
|
|
349
373
|
value,
|
|
350
|
-
|
|
351
|
-
eq_trivia: "",
|
|
352
|
-
quote: "\""
|
|
374
|
+
...SYNTH_ATTR_TRIVIA
|
|
353
375
|
});
|
|
354
376
|
if (structural) this._structure_version++;
|
|
355
377
|
if (geometry) this._geometry_version++;
|
|
@@ -605,9 +627,7 @@ var SvgDocument = class SvgDocument {
|
|
|
605
627
|
local: "d",
|
|
606
628
|
ns: null,
|
|
607
629
|
value: d,
|
|
608
|
-
|
|
609
|
-
eq_trivia: "",
|
|
610
|
-
quote: "\""
|
|
630
|
+
...SYNTH_ATTR_TRIVIA
|
|
611
631
|
});
|
|
612
632
|
let added_fill_none = false;
|
|
613
633
|
if (prev_local === "line" && this.get_attr(id, "fill") === null && this.get_style(id, "fill") === null) {
|
|
@@ -617,9 +637,7 @@ var SvgDocument = class SvgDocument {
|
|
|
617
637
|
local: "fill",
|
|
618
638
|
ns: null,
|
|
619
639
|
value: "none",
|
|
620
|
-
|
|
621
|
-
eq_trivia: "",
|
|
622
|
-
quote: "\""
|
|
640
|
+
...SYNTH_ATTR_TRIVIA
|
|
623
641
|
});
|
|
624
642
|
added_fill_none = true;
|
|
625
643
|
}
|
|
@@ -986,7 +1004,7 @@ var SvgDocument = class SvgDocument {
|
|
|
986
1004
|
}
|
|
987
1005
|
}
|
|
988
1006
|
emit_attr(a) {
|
|
989
|
-
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.quote}${encode_attr_value(a.value, a.quote)}${a.quote}`;
|
|
1007
|
+
return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.eq_trailing}${a.quote}${encode_attr_value(a.value, a.quote)}${a.quote}`;
|
|
990
1008
|
}
|
|
991
1009
|
};
|
|
992
1010
|
function parse_inline_style(s) {
|
|
@@ -1004,6 +1022,131 @@ function parse_inline_style(s) {
|
|
|
1004
1022
|
}
|
|
1005
1023
|
return out;
|
|
1006
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Namespace prefixes resolvable without a source declaration. ONE table,
|
|
1027
|
+
* shared by the paste-side hoist (`insert_fragment`'s xmlns plan) and the
|
|
1028
|
+
* copy-side shell repair (clipboard payload extraction) — the two sides
|
|
1029
|
+
* form a round-trip and must agree: a prefix only one side knows would
|
|
1030
|
+
* produce payloads the other can't honor.
|
|
1031
|
+
*/
|
|
1032
|
+
const WELL_KNOWN_NS_PREFIXES = new Map([["xlink", XLINK_NS$1]]);
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/core/subtree.ts
|
|
1035
|
+
/**
|
|
1036
|
+
* The selection → subtree algebra the two extraction operations share,
|
|
1037
|
+
* plus the clone-specific member (`clone_plan`).
|
|
1038
|
+
*
|
|
1039
|
+
* The clipboard FRD
|
|
1040
|
+
* ([docs/wg/feat-svg-editor/clipboard.md](../../../../docs/wg/feat-svg-editor/clipboard.md)
|
|
1041
|
+
* §Two extraction operations) names two operations over a normalized
|
|
1042
|
+
* selection: **payload extraction** (copy — `core/clipboard.ts`) and
|
|
1043
|
+
* **subtree clone** (duplicate / clone-drag — this module; design note:
|
|
1044
|
+
* [docs/wg/feat-svg-editor/subtree-clone.md](../../../../docs/wg/feat-svg-editor/subtree-clone.md)).
|
|
1045
|
+
* They share exactly two things — selection normalization
|
|
1046
|
+
* ({@link subtree.normalize_roots}) and verbatim subtree serialization
|
|
1047
|
+
* (`SvgDocument.serialize_node`) — and nothing else.
|
|
1048
|
+
*
|
|
1049
|
+
* Unlike copy's payload, a clone carries **no reference closure and no
|
|
1050
|
+
* namespace shell**: the destination is the source document, every
|
|
1051
|
+
* `url(#…)` / `href` reference still resolves against it, and carrying
|
|
1052
|
+
* definitions would deposit duplicate defs on every duplicate.
|
|
1053
|
+
*
|
|
1054
|
+
* Consumers: `commands.duplicate` (⌘D) and the translate orchestrator's
|
|
1055
|
+
* Alt-drag clone session (gridaco/grida#817).
|
|
1056
|
+
*
|
|
1057
|
+
* Verbatim-id policy: authored `id=""` attributes are cloned verbatim,
|
|
1058
|
+
* NEVER rewritten — same stance as `insert_fragment`. A clone of a node
|
|
1059
|
+
* carrying `id="x"` yields a second `id="x"`; reference resolution follows
|
|
1060
|
+
* the host renderer's first-in-document-order rule (a cloned subtree's
|
|
1061
|
+
* internal self-reference resolves to the ORIGINAL), and deduplication is
|
|
1062
|
+
* the explicit Tidy command's job.
|
|
1063
|
+
*/
|
|
1064
|
+
let subtree;
|
|
1065
|
+
(function(_subtree) {
|
|
1066
|
+
function by_document_order(doc) {
|
|
1067
|
+
const index = /* @__PURE__ */ new Map();
|
|
1068
|
+
let i = 0;
|
|
1069
|
+
for (const id of doc.all_nodes()) index.set(id, i++);
|
|
1070
|
+
return (a, b) => (index.get(a) ?? 0) - (index.get(b) ?? 0);
|
|
1071
|
+
}
|
|
1072
|
+
_subtree.by_document_order = by_document_order;
|
|
1073
|
+
function normalize_roots(doc, selection, order) {
|
|
1074
|
+
const live = [...new Set(selection)].filter((id) => doc.is_element(id) && doc.contains(doc.root, id));
|
|
1075
|
+
const roots = doc.prune_nested_nodes(live);
|
|
1076
|
+
if (roots.length > 1) roots.sort(order ?? by_document_order(doc));
|
|
1077
|
+
return roots;
|
|
1078
|
+
}
|
|
1079
|
+
_subtree.normalize_roots = normalize_roots;
|
|
1080
|
+
function clone_plan(doc, selection) {
|
|
1081
|
+
const out = [];
|
|
1082
|
+
for (const origin of normalize_roots(doc, selection)) {
|
|
1083
|
+
const parent = doc.parent_of(origin);
|
|
1084
|
+
if (parent === null) continue;
|
|
1085
|
+
if (doc.tag_of(origin) === "svg") continue;
|
|
1086
|
+
const { roots } = doc.create_fragment(doc.serialize_node(origin));
|
|
1087
|
+
if (roots.length !== 1) throw new Error(`subtree.clone_plan: cloning ${JSON.stringify(origin)} yielded ${roots.length} roots`);
|
|
1088
|
+
out.push({
|
|
1089
|
+
origin,
|
|
1090
|
+
clone: roots[0],
|
|
1091
|
+
parent,
|
|
1092
|
+
before: doc.next_sibling_of(origin)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
1097
|
+
_subtree.clone_plan = clone_plan;
|
|
1098
|
+
function insert_plan(doc, plan) {
|
|
1099
|
+
for (const p of plan) doc.insert(p.clone, p.parent, p.before);
|
|
1100
|
+
}
|
|
1101
|
+
_subtree.insert_plan = insert_plan;
|
|
1102
|
+
function remove_plan(doc, plan) {
|
|
1103
|
+
for (const p of plan) doc.remove(p.clone);
|
|
1104
|
+
}
|
|
1105
|
+
_subtree.remove_plan = remove_plan;
|
|
1106
|
+
/** Per-member rigidity tolerance for the rigid-translate witness in
|
|
1107
|
+
* {@link repeat_delta} — position drift from the shared delta, and
|
|
1108
|
+
* size drift from the origin. Generous against float noise from
|
|
1109
|
+
* re-encoded path data / `getBBox`, far below anything a user would
|
|
1110
|
+
* call a move or a resize. */
|
|
1111
|
+
const REPEAT_RIGID_EPSILON = .01;
|
|
1112
|
+
function repeat_delta(record, targets, bounds_of) {
|
|
1113
|
+
if (record === null) return null;
|
|
1114
|
+
if (record.origins.length === 0) return null;
|
|
1115
|
+
if (record.origins.length !== record.clones.length) return null;
|
|
1116
|
+
if (!array_shallow_equal(record.clones, targets)) return null;
|
|
1117
|
+
const origin_bounds = collect_bounds(record.origins, bounds_of);
|
|
1118
|
+
if (origin_bounds === null) return null;
|
|
1119
|
+
const clone_bounds = collect_bounds(record.clones, bounds_of);
|
|
1120
|
+
if (clone_bounds === null) return null;
|
|
1121
|
+
const a = cmath.rect.union(origin_bounds);
|
|
1122
|
+
const b = cmath.rect.union(clone_bounds);
|
|
1123
|
+
const dx = b.x - a.x;
|
|
1124
|
+
const dy = b.y - a.y;
|
|
1125
|
+
for (let i = 0; i < origin_bounds.length; i++) {
|
|
1126
|
+
const o = origin_bounds[i];
|
|
1127
|
+
const c = clone_bounds[i];
|
|
1128
|
+
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;
|
|
1129
|
+
}
|
|
1130
|
+
if (Math.abs(dx) <= REPEAT_RIGID_EPSILON && Math.abs(dy) <= REPEAT_RIGID_EPSILON) return null;
|
|
1131
|
+
return {
|
|
1132
|
+
x: dx,
|
|
1133
|
+
y: dy
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
_subtree.repeat_delta = repeat_delta;
|
|
1137
|
+
/** All-or-nothing bounds gather for {@link repeat_delta} — one
|
|
1138
|
+
* unmeasurable member and the record can't witness a rigid
|
|
1139
|
+
* translate. */
|
|
1140
|
+
function collect_bounds(ids, bounds_of) {
|
|
1141
|
+
const out = [];
|
|
1142
|
+
for (const id of ids) {
|
|
1143
|
+
const r = bounds_of(id);
|
|
1144
|
+
if (r === null) return null;
|
|
1145
|
+
out.push(r);
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
})(subtree || (subtree = {}));
|
|
1007
1150
|
//#endregion
|
|
1008
1151
|
//#region src/core/transform.ts
|
|
1009
1152
|
let transform;
|
|
@@ -1771,6 +1914,8 @@ let translate_pipeline;
|
|
|
1771
1914
|
})(translate_pipeline || (translate_pipeline = {}));
|
|
1772
1915
|
//#endregion
|
|
1773
1916
|
//#region src/core/translate-pipeline/orchestrator.ts
|
|
1917
|
+
const movers = (s) => s.clone?.ids ?? s.ids;
|
|
1918
|
+
const mover_baselines = (s) => s.clone?.baselines ?? s.baselines;
|
|
1774
1919
|
const PROVIDER_ID$2 = "svg-editor";
|
|
1775
1920
|
var TranslateOrchestrator = class {
|
|
1776
1921
|
constructor(deps) {
|
|
@@ -1793,25 +1938,33 @@ var TranslateOrchestrator = class {
|
|
|
1793
1938
|
drive(input, modifiers, opts) {
|
|
1794
1939
|
if (this.active === null) this.active = this.open(input.ids, opts.snap, opts.label ?? "move");
|
|
1795
1940
|
const session = this.active;
|
|
1941
|
+
this.reconcile_clone(session, modifiers);
|
|
1796
1942
|
const stages = opts.stages ?? translate_pipeline.stages.DEFAULT;
|
|
1797
1943
|
const result = this.run_pass(session, input.movement, modifiers, opts.policy, stages);
|
|
1798
1944
|
session.last_movement = input.movement;
|
|
1799
1945
|
session.last_policy = opts.policy;
|
|
1800
1946
|
session.last_stages = stages;
|
|
1801
|
-
this.
|
|
1947
|
+
if (opts.phase === "commit" && session.clone !== null) this.write_commit_composite(session, result.plan);
|
|
1948
|
+
else this.write_preview_delta(session, result.plan);
|
|
1802
1949
|
if (opts.phase === "commit") {
|
|
1950
|
+
const cloned = session.clone;
|
|
1803
1951
|
session.preview.commit();
|
|
1804
1952
|
this.dispose_session();
|
|
1953
|
+
if (cloned !== null) this.deps.on_clone_commit?.({
|
|
1954
|
+
origins: cloned.plan.map((p) => p.origin),
|
|
1955
|
+
clones: cloned.ids
|
|
1956
|
+
});
|
|
1805
1957
|
}
|
|
1806
1958
|
return result;
|
|
1807
1959
|
}
|
|
1808
1960
|
/** Re-run the current preview frame with new modifiers, reusing the
|
|
1809
1961
|
* last-known movement / policy / stages. Used when a modifier key
|
|
1810
|
-
* changes between pointer-move events (Shift down/up mid-drag
|
|
1811
|
-
* No-op when no session is active. */
|
|
1962
|
+
* changes between pointer-move events (Shift down/up mid-drag,
|
|
1963
|
+
* Alt down/up → clone toggle). No-op when no session is active. */
|
|
1812
1964
|
redrive_modifiers(modifiers) {
|
|
1813
1965
|
if (!this.active) return null;
|
|
1814
1966
|
const session = this.active;
|
|
1967
|
+
this.reconcile_clone(session, modifiers);
|
|
1815
1968
|
const result = this.run_pass(session, session.last_movement, modifiers, session.last_policy, session.last_stages);
|
|
1816
1969
|
this.write_preview_delta(session, result.plan);
|
|
1817
1970
|
return result;
|
|
@@ -1819,15 +1972,22 @@ var TranslateOrchestrator = class {
|
|
|
1819
1972
|
/** Cancel an in-flight gesture (Escape, programmatic abort). */
|
|
1820
1973
|
cancel() {
|
|
1821
1974
|
if (!this.active) return;
|
|
1822
|
-
this.active
|
|
1975
|
+
const session = this.active;
|
|
1976
|
+
session.preview.discard();
|
|
1977
|
+
if (session.clone !== null) {
|
|
1978
|
+
subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
|
|
1979
|
+
this.deps.set_selection?.(session.ids);
|
|
1980
|
+
this.deps.emit();
|
|
1981
|
+
}
|
|
1823
1982
|
this.dispose_session();
|
|
1824
1983
|
}
|
|
1825
1984
|
/** Build a plan + context, run the pipeline, stash guides. Pure
|
|
1826
|
-
* computation — does not touch the preview.
|
|
1985
|
+
* computation — does not touch the preview. The context's modifiers
|
|
1986
|
+
* are the pipeline's own vocabulary: stages never see `clone`. */
|
|
1827
1987
|
run_pass(session, movement, modifiers, policy, stages) {
|
|
1828
1988
|
const plan0 = {
|
|
1829
|
-
ids: session
|
|
1830
|
-
baselines: session
|
|
1989
|
+
ids: movers(session),
|
|
1990
|
+
baselines: mover_baselines(session),
|
|
1831
1991
|
delta: {
|
|
1832
1992
|
x: 0,
|
|
1833
1993
|
y: 0
|
|
@@ -1835,7 +1995,7 @@ var TranslateOrchestrator = class {
|
|
|
1835
1995
|
};
|
|
1836
1996
|
const ctx = {
|
|
1837
1997
|
input: {
|
|
1838
|
-
ids:
|
|
1998
|
+
ids: plan0.ids,
|
|
1839
1999
|
movement
|
|
1840
2000
|
},
|
|
1841
2001
|
modifiers,
|
|
@@ -1847,6 +2007,69 @@ var TranslateOrchestrator = class {
|
|
|
1847
2007
|
this._last_guides = result.guides;
|
|
1848
2008
|
return result;
|
|
1849
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Clone-modifier state machine (Alt-drag translate-with-clone). Runs
|
|
2012
|
+
* before each pipeline pass so a toggle takes effect on the very frame
|
|
2013
|
+
* it happens — lazy clone on the first frame with the modifier held.
|
|
2014
|
+
*
|
|
2015
|
+
* Both edges and the composite commit below depend on one invariant:
|
|
2016
|
+
* `translate_pipeline.*.revert` writes ABSOLUTE baseline values (incl.
|
|
2017
|
+
* tspan's exact-attr restore), so re-reverting an already-reverted set
|
|
2018
|
+
* is a no-op. A future relative-write baseline kind would break this.
|
|
2019
|
+
*/
|
|
2020
|
+
reconcile_clone(session, modifiers) {
|
|
2021
|
+
const want = modifiers.clone === true;
|
|
2022
|
+
const cloned = session.clone !== null;
|
|
2023
|
+
if (want && !cloned) this.enter_clone(session);
|
|
2024
|
+
else if (!want && cloned) this.exit_clone(session);
|
|
2025
|
+
}
|
|
2026
|
+
/** Enter the cloned state: origins back to rest, verbatim clones
|
|
2027
|
+
* inserted next to them, gesture + selection + snap retargeted to the
|
|
2028
|
+
* clones. The origin set stays untouched for the rest of the cloned
|
|
2029
|
+
* gesture — the CLONE moves, the origin stays (Figma convention). */
|
|
2030
|
+
enter_clone(session) {
|
|
2031
|
+
const doc = this.deps.get_doc();
|
|
2032
|
+
translate_pipeline.revert(doc, {
|
|
2033
|
+
ids: session.ids,
|
|
2034
|
+
baselines: session.baselines,
|
|
2035
|
+
delta: {
|
|
2036
|
+
x: 0,
|
|
2037
|
+
y: 0
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
const plan = subtree.clone_plan(doc, session.ids);
|
|
2041
|
+
if (plan.length === 0) return;
|
|
2042
|
+
subtree.insert_plan(doc, plan);
|
|
2043
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
2044
|
+
for (const p of plan) baselines.set(p.clone, session.baselines.get(p.origin));
|
|
2045
|
+
session.clone = {
|
|
2046
|
+
plan,
|
|
2047
|
+
ids: plan.map((p) => p.clone),
|
|
2048
|
+
baselines
|
|
2049
|
+
};
|
|
2050
|
+
this.retarget(session, session.clone.ids);
|
|
2051
|
+
}
|
|
2052
|
+
/** Exit the cloned state: clones removed, gesture + selection + snap
|
|
2053
|
+
* retargeted back to the origins, which resume following the cursor
|
|
2054
|
+
* on the next pass. Removed clones stay in the document's id map
|
|
2055
|
+
* (standard removed-node policy) and are never serialized; a stale
|
|
2056
|
+
* preview delta auto-reverting onto them later is harmless. Re-press
|
|
2057
|
+
* = fresh enter with new clones. */
|
|
2058
|
+
exit_clone(session) {
|
|
2059
|
+
subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
|
|
2060
|
+
session.clone = null;
|
|
2061
|
+
this.retarget(session, session.ids);
|
|
2062
|
+
}
|
|
2063
|
+
/** Point selection + snap at the new mover set. Selection + emit run
|
|
2064
|
+
* BEFORE the snap reopen: the surface re-renders on notify, so freshly
|
|
2065
|
+
* inserted clones are measurable when the new snap session captures
|
|
2066
|
+
* its neighborhood (where the origins are legitimate snap targets). */
|
|
2067
|
+
retarget(session, ids) {
|
|
2068
|
+
this.deps.set_selection?.(ids);
|
|
2069
|
+
this.deps.emit();
|
|
2070
|
+
session.snap?.dispose();
|
|
2071
|
+
session.snap = session.snap_requested ? this.deps.open_snap(ids) : null;
|
|
2072
|
+
}
|
|
1850
2073
|
open(ids, snap, label) {
|
|
1851
2074
|
const doc = this.deps.get_doc();
|
|
1852
2075
|
const filtered = doc.prune_nested_nodes(ids);
|
|
@@ -1854,10 +2077,12 @@ var TranslateOrchestrator = class {
|
|
|
1854
2077
|
ids: filtered,
|
|
1855
2078
|
baselines: translate_pipeline.intent.capture_baselines(doc, filtered),
|
|
1856
2079
|
snap: snap ? this.deps.open_snap(filtered) : null,
|
|
2080
|
+
snap_requested: snap,
|
|
1857
2081
|
preview: this.deps.open_preview(label),
|
|
1858
2082
|
last_movement: [0, 0],
|
|
1859
2083
|
last_policy: "engine",
|
|
1860
|
-
last_stages: translate_pipeline.stages.DEFAULT
|
|
2084
|
+
last_stages: translate_pipeline.stages.DEFAULT,
|
|
2085
|
+
clone: null
|
|
1861
2086
|
};
|
|
1862
2087
|
}
|
|
1863
2088
|
/** Bind a fresh apply/revert pair (closure over `plan`) into the
|
|
@@ -1879,6 +2104,43 @@ var TranslateOrchestrator = class {
|
|
|
1879
2104
|
}
|
|
1880
2105
|
});
|
|
1881
2106
|
}
|
|
2107
|
+
/** Commit-time delta for a CLONED gesture. Per-frame deltas stay
|
|
2108
|
+
* translate-only; the structural insert happened at the toggle edge,
|
|
2109
|
+
* outside the preview. The one delta that gets committed must own the
|
|
2110
|
+
* whole outcome, so a single undo removes clone + move and a redo
|
|
2111
|
+
* restores both:
|
|
2112
|
+
* apply = insert clones (idempotent reposition when live) +
|
|
2113
|
+
* translate + select clones
|
|
2114
|
+
* revert = un-translate + remove clones + select origins
|
|
2115
|
+
* `preview.set` reverts the previous translate-only delta first
|
|
2116
|
+
* (clones back to rest), then runs this apply — the document passes
|
|
2117
|
+
* through exactly the states the closures describe.
|
|
2118
|
+
* Captures locals only (not the session), so the committed delta
|
|
2119
|
+
* retains the minimum undo/redo needs. */
|
|
2120
|
+
write_commit_composite(session, plan) {
|
|
2121
|
+
const doc = this.deps.get_doc();
|
|
2122
|
+
const emit = this.deps.emit;
|
|
2123
|
+
const project = this.deps.project_delta;
|
|
2124
|
+
const set_selection = this.deps.set_selection;
|
|
2125
|
+
const clone_plan = session.clone.plan;
|
|
2126
|
+
const clone_ids = session.clone.ids;
|
|
2127
|
+
const origin_ids = session.ids;
|
|
2128
|
+
session.preview.set({
|
|
2129
|
+
providerId: PROVIDER_ID$2,
|
|
2130
|
+
apply: () => {
|
|
2131
|
+
subtree.insert_plan(doc, clone_plan);
|
|
2132
|
+
translate_pipeline.apply(doc, plan, project);
|
|
2133
|
+
set_selection?.(clone_ids);
|
|
2134
|
+
emit();
|
|
2135
|
+
},
|
|
2136
|
+
revert: () => {
|
|
2137
|
+
translate_pipeline.revert(doc, plan);
|
|
2138
|
+
subtree.remove_plan(doc, clone_plan);
|
|
2139
|
+
set_selection?.(origin_ids);
|
|
2140
|
+
emit();
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1882
2144
|
dispose_session() {
|
|
1883
2145
|
if (!this.active) return;
|
|
1884
2146
|
this.active.snap?.dispose();
|
|
@@ -2739,6 +3001,60 @@ function scale_path_d(d, origin, sx, sy) {
|
|
|
2739
3001
|
}
|
|
2740
3002
|
}
|
|
2741
3003
|
/**
|
|
3004
|
+
* `dispatch_resize`'s write surface as data: the attribute names each
|
|
3005
|
+
* handler may write, by tag. Capture-side snapshots (the resize
|
|
3006
|
+
* pipeline's `baseline.raw`) are built from this list so revert can
|
|
3007
|
+
* restore exactly what a handler may touch — when a handler starts
|
|
3008
|
+
* writing a new attribute, extend this table or undo will silently
|
|
3009
|
+
* miss it. Cross-checked against the live handlers by
|
|
3010
|
+
* `__tests__/resize-snapshot-coverage.test.ts`.
|
|
3011
|
+
*/
|
|
3012
|
+
const RESIZE_WRITE_ATTRS = {
|
|
3013
|
+
rect: [
|
|
3014
|
+
"x",
|
|
3015
|
+
"y",
|
|
3016
|
+
"width",
|
|
3017
|
+
"height"
|
|
3018
|
+
],
|
|
3019
|
+
image: [
|
|
3020
|
+
"x",
|
|
3021
|
+
"y",
|
|
3022
|
+
"width",
|
|
3023
|
+
"height"
|
|
3024
|
+
],
|
|
3025
|
+
use: [
|
|
3026
|
+
"x",
|
|
3027
|
+
"y",
|
|
3028
|
+
"width",
|
|
3029
|
+
"height"
|
|
3030
|
+
],
|
|
3031
|
+
circle: [
|
|
3032
|
+
"cx",
|
|
3033
|
+
"cy",
|
|
3034
|
+
"r"
|
|
3035
|
+
],
|
|
3036
|
+
ellipse: [
|
|
3037
|
+
"cx",
|
|
3038
|
+
"cy",
|
|
3039
|
+
"rx",
|
|
3040
|
+
"ry"
|
|
3041
|
+
],
|
|
3042
|
+
line: [
|
|
3043
|
+
"x1",
|
|
3044
|
+
"y1",
|
|
3045
|
+
"x2",
|
|
3046
|
+
"y2"
|
|
3047
|
+
],
|
|
3048
|
+
polyline: ["points"],
|
|
3049
|
+
polygon: ["points"],
|
|
3050
|
+
path: ["d"],
|
|
3051
|
+
text: [
|
|
3052
|
+
"x",
|
|
3053
|
+
"y",
|
|
3054
|
+
"font-size"
|
|
3055
|
+
]
|
|
3056
|
+
};
|
|
3057
|
+
/**
|
|
2742
3058
|
* VertexChain × resize — vertex transport in local space.
|
|
2743
3059
|
* Line carries its own (x1, y1, x2, y2); polyline / polygon share `points`.
|
|
2744
3060
|
* Result type is preserved.
|
|
@@ -3045,6 +3361,15 @@ let resize_pipeline;
|
|
|
3045
3361
|
function num(doc, id, name, fallback = 0) {
|
|
3046
3362
|
return svg_parse.parse_number(doc.get_attr(id, name), fallback);
|
|
3047
3363
|
}
|
|
3364
|
+
/** Attribute names a resize gesture may write for `tag`: the
|
|
3365
|
+
* handler write surface (`RESIZE_WRITE_ATTRS`, owned next to the
|
|
3366
|
+
* handlers) plus `transform` — the pipeline's own write
|
|
3367
|
+
* (commit-phase `renormalize_rotate_pivot`). Drives the
|
|
3368
|
+
* `baseline.raw` snapshot that `restore` writes back. */
|
|
3369
|
+
function writable_attrs(tag) {
|
|
3370
|
+
const handler_writes = RESIZE_WRITE_ATTRS[tag];
|
|
3371
|
+
return handler_writes ? [...handler_writes, "transform"] : [];
|
|
3372
|
+
}
|
|
3048
3373
|
function is_resizable(tag) {
|
|
3049
3374
|
switch (tag) {
|
|
3050
3375
|
case "rect":
|
|
@@ -3146,12 +3471,21 @@ let resize_pipeline;
|
|
|
3146
3471
|
break;
|
|
3147
3472
|
default: attrs = { kind: "unsupported" };
|
|
3148
3473
|
}
|
|
3474
|
+
const raw = writable_attrs(tag).map((name) => ({
|
|
3475
|
+
name,
|
|
3476
|
+
value: doc.get_attr(id, name)
|
|
3477
|
+
}));
|
|
3149
3478
|
return {
|
|
3150
3479
|
bbox,
|
|
3151
|
-
attrs
|
|
3480
|
+
attrs,
|
|
3481
|
+
raw
|
|
3152
3482
|
};
|
|
3153
3483
|
}
|
|
3154
3484
|
_intent.capture_baseline = capture_baseline;
|
|
3485
|
+
function restore(doc, id, baseline) {
|
|
3486
|
+
for (const a of baseline.raw) doc.set_attr(id, a.name, a.value);
|
|
3487
|
+
}
|
|
3488
|
+
_intent.restore = restore;
|
|
3155
3489
|
function compute_factors(baseline, dir, dx, dy, shift) {
|
|
3156
3490
|
const b = baseline.bbox;
|
|
3157
3491
|
let anchorX = 0;
|
|
@@ -3554,12 +3888,11 @@ let resize_pipeline;
|
|
|
3554
3888
|
}
|
|
3555
3889
|
_resize_pipeline.apply = apply;
|
|
3556
3890
|
function revert(doc, plan) {
|
|
3557
|
-
const f = intent.compute_factors(plan.baseline, plan.direction, 0, 0, false);
|
|
3558
3891
|
const members = plan.members ?? [{
|
|
3559
3892
|
id: plan.id,
|
|
3560
3893
|
baseline: plan.baseline
|
|
3561
3894
|
}];
|
|
3562
|
-
for (const m of members) intent.
|
|
3895
|
+
for (const m of members) intent.restore(doc, m.id, m.baseline);
|
|
3563
3896
|
}
|
|
3564
3897
|
_resize_pipeline.revert = revert;
|
|
3565
3898
|
function synthesize_group_baseline(union) {
|
|
@@ -3576,7 +3909,8 @@ let resize_pipeline;
|
|
|
3576
3909
|
y: union.y,
|
|
3577
3910
|
w: union.width,
|
|
3578
3911
|
h: union.height
|
|
3579
|
-
}
|
|
3912
|
+
},
|
|
3913
|
+
raw: []
|
|
3580
3914
|
};
|
|
3581
3915
|
}
|
|
3582
3916
|
_resize_pipeline.synthesize_group_baseline = synthesize_group_baseline;
|
|
@@ -4834,4 +5168,4 @@ function emitWithVerbs(network, meta) {
|
|
|
4834
5168
|
return encodeSVGPath(commands);
|
|
4835
5169
|
}
|
|
4836
5170
|
//#endregion
|
|
4837
|
-
export {
|
|
5171
|
+
export { is_text_input_focused as S, SVG_NS as _, paint as a, XMLNS_NS as b, hit_shape_svg as c, NudgeDwellWatcher as d, TranslateOrchestrator as f, subtree as g, transform as h, TOOL_CURSOR as i, RotateOrchestrator as l, group as m, insertions as n, ResizeOrchestrator as o, translate_pipeline as p, DEFAULT_STYLE as r, resize_pipeline as s, PathModel as t, rotate_pipeline as u, SvgDocument as v, array_shallow_equal as x, WELL_KNOWN_NS_PREFIXES as y };
|
package/dist/presets.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as SvgEditor } from "./editor-
|
|
2
|
-
import { n as DomSurfaceOptions, t as DomSurfaceHandle } from "./dom-
|
|
1
|
+
import { c as SvgEditor } from "./editor-KqpIW1qm.mjs";
|
|
2
|
+
import { n as DomSurfaceOptions, t as DomSurfaceHandle } from "./dom-TctdgRnn.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/presets/keynote.d.ts
|
|
5
5
|
declare namespace keynote_d_exports {
|
package/dist/presets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as SvgEditor } from "./editor-
|
|
2
|
-
import { n as DomSurfaceOptions, t as DomSurfaceHandle } from "./dom-
|
|
1
|
+
import { c as SvgEditor } from "./editor-BSxTUsW_.js";
|
|
2
|
+
import { n as DomSurfaceOptions, t as DomSurfaceHandle } from "./dom-BMzX1CXZ.js";
|
|
3
3
|
|
|
4
4
|
//#region \0rolldown/runtime.js
|
|
5
5
|
declare namespace keynote_d_exports {
|
package/dist/presets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_model = require("./model-
|
|
3
|
-
const require_dom = require("./dom-
|
|
2
|
+
const require_model = require("./model-BLhMJZKJ.js");
|
|
3
|
+
const require_dom = require("./dom-DKQ4Vt3z.js");
|
|
4
4
|
//#region src/presets/keynote.ts
|
|
5
5
|
var keynote_exports = /* @__PURE__ */ require_model.__exportAll({ attach: () => attach });
|
|
6
6
|
/**
|
package/dist/presets.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
-
import { t as attach_dom_surface } from "./dom-
|
|
2
|
+
import { t as attach_dom_surface } from "./dom-OP-kmK8k.mjs";
|
|
3
3
|
//#region src/presets/keynote.ts
|
|
4
4
|
var keynote_exports = /* @__PURE__ */ __exportAll({ attach: () => attach });
|
|
5
5
|
/**
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as EditorState, H as Mode, J as PickEvent, K as PaintPreviewSession, Q as Providers, U as NodeId, Y as PreviewSession, c as SvgEditor, j as EditorStyle, rt as Tool, t as Commands } from "./editor-
|
|
2
|
-
import { t as DomSurfaceHandle } from "./dom-
|
|
1
|
+
import { A as EditorState, H as Mode, J as PickEvent, K as PaintPreviewSession, Q as Providers, U as NodeId, Y as PreviewSession, c as SvgEditor, j as EditorStyle, rt as Tool, t as Commands } from "./editor-KqpIW1qm.mjs";
|
|
2
|
+
import { t as DomSurfaceHandle } from "./dom-TctdgRnn.mjs";
|
|
3
3
|
import cmath from "@grida/cmath";
|
|
4
4
|
import { ReactNode } from "react";
|
|
5
5
|
|
|
@@ -47,7 +47,13 @@ type SvgEditorCanvasProps = {
|
|
|
47
47
|
* Auto-fit the document on initial attach. Default `false`. See
|
|
48
48
|
* `DomSurfaceOptions.fit`.
|
|
49
49
|
*/
|
|
50
|
-
fit?: boolean;
|
|
50
|
+
fit?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Wire native ClipboardEvent transport (copy/cut/paste). Default `true`.
|
|
53
|
+
* Pass `false` to route all clipboard traffic through the
|
|
54
|
+
* `ClipboardProvider` seam. See `DomSurfaceOptions.clipboard`.
|
|
55
|
+
*/
|
|
56
|
+
clipboard?: boolean; /** Initial camera transform. Default identity. */
|
|
51
57
|
initial_camera?: cmath.Transform;
|
|
52
58
|
/**
|
|
53
59
|
* Receives the `DomSurfaceHandle` once the surface is attached, and
|
|
@@ -70,6 +76,7 @@ declare function SvgEditorCanvas({
|
|
|
70
76
|
style,
|
|
71
77
|
gestures,
|
|
72
78
|
fit,
|
|
79
|
+
clipboard,
|
|
73
80
|
initial_camera,
|
|
74
81
|
onAttach
|
|
75
82
|
}: SvgEditorCanvasProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as EditorState, H as Mode, J as PickEvent, K as PaintPreviewSession, Q as Providers, U as NodeId, Y as PreviewSession, c as SvgEditor, j as EditorStyle, rt as Tool, t as Commands } from "./editor-
|
|
2
|
-
import { t as DomSurfaceHandle } from "./dom-
|
|
1
|
+
import { A as EditorState, H as Mode, J as PickEvent, K as PaintPreviewSession, Q as Providers, U as NodeId, Y as PreviewSession, c as SvgEditor, j as EditorStyle, rt as Tool, t as Commands } from "./editor-BSxTUsW_.js";
|
|
2
|
+
import { t as DomSurfaceHandle } from "./dom-BMzX1CXZ.js";
|
|
3
3
|
import cmath from "@grida/cmath";
|
|
4
4
|
import { ReactNode } from "react";
|
|
5
5
|
|
|
@@ -47,7 +47,13 @@ type SvgEditorCanvasProps = {
|
|
|
47
47
|
* Auto-fit the document on initial attach. Default `false`. See
|
|
48
48
|
* `DomSurfaceOptions.fit`.
|
|
49
49
|
*/
|
|
50
|
-
fit?: boolean;
|
|
50
|
+
fit?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Wire native ClipboardEvent transport (copy/cut/paste). Default `true`.
|
|
53
|
+
* Pass `false` to route all clipboard traffic through the
|
|
54
|
+
* `ClipboardProvider` seam. See `DomSurfaceOptions.clipboard`.
|
|
55
|
+
*/
|
|
56
|
+
clipboard?: boolean; /** Initial camera transform. Default identity. */
|
|
51
57
|
initial_camera?: cmath.Transform;
|
|
52
58
|
/**
|
|
53
59
|
* Receives the `DomSurfaceHandle` once the surface is attached, and
|
|
@@ -70,6 +76,7 @@ declare function SvgEditorCanvas({
|
|
|
70
76
|
style,
|
|
71
77
|
gestures,
|
|
72
78
|
fit,
|
|
79
|
+
clipboard,
|
|
73
80
|
initial_camera,
|
|
74
81
|
onAttach
|
|
75
82
|
}: SvgEditorCanvasProps): import("react/jsx-runtime").JSX.Element;
|