@grida/svg-editor 1.0.0-alpha.21 → 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 };
@@ -1,4 +1,4 @@
1
- import { S as is_text_input_focused, a as paint, c as hit_shape_svg, d as NudgeDwellWatcher, f as TranslateOrchestrator, h as transform, i as TOOL_CURSOR, l as RotateOrchestrator, m as group, n as insertions, o as ResizeOrchestrator, s as resize_pipeline, t as PathModel, w as default_nudge_handler, x as array_shallow_equal } from "./model-C6jCFK_p.mjs";
1
+ import { S as is_text_input_focused, a as paint, c as hit_shape_svg, d as NudgeDwellWatcher, f as TranslateOrchestrator, h as transform, i as TOOL_CURSOR, l as RotateOrchestrator, m as group, n as insertions, o as ResizeOrchestrator, s as resize_pipeline, t as PathModel, w as default_nudge_handler, x as array_shallow_equal } from "./model-zMPCOVAr.mjs";
2
2
  import cmath from "@grida/cmath";
3
3
  import { svg_parse } from "@grida/svg/parse";
4
4
  import { SVGShapes } from "@grida/svg/pathdata";
@@ -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();
@@ -3192,7 +3187,7 @@ var DomSurface = class DomSurface {
3192
3187
  mods: next
3193
3188
  });
3194
3189
  if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.translate_orchestrator.has_active_session()) this.translate_orchestrator.redrive_modifiers(this.current_translate_modifiers());
