@grida/svg-editor 1.0.0-alpha.22 → 1.0.0-alpha.23

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.
@@ -198,6 +198,22 @@ declare function project_point_through_ctm(px: number, py: number, ctm: {
198
198
  e: number;
199
199
  f: number;
200
200
  }, container_offset: readonly [number, number]): [number, number];
201
+ /**
202
+ * The matrix form of {@link project_point_through_ctm}: the full
203
+ * `local → container-px` affine for a CTM + container offset, as a
204
+ * `cmath.Transform`. Use when handing the transform to a downstream projector
205
+ * (e.g. HUD chrome) instead of projecting a single point.
206
+ *
207
+ * Pure function, no DOM types.
208
+ */
209
+ declare function ctm_to_container_transform(ctm: {
210
+ a: number;
211
+ b: number;
212
+ c: number;
213
+ d: number;
214
+ e: number;
215
+ f: number;
216
+ }, container_offset: readonly [number, number]): cmath.Transform;
201
217
  /**
202
218
  * Inverse of the CTM's linear part applied to a delta vector. Drops
203
219
  * translation. Used to convert a HUD-reported container-space drag delta
@@ -236,4 +252,4 @@ declare function inverse_project_rect(rect: {
236
252
  f: number;
237
253
  }, offset: readonly [number, number]): cmath.Rectangle | null;
238
254
  //#endregion
239
- export { install_font_load_geometry_bump as a, project_point_through_ctm as c, attach_dom_surface as i, DEFAULT_SNAP_OPTIONS as l, DomSurfaceHandle as n, inverse_project_rect as o, DomSurfaceOptions as r, project_delta_inverse_ctm as s, AttentionScope as t, SnapOptions as u };
255
+ export { ctm_to_container_transform as a, project_delta_inverse_ctm as c, SnapOptions as d, attach_dom_surface as i, project_point_through_ctm as l, DomSurfaceHandle as n, install_font_load_geometry_bump as o, DomSurfaceOptions as r, inverse_project_rect as s, AttentionScope as t, DEFAULT_SNAP_OPTIONS as u };
@@ -1222,44 +1222,32 @@ function create_attention_tracker(container) {
1222
1222
  }
1223
1223
  //#endregion
1224
1224
  //#region src/text-surface.ts
1225
- const SVG_NS = "http://www.w3.org/2000/svg";
1226
1225
  const XML_NS = "http://www.w3.org/XML/1998/namespace";
1227
1226
  var SvgTextSurface = class {
1228
- constructor(textEl) {
1227
+ /**
1228
+ * @param textEl the live text element to drive.
1229
+ * @param sink receives the caret + selection geometry (local space) on
1230
+ * every change; the host projects + forwards it to the HUD. Called with
1231
+ * `null` on dispose so the host clears the chrome.
1232
+ */
1233
+ constructor(textEl, sink) {
1229
1234
  this.prevXmlSpace = void 0;
1230
1235
  this.prevPointerEvents = void 0;
1236
+ this.caret = null;
1237
+ this.selection = null;
1231
1238
  this.last_caret_idx = -1;
1232
1239
  this.last_caret_visible = false;
1233
1240
  this.last_sel_start = -1;
1234
1241
  this.last_sel_end = -1;
1235
1242
  this.textEl = textEl;
1236
- const ownerDoc = textEl.ownerDocument;
1237
- let mountAnchor = textEl;
1238
- while (mountAnchor.parentElement instanceof SVGElement && (mountAnchor.localName === "tspan" || mountAnchor.localName === "textPath")) mountAnchor = mountAnchor.parentElement;
1239
- const parent = mountAnchor.parentNode;
1240
- if (!parent) throw new Error("text element has no parent");
1241
- const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
1243
+ this.sink = sink;
1244
+ const computedWhitespace = textEl.ownerDocument.defaultView?.getComputedStyle(textEl).whiteSpace;
1242
1245
  if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
1243
1246
  this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
1244
1247
  textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
1245
1248
  }
1246
1249
  this.prevPointerEvents = textEl.getAttribute("pointer-events");
1247
1250
  textEl.setAttribute("pointer-events", "bounding-box");
