@grida/svg-editor 1.0.0-alpha.16 → 1.0.0-alpha.17
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-DOvcMvl4.mjs → dom-Bjj9xySE.mjs} +105 -12
- package/dist/{dom-U6ae5fQF.js → dom-CaByuo6C.js} +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-DKQOIKuU.mjs → editor-BLsELHSZ.mjs} +384 -31
- package/dist/{editor-CYoGJ3Hf.d.ts → editor-BSxTUsW_.d.ts} +246 -4
- package/dist/{editor-D2eQe8lB.d.mts → editor-KqpIW1qm.d.mts} +246 -4
- package/dist/{editor-C6Lj1In-.js → editor-N9af0JD2.js} +383 -31
- 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-L3t9ixT_.mjs → model-DMaN5GnH.mjs} +286 -24
- package/dist/{model-D0nU_EkL.js → model-GpysNbOv.js} +297 -23
- 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();
|
|
@@ -4915,6 +5177,12 @@ Object.defineProperty(exports, "TranslateOrchestrator", {
|
|
|
4915
5177
|
return TranslateOrchestrator;
|
|
4916
5178
|
}
|
|
4917
5179
|
});
|
|
5180
|
+
Object.defineProperty(exports, "WELL_KNOWN_NS_PREFIXES", {
|
|
5181
|
+
enumerable: true,
|
|
5182
|
+
get: function() {
|
|
5183
|
+
return WELL_KNOWN_NS_PREFIXES;
|
|
5184
|
+
}
|
|
5185
|
+
});
|
|
4918
5186
|
Object.defineProperty(exports, "__exportAll", {
|
|
4919
5187
|
enumerable: true,
|
|
4920
5188
|
get: function() {
|
|
@@ -4975,6 +5243,12 @@ Object.defineProperty(exports, "rotate_pipeline", {
|
|
|
4975
5243
|
return rotate_pipeline;
|
|
4976
5244
|
}
|
|
4977
5245
|
});
|
|
5246
|
+
Object.defineProperty(exports, "subtree", {
|
|
5247
|
+
enumerable: true,
|
|
5248
|
+
get: function() {
|
|
5249
|
+
return subtree;
|
|
5250
|
+
}
|
|
5251
|
+
});
|
|
4978
5252
|
Object.defineProperty(exports, "transform", {
|
|
4979
5253
|
enumerable: true,
|
|
4980
5254
|
get: function() {
|
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-GpysNbOv.js");
|
|
3
|
+
const require_dom = require("./dom-CaByuo6C.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-Bjj9xySE.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;
|
package/dist/react.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_editor = require("./editor-
|
|
4
|
-
const require_dom = require("./dom-
|
|
3
|
+
const require_editor = require("./editor-N9af0JD2.js");
|
|
4
|
+
const require_dom = require("./dom-CaByuo6C.js");
|
|
5
5
|
let react = require("react");
|
|
6
6
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
7
|
//#region src/react.tsx
|
|
@@ -40,7 +40,7 @@ function SvgEditorProvider({ initialSvg, providers, style, children }) {
|
|
|
40
40
|
* context for them, because a host may mount multiple canvases in the
|
|
41
41
|
* same editor session.
|
|
42
42
|
*/
|
|
43
|
-
function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAttach }) {
|
|
43
|
+
function SvgEditorCanvas({ className, style, gestures, fit, clipboard, initial_camera, onAttach }) {
|
|
44
44
|
const editor = useSvgEditor();
|
|
45
45
|
const ref = (0, react.useRef)(null);
|
|
46
46
|
const on_attach_ref = (0, react.useRef)(onAttach);
|
|
@@ -54,6 +54,7 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
|
|
|
54
54
|
container,
|
|
55
55
|
gestures,
|
|
56
56
|
fit,
|
|
57
|
+
clipboard,
|
|
57
58
|
initial_camera: initial_camera_ref.current
|
|
58
59
|
});
|
|
59
60
|
on_attach_ref.current?.(handle);
|
|
@@ -64,7 +65,8 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
|
|
|
64
65
|
}, [
|
|
65
66
|
editor,
|
|
66
67
|
gestures,
|
|
67
|
-
fit
|
|
68
|
+
fit,
|
|
69
|
+
clipboard
|
|
68
70
|
]);
|
|
69
71
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
70
72
|
ref,
|
package/dist/react.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { t as createSvgEditor } from "./editor-
|
|
3
|
-
import { t as attach_dom_surface } from "./dom-
|
|
2
|
+
import { t as createSvgEditor } from "./editor-BLsELHSZ.mjs";
|
|
3
|
+
import { t as attach_dom_surface } from "./dom-Bjj9xySE.mjs";
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
//#region src/react.tsx
|
|
@@ -39,7 +39,7 @@ function SvgEditorProvider({ initialSvg, providers, style, children }) {
|
|
|
39
39
|
* context for them, because a host may mount multiple canvases in the
|
|
40
40
|
* same editor session.
|
|
41
41
|
*/
|
|
42
|
-
function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAttach }) {
|
|
42
|
+
function SvgEditorCanvas({ className, style, gestures, fit, clipboard, initial_camera, onAttach }) {
|
|
43
43
|
const editor = useSvgEditor();
|
|
44
44
|
const ref = useRef(null);
|
|
45
45
|
const on_attach_ref = useRef(onAttach);
|
|
@@ -53,6 +53,7 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
|
|
|
53
53
|
container,
|
|
54
54
|
gestures,
|
|
55
55
|
fit,
|
|
56
|
+
clipboard,
|
|
56
57
|
initial_camera: initial_camera_ref.current
|
|
57
58
|
});
|
|
58
59
|
on_attach_ref.current?.(handle);
|
|
@@ -63,7 +64,8 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
|
|
|
63
64
|
}, [
|
|
64
65
|
editor,
|
|
65
66
|
gestures,
|
|
66
|
-
fit
|
|
67
|
+
fit,
|
|
68
|
+
clipboard
|
|
67
69
|
]);
|
|
68
70
|
return /* @__PURE__ */ jsx("div", {
|
|
69
71
|
ref,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grida/svg-editor",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.17",
|
|
4
4
|
"description": "Headless SVG editor (experimental).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bezier",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@grida/cmath": "0.2.3",
|
|
62
|
-
"@grida/
|
|
62
|
+
"@grida/hud": "0.2.2",
|
|
63
63
|
"@grida/keybinding": "0.2.1",
|
|
64
|
-
"@grida/svg": "0.
|
|
65
|
-
"@grida/
|
|
64
|
+
"@grida/svg": "0.2.0",
|
|
65
|
+
"@grida/history": "0.1.1",
|
|
66
66
|
"@grida/vn": "0.1.0",
|
|
67
|
-
"@grida/
|
|
67
|
+
"@grida/text-editor": "0.1.2"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/react": "^19",
|