3195
- if (prev.shift !== next.shift && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
3190
+ if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
3196
3191
  if (prev.shift !== next.shift && this.rotate_orchestrator.has_active_session()) this.rotate_orchestrator.redrive_modifiers(this.current_rotate_modifiers());
3197
3192
  let cursor_changed = response.cursorChanged;
3198
3193
  let hover_changed = response.hoverChanged;
@@ -3690,8 +3685,10 @@ var DomSurface = class DomSurface {
3690
3685
  /** Snapshot of HUD modifier state mapped to `ResizeModifiers`. Same
3691
3686
  * pull-at-consume discipline as `current_translate_modifiers`. */
3692
3687
  current_resize_modifiers() {
3688
+ const mods = this.hud.modifiers();
3693
3689
  return {
3694
- aspect_lock: this.hud.modifiers().shift ? "uniform" : "off",
3690
+ aspect_lock: mods.shift ? "uniform" : "off",
3691
+ from_center: mods.alt,
3695
3692
  force_disable_snap: false
3696
3693
  };
3697
3694
  }
@@ -4072,7 +4069,10 @@ var DomSurface = class DomSurface {
4072
4069
  this.sync_surface_selection();
4073
4070
  this.sync_cursor();
4074
4071
  this.redraw();
4075
- 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
+ });
4076
4076
  const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
4077
4077
  let settled = false;
4078
4078
  this.text_edit = createTextEditor({
@@ -4120,11 +4120,45 @@ var DomSurface = class DomSurface {
4120
4120
  this.render();
4121
4121
  this.sync_surface_selection();
4122
4122
  this.sync_cursor();
4123
+ this.push_text_edit_chrome(null);
4123
4124
  this.redraw();
4124
4125
  this.text_edit = null;
4125
4126
  this.text_edit_target = null;
4126
4127
  }
4127
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
+ /**
4128
4162
  * Realize the result of a text content-edit session and exit. `result`
4129
4163
  * is the text that should remain — the typed text on commit, the original
4130
4164
  * on cancel. Implements the empty-equals-delete rule (design:
@@ -4943,6 +4977,25 @@ function project_point_through_ctm(px, py, ctm, container_offset) {
4943
4977
  return [sx + container_offset[0], sy + container_offset[1]];
4944
4978
  }
4945
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
+ /**
4946
4999
  * Inverse of the CTM's linear part applied to a delta vector. Drops
4947
5000
  * translation. Used to convert a HUD-reported container-space drag delta
4948
5001
  * back to the path's local coord space for `PathModel.translateVertices`.
@@ -5218,4 +5271,4 @@ var SvgHitShapeDriver = class {
5218
5271
  }
5219
5272
  };
5220
5273
  //#endregion
5221
- 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 };
@@ -1,4 +1,4 @@
1
- const require_model = require("./model-DVwjrVYp.js");
1
+ const require_model = require("./model-HEKGO-56.js");
2
2
  let _grida_cmath = require("@grida/cmath");
3
3
  _grida_cmath = require_model.__toESM(_grida_cmath);
4
4
  let _grida_svg_parse = require("@grida/svg/parse");
@@ -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();
@@ -3194,7 +3189,7 @@ var DomSurface = class DomSurface {
3194
3189
  mods: next
3195
3190
  });
3196
3191
  if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.translate_orchestrator.has_active_session()) this.translate_orchestrator.redrive_modifiers(this.current_translate_modifiers());
3197
- if (prev.shift !== next.shift && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
3192
+ if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
3198
3193
  if (prev.shift !== next.shift && this.rotate_orchestrator.has_active_session()) this.rotate_orchestrator.redrive_modifiers(this.current_rotate_modifiers());
3199
3194
  let cursor_changed = response.cursorChanged;
3200
3195
  let hover_changed = response.hoverChanged;
@@ -3692,8 +3687,10 @@ var DomSurface = class DomSurface {
3692
3687
  /** Snapshot of HUD modifier state mapped to `ResizeModifiers`. Same
3693
3688
  * pull-at-consume discipline as `current_translate_modifiers`. */
3694
3689
  current_resize_modifiers() {
3690
+ const mods = this.hud.modifiers();
3695
3691
  return {
3696
- aspect_lock: this.hud.modifiers().shift ? "uniform" : "off",
3692
+ aspect_lock: mods.shift ? "uniform" : "off",
3693
+ from_center: mods.alt,
3697
3694
  force_disable_snap: false
3698
3695
  };
3699
3696
  }
@@ -4074,7 +4071,10 @@ var DomSurface = class DomSurface {
4074
4071
  this.sync_surface_selection();
4075
4072
  this.sync_cursor();
4076
4073
  this.redraw();
4077
- 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
+ });
4078
4078
  const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
4079
4079
  let settled = false;
4080
4080
  this.text_edit = (0, _grida_text_editor_dom.createTextEditor)({
@@ -4122,11 +4122,45 @@ var DomSurface = class DomSurface {
4122
4122
  this.render();
4123
4123
  this.sync_surface_selection();
4124
4124
  this.sync_cursor();
4125
+ this.push_text_edit_chrome(null);
4125
4126
  this.redraw();
4126
4127
  this.text_edit = null;
4127
4128
  this.text_edit_target = null;
4128
4129
  }
4129
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
+ /**
4130
4164
  * Realize the result of a text content-edit session and exit. `result`
4131
4165
  * is the text that should remain — the typed text on commit, the original
4132
4166
  * on cancel. Implements the empty-equals-delete rule (design:
@@ -4945,6 +4979,25 @@ function project_point_through_ctm(px, py, ctm, container_offset) {
4945
4979
  return [sx + container_offset[0], sy + container_offset[1]];
4946
4980
  }
4947
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
+ /**
4948
5001
  * Inverse of the CTM's linear part applied to a delta vector. Drops
4949
5002
  * translation. Used to convert a HUD-reported container-space drag delta
4950
5003
  * back to the path's local coord space for `PathModel.translateVertices`.
@@ -5250,6 +5303,12 @@ Object.defineProperty(exports, "attach_dom_surface", {
5250
5303
  return attach_dom_surface;
5251
5304
  }
5252
5305
  });
5306
+ Object.defineProperty(exports, "ctm_to_container_transform", {
5307
+ enumerable: true,
5308
+ get: function() {
5309
+ return ctm_to_container_transform;
5310
+ }
5311
+ });
5253
5312
  Object.defineProperty(exports, "install_font_load_geometry_bump", {
5254
5313
  enumerable: true,
5255
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-CuK0LFUY.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-DHaTIObb.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,4 +1,4 @@
1
- import { C as TOOL_SET, S as is_text_input_focused, T as registerDefaultCommands, _ as SVG_NS, a as paint, b as XMLNS_NS, g as subtree, h as transform, i as TOOL_CURSOR, m as group, n as insertions, p as translate_pipeline, r as DEFAULT_STYLE, s as resize_pipeline, u as rotate_pipeline, v as SvgDocument, x as array_shallow_equal, y as WELL_KNOWN_NS_PREFIXES } from "./model-C6jCFK_p.mjs";
1
+ import { C as TOOL_SET, S as is_text_input_focused, T as registerDefaultCommands, _ as SVG_NS, a as paint, b as XMLNS_NS, g as subtree, h as transform, i as TOOL_CURSOR, m as group, n as insertions, p as translate_pipeline, r as DEFAULT_STYLE, s as resize_pipeline, u as rotate_pipeline, v as SvgDocument, x as array_shallow_equal, y as WELL_KNOWN_NS_PREFIXES } from "./model-zMPCOVAr.mjs";
2
2
  import { HistoryImpl } from "@grida/history";
3
3
  import { KeyCode, M, chunkKey, eventToChunk, getKeyboardOS, kb, keybindingsToKeyCodes } from "@grida/keybinding";
4
4
  import cmath from "@grida/cmath";
@@ -1,4 +1,4 @@
1
- const require_model = require("./model-DVwjrVYp.js");
1
+ const require_model = require("./model-HEKGO-56.js");
2
2
  let _grida_history = require("@grida/history");
3
3
  let _grida_keybinding = require("@grida/keybinding");
4
4
  let _grida_cmath = require("@grida/cmath");
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_model = require("./model-DVwjrVYp.js");
3
- const require_editor = require("./editor-BlByfVyF.js");
2
+ const require_model = require("./model-HEKGO-56.js");
3
+ const require_editor = require("./editor-DFvojUwn.js");
4
4
  exports.DEFAULT_STYLE = require_model.DEFAULT_STYLE;
5
5
  exports.PathModel = require_model.PathModel;
6
6
  exports.TOOL_CURSOR = require_model.TOOL_CURSOR;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as createSvgEditor } from "./editor-CJ3ROm0G.mjs";
2
- import { i as TOOL_CURSOR, r as DEFAULT_STYLE, t as PathModel } from "./model-C6jCFK_p.mjs";
1
+ import { t as createSvgEditor } from "./editor-DCDQl18y.mjs";
2
+ import { i as TOOL_CURSOR, r as DEFAULT_STYLE, t as PathModel } from "./model-zMPCOVAr.mjs";
3
3
  export { DEFAULT_STYLE, PathModel, TOOL_CURSOR, createSvgEditor };
@@ -3518,9 +3518,12 @@ let resize_capability;
3518
3518
  }
3519
3519
  }
3520
3520
  _resize_capability.origin_of_direction = origin_of_direction;
3521
- function effective(baseline, dir, sx_gesture, sy_gesture) {
3521
+ function effective(baseline, dir, sx_gesture, sy_gesture, from_center = false) {
3522
3522
  const bbox = baseline.bbox;
3523
- const origin = origin_of_direction(bbox, dir);
3523
+ const origin = from_center ? {
3524
+ x: bbox.x + bbox.width / 2,
3525
+ y: bbox.y + bbox.height / 2
3526
+ } : origin_of_direction(bbox, dir);
3524
3527
  const c = constraint(baseline, dir, sx_gesture, sy_gesture);
3525
3528
  const mask = direction_mask(dir);
3526
3529
  const rect = {
@@ -3678,7 +3681,7 @@ let resize_pipeline;
3678
3681
  for (const a of baseline.raw) doc.set_attr(id, a.name, a.value);
3679
3682
  }
3680
3683
  _intent.restore = restore;
3681
- function compute_factors(baseline, dir, dx, dy, shift) {
3684
+ function compute_factors(baseline, dir, dx, dy, shift, from_center = false) {
3682
3685
  const b = baseline.bbox;
3683
3686
  let anchorX = 0;
3684
3687
  let anchorY = 0;
@@ -3740,6 +3743,10 @@ let resize_pipeline;
3740
3743
  affectsY = false;
3741
3744
  break;
3742
3745
  }
3746
+ if (from_center) {
3747
+ anchorX = b.x + b.width / 2;
3748
+ anchorY = b.y + b.height / 2;
3749
+ }
3743
3750
  const newHX = baseHX + (affectsX ? dx : 0);
3744
3751
  const newHY = baseHY + (affectsY ? dy : 0);
3745
3752
  const denomX = baseHX - anchorX;
@@ -3962,7 +3969,7 @@ let resize_pipeline;
3962
3969
  if (ctx.modifiers.aspect_lock !== "uniform") return { plan };
3963
3970
  if (!resize_capability.is_corner(plan.direction)) return { plan };
3964
3971
  const pbase = pipeline_baseline(plan);
3965
- const locked = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, true);
3972
+ const locked = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, true, ctx.modifiers.from_center);
3966
3973
  const bbox = pbase.bbox;
3967
3974
  const Hx_base = corner_x_of(bbox, plan.direction);
3968
3975
  const Hy_base = corner_y_of(bbox, plan.direction);
@@ -3982,8 +3989,8 @@ let resize_pipeline;
3982
3989
  if (!ctx.snap_session) return { plan };
3983
3990
  if (!ctx.options.snap_enabled) return { plan };
3984
3991
  const pbase = pipeline_baseline(plan);
3985
- const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
3986
- const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
3992
+ const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false, ctx.modifiers.from_center);
3993
+ const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy, ctx.modifiers.from_center);
3987
3994
  if (eff.no_op) return { plan };
3988
3995
  const r = ctx.snap_session.snap_resize(eff.rect, {
3989
3996
  x: eff.mask.x_edge,
@@ -4035,8 +4042,8 @@ let resize_pipeline;
4035
4042
  const q = ctx.options.pixel_grid_quantum;
4036
4043
  if (q === null || q <= 0) return { plan };
4037
4044
  const pbase = pipeline_baseline(plan);
4038
- const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
4039
- const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
4045
+ const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false, ctx.modifiers.from_center);
4046
+ const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy, ctx.modifiers.from_center);
4040
4047
  if (eff.no_op) return { plan };
4041
4048
  const target_Hx = eff.mask.affects_x ? Math.round(eff.moving_corner.x / q) * q : eff.moving_corner.x;
4042
4049
  const target_Hy = eff.mask.affects_y ? Math.round(eff.moving_corner.y / q) * q : eff.moving_corner.y;
@@ -4071,7 +4078,7 @@ let resize_pipeline;
4071
4078
  }
4072
4079
  _resize_pipeline.run = run;
4073
4080
  function apply(doc, plan, phase = "commit") {
4074
- const f = intent.compute_factors(plan.baseline, plan.direction, plan.dx, plan.dy, false);
4081
+ const f = intent.compute_factors(plan.baseline, plan.direction, plan.dx, plan.dy, false, plan.from_center ?? false);
4075
4082
  const members = plan.members ?? [{
4076
4083
  id: plan.id,
4077
4084
  baseline: plan.baseline
@@ -4196,7 +4203,8 @@ var ResizeOrchestrator = class {
4196
4203
  members: session.members,
4197
4204
  direction: session.direction,
4198
4205
  dx,
4199
- dy
4206
+ dy,
4207
+ from_center: modifiers.from_center
4200
4208
  };
4201
4209
  const ctx = {
4202
4210
  input: {
@@ -3484,9 +3484,12 @@ let resize_capability;
3484
3484
  }
3485
3485
  }
3486
3486
  _resize_capability.origin_of_direction = origin_of_direction;
3487
- function effective(baseline, dir, sx_gesture, sy_gesture) {
3487
+ function effective(baseline, dir, sx_gesture, sy_gesture, from_center = false) {
3488
3488
  const bbox = baseline.bbox;
3489
- const origin = origin_of_direction(bbox, dir);
3489
+ const origin = from_center ? {
3490
+ x: bbox.x + bbox.width / 2,
3491
+ y: bbox.y + bbox.height / 2
3492
+ } : origin_of_direction(bbox, dir);
3490
3493
  const c = constraint(baseline, dir, sx_gesture, sy_gesture);
3491
3494
  const mask = direction_mask(dir);
3492
3495
  const rect = {
@@ -3644,7 +3647,7 @@ let resize_pipeline;
3644
3647
  for (const a of baseline.raw) doc.set_attr(id, a.name, a.value);
3645
3648
  }
3646
3649
  _intent.restore = restore;
3647
- function compute_factors(baseline, dir, dx, dy, shift) {
3650
+ function compute_factors(baseline, dir, dx, dy, shift, from_center = false) {
3648
3651
  const b = baseline.bbox;
3649
3652
  let anchorX = 0;
3650
3653
  let anchorY = 0;
@@ -3706,6 +3709,10 @@ let resize_pipeline;
3706
3709
  affectsY = false;
3707
3710
  break;
3708
3711
  }
3712
+ if (from_center) {
3713
+ anchorX = b.x + b.width / 2;
3714
+ anchorY = b.y + b.height / 2;
3715
+ }
3709
3716
  const newHX = baseHX + (affectsX ? dx : 0);
3710
3717
  const newHY = baseHY + (affectsY ? dy : 0);
3711
3718
  const denomX = baseHX - anchorX;
@@ -3928,7 +3935,7 @@ let resize_pipeline;
3928
3935
  if (ctx.modifiers.aspect_lock !== "uniform") return { plan };
3929
3936
  if (!resize_capability.is_corner(plan.direction)) return { plan };
3930
3937
  const pbase = pipeline_baseline(plan);
3931
- const locked = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, true);
3938
+ const locked = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, true, ctx.modifiers.from_center);
3932
3939
  const bbox = pbase.bbox;
3933
3940
  const Hx_base = corner_x_of(bbox, plan.direction);
3934
3941
  const Hy_base = corner_y_of(bbox, plan.direction);
@@ -3948,8 +3955,8 @@ let resize_pipeline;
3948
3955
  if (!ctx.snap_session) return { plan };
3949
3956
  if (!ctx.options.snap_enabled) return { plan };
3950
3957
  const pbase = pipeline_baseline(plan);
3951
- const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
3952
- const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
3958
+ const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false, ctx.modifiers.from_center);
3959
+ const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy, ctx.modifiers.from_center);
3953
3960
  if (eff.no_op) return { plan };
3954
3961
  const r = ctx.snap_session.snap_resize(eff.rect, {
3955
3962
  x: eff.mask.x_edge,
@@ -4001,8 +4008,8 @@ let resize_pipeline;
4001
4008
  const q = ctx.options.pixel_grid_quantum;
4002
4009
  if (q === null || q <= 0) return { plan };
4003
4010
  const pbase = pipeline_baseline(plan);
4004
- const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false);
4005
- const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy);
4011
+ const f = intent.compute_factors(pbase, plan.direction, plan.dx, plan.dy, false, ctx.modifiers.from_center);
4012
+ const eff = resize_capability.effective(pbase, plan.direction, f.sx, f.sy, ctx.modifiers.from_center);
4006
4013
  if (eff.no_op) return { plan };
4007
4014
  const target_Hx = eff.mask.affects_x ? Math.round(eff.moving_corner.x / q) * q : eff.moving_corner.x;
4008
4015
  const target_Hy = eff.mask.affects_y ? Math.round(eff.moving_corner.y / q) * q : eff.moving_corner.y;
@@ -4037,7 +4044,7 @@ let resize_pipeline;
4037
4044
  }
4038
4045
  _resize_pipeline.run = run;
4039
4046
  function apply(doc, plan, phase = "commit") {
4040
- const f = intent.compute_factors(plan.baseline, plan.direction, plan.dx, plan.dy, false);
4047
+ const f = intent.compute_factors(plan.baseline, plan.direction, plan.dx, plan.dy, false, plan.from_center ?? false);
4041
4048
  const members = plan.members ?? [{
4042
4049
  id: plan.id,
4043
4050
  baseline: plan.baseline
@@ -4162,7 +4169,8 @@ var ResizeOrchestrator = class {
4162
4169
  members: session.members,
4163
4170
  direction: session.direction,
4164
4171
  dx,
4165
- dy
4172
+ dy,
4173
+ from_center: modifiers.from_center
4166
4174
  };
4167
4175
  const ctx = {
4168
4176
  input: {
@@ -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
- const require_model = require("./model-DVwjrVYp.js");
3
- const require_dom = require("./dom-CuK0LFUY.js");
2
+ const require_model = require("./model-HEKGO-56.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-DHaTIObb.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
- const require_editor = require("./editor-BlByfVyF.js");
4
- const require_dom = require("./dom-CuK0LFUY.js");
3
+ const require_editor = require("./editor-DFvojUwn.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
- import { t as createSvgEditor } from "./editor-CJ3ROm0G.mjs";
3
- import { t as attach_dom_surface } from "./dom-DHaTIObb.mjs";
2
+ import { t as createSvgEditor } from "./editor-DCDQl18y.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.21",
3
+ "version": "1.0.0-alpha.23",
4
4
  "description": "Headless SVG editor (experimental).",
5
5
  "keywords": [
6
6
  "bezier",
@@ -59,13 +59,13 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@grida/cmath": "0.2.3",
62
- "@grida/keybinding": "0.2.1",
62
+ "@grida/color": "0.1.0",
63
63
  "@grida/history": "0.1.2",
64
- "@grida/hud": "0.2.3",
65
64
  "@grida/vn": "0.1.0",
66
- "@grida/svg": "0.2.0",
67
65
  "@grida/text-editor": "0.1.2",
68
- "@grida/color": "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",