1248
- const selection = ownerDoc.createElementNS(SVG_NS, "rect");
1249
- selection.setAttribute("fill", "#2563eb");
1250
- selection.setAttribute("fill-opacity", "0.25");
1251
- selection.setAttribute("pointer-events", "none");
1252
- selection.setAttribute("data-svg-text-edit-selection", "");
1253
- selection.style.display = "none";
1254
- parent.insertBefore(selection, mountAnchor);
1255
- this.selectionRect = selection;
1256
- const caret = ownerDoc.createElementNS(SVG_NS, "rect");
1257
- caret.setAttribute("fill", "#2563eb");
1258
- caret.setAttribute("pointer-events", "none");
1259
- caret.setAttribute("data-svg-text-edit-caret", "");
1260
- caret.style.display = "none";
1261
- parent.insertBefore(caret, mountAnchor.nextSibling);
1262
- this.caretRect = caret;
1263
1251
  }
1264
1252
  setText(text) {
1265
1253
  if (this.textEl.textContent !== text) this.textEl.textContent = text;
@@ -1268,38 +1256,43 @@ var SvgTextSurface = class {
1268
1256
  if (index === this.last_caret_idx && visible === this.last_caret_visible) return;
1269
1257
  this.last_caret_idx = index;
1270
1258
  this.last_caret_visible = visible;
1271
- if (!visible) {
1272
- this.caretRect.style.display = "none";
1273
- return;
1274
- }
1275
1259
  const m = this.metrics();
1276
1260
  const x = this.charX(index);
1277
- this.caretRect.setAttribute("x", String(x - .75));
1278
- this.caretRect.setAttribute("y", String(m.top));
1279
- this.caretRect.setAttribute("width", "1.5");
1280
- this.caretRect.setAttribute("height", String(m.height));
1281
- this.caretRect.style.display = "block";
1261
+ this.caret = {
1262
+ top: [x, m.top],
1263
+ bottom: [x, m.top + m.height],
1264
+ visible
1265
+ };
1266
+ this.emit();
1282
1267
  }
1283
1268
  setSelection(start, end) {
1284
1269
  if (start === this.last_sel_start && end === this.last_sel_end) return;
1285
1270
  this.last_sel_start = start;
1286
1271
  this.last_sel_end = end;
1287
1272
  if (start === end) {
1288
- this.selectionRect.style.display = "none";
1273
+ this.selection = null;
1274
+ this.emit();
1289
1275
  return;
1290
1276
  }
1291
1277
  const m = this.metrics();
1292
1278
  const x1 = this.charX(start);
1293
1279
  const x2 = this.charX(end);
1294
- this.selectionRect.setAttribute("x", String(Math.min(x1, x2)));
1295
- this.selectionRect.setAttribute("y", String(m.top));
1296
- this.selectionRect.setAttribute("width", String(Math.abs(x2 - x1)));
1297
- this.selectionRect.setAttribute("height", String(m.height));
1298
- this.selectionRect.style.display = "block";
1280
+ this.selection = {
1281
+ x: Math.min(x1, x2),
1282
+ y: m.top,
1283
+ width: Math.abs(x2 - x1),
1284
+ height: m.height
1285
+ };
1286
+ this.emit();
1287
+ }
1288
+ emit() {
1289
+ this.sink({
1290
+ caret: this.caret,
1291
+ selection: this.selection
1292
+ });
1299
1293
  }
1300
1294
  dispose(keepEditMutations = false) {
1301
- this.caretRect.remove();
1302
- this.selectionRect.remove();
1295
+ this.sink(null);
1303
1296
  if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
1304
1297
  else this.textEl.setAttributeNS(XML_NS, "xml:space", this.prevXmlSpace);
1305
1298
  if (this.prevPointerEvents !== void 0) if (this.prevPointerEvents === null) this.textEl.removeAttribute("pointer-events");
@@ -1923,6 +1916,7 @@ var DomSurface = class DomSurface {
1923
1916
  this.text_edit = null;
1924
1917
  this.text_edit_target = null;
1925
1918
  this.text_edit_original = "";
1919
+ this.text_edit_geom = null;
1926
1920
  this.pending_text_insert = null;
1927
1921
  this.vector_edit = null;
1928
1922
  this.point_snap_guide = void 0;
@@ -2067,6 +2061,7 @@ var DomSurface = class DomSurface {
2067
2061
  this.apply_camera_transform();
2068
2062
  this.hud.setPixelGridTransform(this.camera.transform);
2069
2063
  this.sync_surface_selection();
2064
+ if (this.text_edit_geom) this.push_text_edit_chrome(this.text_edit_geom);
2070
2065
  this.redraw();
2071
2066
  }));
2072
2067
  this.render();
@@ -4074,7 +4069,10 @@ var DomSurface = class DomSurface {
4074
4069
  this.sync_surface_selection();
4075
4070
  this.sync_cursor();
4076
4071
  this.redraw();
4077
- const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el);
4072
+ const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el, (geom) => {
4073
+ this.push_text_edit_chrome(geom);
4074
+ this.request_redraw();
4075
+ });
4078
4076
  const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
4079
4077
  let settled = false;
4080
4078
  this.text_edit = createTextEditor({
@@ -4122,11 +4120,45 @@ var DomSurface = class DomSurface {
4122
4120
  this.render();
4123
4121
  this.sync_surface_selection();
4124
4122
  this.sync_cursor();
4123
+ this.push_text_edit_chrome(null);
4125
4124
  this.redraw();
4126
4125
  this.text_edit = null;
4127
4126
  this.text_edit_target = null;
4128
4127
  }
4129
4128
  /**
4129
+ * Project the active session's text-edit geometry (LOCAL svg user-space) and
4130
+ * push it to the HUD as text-edit chrome (caret + selection), or clear it on
4131
+ * `null`. The local→container-px transform comes from the live text
4132
+ * element's `getScreenCTM` (which folds in the camera's CSS transform on the
4133
+ * SVG root) minus the container offset — the same projection convention
4134
+ * `vector_of` / `container_box` use. The HUD keeps its own transform at
4135
+ * identity, so this per-chrome transform is the whole projection. Does NOT
4136
+ * redraw — callers own that.
4137
+ */
4138
+ push_text_edit_chrome(geom) {
4139
+ this.text_edit_geom = geom;
4140
+ if (!geom) {
4141
+ this.hud.setTextEditChrome(null);
4142
+ return;
4143
+ }
4144
+ const ctm = (this.text_edit_target ? this.element_index.get(this.text_edit_target) : void 0)?.getScreenCTM?.() ?? null;
4145
+ if (!ctm) {
4146
+ this.hud.setTextEditChrome(null);
4147
+ return;
4148
+ }
4149
+ const input = {
4150
+ transform: ctm_to_container_transform(ctm, this.container_offset()),
4151
+ caret: geom.caret ? {
4152
+ top: geom.caret.top,
4153
+ bottom: geom.caret.bottom
4154
+ } : void 0,
4155
+ caretVisible: geom.caret?.visible ?? false,
4156
+ selectionRects: geom.selection ? [geom.selection] : void 0,
4157
+ style: { caretColor: "#000000" }
4158
+ };
4159
+ this.hud.setTextEditChrome(input);
4160
+ }
4161
+ /**
4130
4162
  * Realize the result of a text content-edit session and exit. `result`
4131
4163
  * is the text that should remain — the typed text on commit, the original
4132
4164
  * on cancel. Implements the empty-equals-delete rule (design:
@@ -4945,6 +4977,25 @@ function project_point_through_ctm(px, py, ctm, container_offset) {
4945
4977
  return [sx + container_offset[0], sy + container_offset[1]];
4946
4978
  }
4947
4979
  /**
4980
+ * The matrix form of {@link project_point_through_ctm}: the full
4981
+ * `local → container-px` affine for a CTM + container offset, as a
4982
+ * `cmath.Transform`. Use when handing the transform to a downstream projector
4983
+ * (e.g. HUD chrome) instead of projecting a single point.
4984
+ *
4985
+ * Pure function, no DOM types.
4986
+ */
4987
+ function ctm_to_container_transform(ctm, container_offset) {
4988
+ return [[
4989
+ ctm.a,
4990
+ ctm.c,
4991
+ ctm.e + container_offset[0]
4992
+ ], [
4993
+ ctm.b,
4994
+ ctm.d,
4995
+ ctm.f + container_offset[1]
4996
+ ]];
4997
+ }
4998
+ /**
4948
4999
  * Inverse of the CTM's linear part applied to a delta vector. Drops
4949
5000
  * translation. Used to convert a HUD-reported container-space drag delta
4950
5001
  * back to the path's local coord space for `PathModel.translateVertices`.
@@ -5220,4 +5271,4 @@ var SvgHitShapeDriver = class {
5220
5271
  }
5221
5272
  };
5222
5273
  //#endregion
5223
- export { project_point_through_ctm as a, MemoizedGeometryProvider as c, project_delta_inverse_ctm as i, Camera as l, install_font_load_geometry_bump as n, Gestures as o, inverse_project_rect as r, DEFAULT_SNAP_OPTIONS as s, attach_dom_surface as t };
5274
+ export { project_delta_inverse_ctm as a, DEFAULT_SNAP_OPTIONS as c, inverse_project_rect as i, MemoizedGeometryProvider as l, ctm_to_container_transform as n, project_point_through_ctm as o, install_font_load_geometry_bump as r, Gestures as s, attach_dom_surface as t, Camera as u };
@@ -1224,44 +1224,32 @@ function create_attention_tracker(container) {
1224
1224
  }
1225
1225
  //#endregion
1226
1226
  //#region src/text-surface.ts
1227
- const SVG_NS = "http://www.w3.org/2000/svg";
1228
1227
  const XML_NS = "http://www.w3.org/XML/1998/namespace";
1229
1228
  var SvgTextSurface = class {
1230
- constructor(textEl) {
1229
+ /**
1230
+ * @param textEl the live text element to drive.
1231
+ * @param sink receives the caret + selection geometry (local space) on
1232
+ * every change; the host projects + forwards it to the HUD. Called with
1233
+ * `null` on dispose so the host clears the chrome.
1234
+ */
1235
+ constructor(textEl, sink) {
1231
1236
  this.prevXmlSpace = void 0;
1232
1237
  this.prevPointerEvents = void 0;
1238
+ this.caret = null;
1239
+ this.selection = null;
1233
1240
  this.last_caret_idx = -1;
1234
1241
  this.last_caret_visible = false;
1235
1242
  this.last_sel_start = -1;
1236
1243
  this.last_sel_end = -1;
1237
1244
  this.textEl = textEl;
1238
- const ownerDoc = textEl.ownerDocument;
1239
- let mountAnchor = textEl;
1240
- while (mountAnchor.parentElement instanceof SVGElement && (mountAnchor.localName === "tspan" || mountAnchor.localName === "textPath")) mountAnchor = mountAnchor.parentElement;
1241
- const parent = mountAnchor.parentNode;
1242
- if (!parent) throw new Error("text element has no parent");
1243
- const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
1245
+ this.sink = sink;
1246
+ const computedWhitespace = textEl.ownerDocument.defaultView?.getComputedStyle(textEl).whiteSpace;
1244
1247
  if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
1245
1248
  this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
1246
1249
  textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
1247
1250
  }
1248
1251
  this.prevPointerEvents = textEl.getAttribute("pointer-events");
1249
1252
  textEl.setAttribute("pointer-events", "bounding-box");
1250
- const selection = ownerDoc.createElementNS(SVG_NS, "rect");
1251
- selection.setAttribute("fill", "#2563eb");
1252
- selection.setAttribute("fill-opacity", "0.25");
1253
- selection.setAttribute("pointer-events", "none");
1254
- selection.setAttribute("data-svg-text-edit-selection", "");
1255
- selection.style.display = "none";
1256
- parent.insertBefore(selection, mountAnchor);
1257
- this.selectionRect = selection;
1258
- const caret = ownerDoc.createElementNS(SVG_NS, "rect");
1259
- caret.setAttribute("fill", "#2563eb");
1260
- caret.setAttribute("pointer-events", "none");
1261
- caret.setAttribute("data-svg-text-edit-caret", "");
1262
- caret.style.display = "none";
1263
- parent.insertBefore(caret, mountAnchor.nextSibling);
1264
- this.caretRect = caret;
1265
1253
  }
1266
1254
  setText(text) {
1267
1255
  if (this.textEl.textContent !== text) this.textEl.textContent = text;
@@ -1270,38 +1258,43 @@ var SvgTextSurface = class {
1270
1258
  if (index === this.last_caret_idx && visible === this.last_caret_visible) return;
1271
1259
  this.last_caret_idx = index;
1272
1260
  this.last_caret_visible = visible;
1273
- if (!visible) {
1274
- this.caretRect.style.display = "none";
1275
- return;
1276
- }
1277
1261
  const m = this.metrics();
1278
1262
  const x = this.charX(index);
1279
- this.caretRect.setAttribute("x", String(x - .75));
1280
- this.caretRect.setAttribute("y", String(m.top));
1281
- this.caretRect.setAttribute("width", "1.5");
1282
- this.caretRect.setAttribute("height", String(m.height));
1283
- this.caretRect.style.display = "block";
1263
+ this.caret = {
1264
+ top: [x, m.top],
1265
+ bottom: [x, m.top + m.height],
1266
+ visible
1267
+ };
1268
+ this.emit();
1284
1269
  }
1285
1270
  setSelection(start, end) {
1286
1271
  if (start === this.last_sel_start && end === this.last_sel_end) return;
1287
1272
  this.last_sel_start = start;
1288
1273
  this.last_sel_end = end;
1289
1274
  if (start === end) {
1290
- this.selectionRect.style.display = "none";
1275
+ this.selection = null;
1276
+ this.emit();
1291
1277
  return;
1292
1278
  }
1293
1279
  const m = this.metrics();
1294
1280
  const x1 = this.charX(start);
1295
1281
  const x2 = this.charX(end);
1296
- this.selectionRect.setAttribute("x", String(Math.min(x1, x2)));
1297
- this.selectionRect.setAttribute("y", String(m.top));
1298
- this.selectionRect.setAttribute("width", String(Math.abs(x2 - x1)));
1299
- this.selectionRect.setAttribute("height", String(m.height));
1300
- this.selectionRect.style.display = "block";
1282
+ this.selection = {
1283
+ x: Math.min(x1, x2),
1284
+ y: m.top,
1285
+ width: Math.abs(x2 - x1),
1286
+ height: m.height
1287
+ };
1288
+ this.emit();
1289
+ }
1290
+ emit() {
1291
+ this.sink({
1292
+ caret: this.caret,
1293
+ selection: this.selection
1294
+ });
1301
1295
  }
1302
1296
  dispose(keepEditMutations = false) {
1303
- this.caretRect.remove();
1304
- this.selectionRect.remove();
1297
+ this.sink(null);
1305
1298
  if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
1306
1299
  else this.textEl.setAttributeNS(XML_NS, "xml:space", this.prevXmlSpace);
1307
1300
  if (this.prevPointerEvents !== void 0) if (this.prevPointerEvents === null) this.textEl.removeAttribute("pointer-events");
@@ -1925,6 +1918,7 @@ var DomSurface = class DomSurface {
1925
1918
  this.text_edit = null;
1926
1919
  this.text_edit_target = null;
1927
1920
  this.text_edit_original = "";
1921
+ this.text_edit_geom = null;
1928
1922
  this.pending_text_insert = null;
1929
1923
  this.vector_edit = null;
1930
1924
  this.point_snap_guide = void 0;
@@ -2069,6 +2063,7 @@ var DomSurface = class DomSurface {
2069
2063
  this.apply_camera_transform();
2070
2064
  this.hud.setPixelGridTransform(this.camera.transform);
2071
2065
  this.sync_surface_selection();
2066
+ if (this.text_edit_geom) this.push_text_edit_chrome(this.text_edit_geom);
2072
2067
  this.redraw();
2073
2068
  }));
2074
2069
  this.render();
@@ -4076,7 +4071,10 @@ var DomSurface = class DomSurface {
4076
4071
  this.sync_surface_selection();
4077
4072
  this.sync_cursor();
4078
4073
  this.redraw();
4079
- const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el);
4074
+ const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el, (geom) => {
4075
+ this.push_text_edit_chrome(geom);
4076
+ this.request_redraw();
4077
+ });
4080
4078
  const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
4081
4079
  let settled = false;
4082
4080
  this.text_edit = (0, _grida_text_editor_dom.createTextEditor)({
@@ -4124,11 +4122,45 @@ var DomSurface = class DomSurface {
4124
4122
  this.render();
4125
4123
  this.sync_surface_selection();
4126
4124
  this.sync_cursor();
4125
+ this.push_text_edit_chrome(null);
4127
4126
  this.redraw();
4128
4127
  this.text_edit = null;
4129
4128
  this.text_edit_target = null;
4130
4129
  }
4131
4130
  /**
4131
+ * Project the active session's text-edit geometry (LOCAL svg user-space) and
4132
+ * push it to the HUD as text-edit chrome (caret + selection), or clear it on
4133
+ * `null`. The local→container-px transform comes from the live text
4134
+ * element's `getScreenCTM` (which folds in the camera's CSS transform on the
4135
+ * SVG root) minus the container offset — the same projection convention
4136
+ * `vector_of` / `container_box` use. The HUD keeps its own transform at
4137
+ * identity, so this per-chrome transform is the whole projection. Does NOT
4138
+ * redraw — callers own that.
4139
+ */
4140
+ push_text_edit_chrome(geom) {
4141
+ this.text_edit_geom = geom;
4142
+ if (!geom) {
4143
+ this.hud.setTextEditChrome(null);
4144
+ return;
4145
+ }
4146
+ const ctm = (this.text_edit_target ? this.element_index.get(this.text_edit_target) : void 0)?.getScreenCTM?.() ?? null;
4147
+ if (!ctm) {
4148
+ this.hud.setTextEditChrome(null);
4149
+ return;
4150
+ }
4151
+ const input = {
4152
+ transform: ctm_to_container_transform(ctm, this.container_offset()),
4153
+ caret: geom.caret ? {
4154
+ top: geom.caret.top,
4155
+ bottom: geom.caret.bottom
4156
+ } : void 0,
4157
+ caretVisible: geom.caret?.visible ?? false,
4158
+ selectionRects: geom.selection ? [geom.selection] : void 0,
4159
+ style: { caretColor: "#000000" }
4160
+ };
4161
+ this.hud.setTextEditChrome(input);
4162
+ }
4163
+ /**
4132
4164
  * Realize the result of a text content-edit session and exit. `result`
4133
4165
  * is the text that should remain — the typed text on commit, the original
4134
4166
  * on cancel. Implements the empty-equals-delete rule (design:
@@ -4947,6 +4979,25 @@ function project_point_through_ctm(px, py, ctm, container_offset) {
4947
4979
  return [sx + container_offset[0], sy + container_offset[1]];
4948
4980
  }
4949
4981
  /**
4982
+ * The matrix form of {@link project_point_through_ctm}: the full
4983
+ * `local → container-px` affine for a CTM + container offset, as a
4984
+ * `cmath.Transform`. Use when handing the transform to a downstream projector
4985
+ * (e.g. HUD chrome) instead of projecting a single point.
4986
+ *
4987
+ * Pure function, no DOM types.
4988
+ */
4989
+ function ctm_to_container_transform(ctm, container_offset) {
4990
+ return [[
4991
+ ctm.a,
4992
+ ctm.c,
4993
+ ctm.e + container_offset[0]
4994
+ ], [
4995
+ ctm.b,
4996
+ ctm.d,
4997
+ ctm.f + container_offset[1]
4998
+ ]];
4999
+ }
5000
+ /**
4950
5001
  * Inverse of the CTM's linear part applied to a delta vector. Drops
4951
5002
  * translation. Used to convert a HUD-reported container-space drag delta
4952
5003
  * back to the path's local coord space for `PathModel.translateVertices`.
@@ -5252,6 +5303,12 @@ Object.defineProperty(exports, "attach_dom_surface", {
5252
5303
  return attach_dom_surface;
5253
5304
  }
5254
5305
  });
5306
+ Object.defineProperty(exports, "ctm_to_container_transform", {
5307
+ enumerable: true,
5308
+ get: function() {
5309
+ return ctm_to_container_transform;
5310
+ }
5311
+ });
5255
5312
  Object.defineProperty(exports, "install_font_load_geometry_bump", {
5256
5313
  enumerable: true,
5257
5314
  get: function() {
@@ -196,6 +196,22 @@ declare function project_point_through_ctm(px: number, py: number, ctm: {
196
196
  e: number;
197
197
  f: number;
198
198
  }, container_offset: readonly [number, number]): [number, number];
199
+ /**
200
+ * The matrix form of {@link project_point_through_ctm}: the full
201
+ * `local → container-px` affine for a CTM + container offset, as a
202
+ * `cmath.Transform`. Use when handing the transform to a downstream projector
203
+ * (e.g. HUD chrome) instead of projecting a single point.
204
+ *
205
+ * Pure function, no DOM types.
206
+ */
207
+ declare function ctm_to_container_transform(ctm: {
208
+ a: number;
209
+ b: number;
210
+ c: number;
211
+ d: number;
212
+ e: number;
213
+ f: number;
214
+ }, container_offset: readonly [number, number]): cmath.Transform;
199
215
  /**
200
216
  * Inverse of the CTM's linear part applied to a delta vector. Drops
201
217
  * translation. Used to convert a HUD-reported container-space drag delta
@@ -234,4 +250,4 @@ declare function inverse_project_rect(rect: {
234
250
  f: number;
235
251
  }, offset: readonly [number, number]): cmath.Rectangle | null;
236
252
  //#endregion
237
- export { install_font_load_geometry_bump as a, project_point_through_ctm as c, attach_dom_surface as i, DEFAULT_SNAP_OPTIONS as l, DomSurfaceHandle as n, inverse_project_rect as o, DomSurfaceOptions as r, project_delta_inverse_ctm as s, AttentionScope as t, SnapOptions as u };
253
+ export { ctm_to_container_transform as a, project_delta_inverse_ctm as c, SnapOptions as d, attach_dom_surface as i, project_point_through_ctm as l, DomSurfaceHandle as n, install_font_load_geometry_bump as o, DomSurfaceOptions as r, inverse_project_rect as s, AttentionScope as t, DEFAULT_SNAP_OPTIONS as u };
package/dist/dom.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-CcW4BVth.mjs";
2
- import { a as install_font_load_geometry_bump, c as project_point_through_ctm, i as attach_dom_surface, l as DEFAULT_SNAP_OPTIONS, n as DomSurfaceHandle, o as inverse_project_rect, r as DomSurfaceOptions, s as project_delta_inverse_ctm, t as AttentionScope, u as SnapOptions } from "./dom-Dw2SPHgc.mjs";
3
- export { AttentionScope, type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
2
+ import { a as ctm_to_container_transform, c as project_delta_inverse_ctm, d as SnapOptions, i as attach_dom_surface, l as project_point_through_ctm, n as DomSurfaceHandle, o as install_font_load_geometry_bump, r as DomSurfaceOptions, s as inverse_project_rect, t as AttentionScope, u as DEFAULT_SNAP_OPTIONS } from "./dom-D-ZJO9aK.mjs";
3
+ export { AttentionScope, type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, ctm_to_container_transform, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
package/dist/dom.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-CxqRhhzP.js";
2
- import { a as install_font_load_geometry_bump, c as project_point_through_ctm, i as attach_dom_surface, l as DEFAULT_SNAP_OPTIONS, n as DomSurfaceHandle, o as inverse_project_rect, r as DomSurfaceOptions, s as project_delta_inverse_ctm, t as AttentionScope, u as SnapOptions } from "./dom-CQkWJNrK.js";
3
- export { AttentionScope, type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
2
+ import { a as ctm_to_container_transform, c as project_delta_inverse_ctm, d as SnapOptions, i as attach_dom_surface, l as project_point_through_ctm, n as DomSurfaceHandle, o as install_font_load_geometry_bump, r as DomSurfaceOptions, s as inverse_project_rect, t as AttentionScope, u as DEFAULT_SNAP_OPTIONS } from "./dom-Dl56kUW5.js";
3
+ export { AttentionScope, type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, ctm_to_container_transform, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
package/dist/dom.js CHANGED
@@ -1,10 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dom = require("./dom-H4PvmPe3.js");
2
+ const require_dom = require("./dom-Disackua.js");
3
3
  exports.Camera = require_dom.Camera;
4
4
  exports.DEFAULT_SNAP_OPTIONS = require_dom.DEFAULT_SNAP_OPTIONS;
5
5
  exports.Gestures = require_dom.Gestures;
6
6
  exports.MemoizedGeometryProvider = require_dom.MemoizedGeometryProvider;
7
7
  exports.attach_dom_surface = require_dom.attach_dom_surface;
8
+ exports.ctm_to_container_transform = require_dom.ctm_to_container_transform;
8
9
  exports.install_font_load_geometry_bump = require_dom.install_font_load_geometry_bump;
9
10
  exports.inverse_project_rect = require_dom.inverse_project_rect;
10
11
  exports.project_delta_inverse_ctm = require_dom.project_delta_inverse_ctm;
package/dist/dom.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as project_point_through_ctm, c as MemoizedGeometryProvider, i as project_delta_inverse_ctm, l as Camera, n as install_font_load_geometry_bump, o as Gestures, r as inverse_project_rect, s as DEFAULT_SNAP_OPTIONS, t as attach_dom_surface } from "./dom-BIjCxCgx.mjs";
2
- export { Camera, DEFAULT_SNAP_OPTIONS, Gestures, MemoizedGeometryProvider, attach_dom_surface, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
1
+ import { a as project_delta_inverse_ctm, c as DEFAULT_SNAP_OPTIONS, i as inverse_project_rect, l as MemoizedGeometryProvider, n as ctm_to_container_transform, o as project_point_through_ctm, r as install_font_load_geometry_bump, s as Gestures, t as attach_dom_surface, u as Camera } from "./dom-DVxR7PNh.mjs";
2
+ export { Camera, DEFAULT_SNAP_OPTIONS, Gestures, MemoizedGeometryProvider, attach_dom_surface, ctm_to_container_transform, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
@@ -1,5 +1,5 @@
1
1
  import { c as SvgEditor } from "./editor-CcW4BVth.mjs";
2
- import { n as DomSurfaceHandle, r as DomSurfaceOptions } from "./dom-Dw2SPHgc.mjs";
2
+ import { n as DomSurfaceHandle, r as DomSurfaceOptions } from "./dom-D-ZJO9aK.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
1
  import { c as SvgEditor } from "./editor-CxqRhhzP.js";
2
- import { n as DomSurfaceHandle, r as DomSurfaceOptions } from "./dom-CQkWJNrK.js";
2
+ import { n as DomSurfaceHandle, r as DomSurfaceOptions } from "./dom-Dl56kUW5.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
2
  const require_model = require("./model-HEKGO-56.js");
3
- const require_dom = require("./dom-H4PvmPe3.js");
3
+ const require_dom = require("./dom-Disackua.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-BIjCxCgx.mjs";
2
+ import { t as attach_dom_surface } from "./dom-DVxR7PNh.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
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-CcW4BVth.mjs";
2
- import { n as DomSurfaceHandle } from "./dom-Dw2SPHgc.mjs";
2
+ import { n as DomSurfaceHandle } from "./dom-D-ZJO9aK.mjs";
3
3
  import cmath from "@grida/cmath";
4
4
  import { ReactNode } from "react";
5
5
 
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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-CxqRhhzP.js";
2
- import { n as DomSurfaceHandle } from "./dom-CQkWJNrK.js";
2
+ import { n as DomSurfaceHandle } from "./dom-Dl56kUW5.js";
3
3
  import cmath from "@grida/cmath";
4
4
  import { ReactNode } from "react";
5
5
 
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_editor = require("./editor-DFvojUwn.js");
4
- const require_dom = require("./dom-H4PvmPe3.js");
4
+ const require_dom = require("./dom-Disackua.js");
5
5
  let react = require("react");
6
6
  let react_jsx_runtime = require("react/jsx-runtime");
7
7
  //#region src/react.tsx
package/dist/react.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { t as createSvgEditor } from "./editor-DCDQl18y.mjs";
3
- import { t as attach_dom_surface } from "./dom-BIjCxCgx.mjs";
3
+ import { t as attach_dom_surface } from "./dom-DVxR7PNh.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grida/svg-editor",
3
- "version": "1.0.0-alpha.22",
3
+ "version": "1.0.0-alpha.23",
4
4
  "description": "Headless SVG editor (experimental).",
5
5
  "keywords": [
6
6
  "bezier",
@@ -61,11 +61,11 @@
61
61
  "@grida/cmath": "0.2.3",
62
62
  "@grida/color": "0.1.0",
63
63
  "@grida/history": "0.1.2",
64
- "@grida/keybinding": "0.2.1",
65
- "@grida/hud": "0.2.4",
66
- "@grida/svg": "0.2.0",
64
+ "@grida/vn": "0.1.0",
67
65
  "@grida/text-editor": "0.1.2",
68
- "@grida/vn": "0.1.0"
66
+ "@grida/hud": "0.3.0",
67
+ "@grida/keybinding": "0.2.1",
68
+ "@grida/svg": "0.2.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/react": "^19",