@grida/svg-editor 1.0.0-alpha.15 → 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.
@@ -1,4 +1,4 @@
1
- const require_model = require("./model-CJ1Ctq14.js");
1
+ const require_model = require("./model-GpysNbOv.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");
@@ -1007,15 +1007,18 @@ function create_attention_tracker(container) {
1007
1007
  };
1008
1008
  container.addEventListener("pointerenter", on_enter);
1009
1009
  container.addEventListener("pointerleave", on_leave);
1010
- const is_attended = () => {
1010
+ const is_focus_within = () => {
1011
1011
  const owner = container.ownerDocument;
1012
- if (!owner) return pointer_over;
1012
+ if (!owner) return false;
1013
1013
  const active = owner.activeElement;
1014
- if (active && active !== owner.body && container.contains(active)) return true;
1015
- return pointer_over;
1014
+ return !!active && active !== owner.body && container.contains(active);
1015
+ };
1016
+ const is_attended = () => {
1017
+ return is_focus_within() || pointer_over;
1016
1018
  };
1017
1019
  return {
1018
1020
  is_attended,
1021
+ is_focus_within,
1019
1022
  dispose: () => {
1020
1023
  container.removeEventListener("pointerenter", on_enter);
1021
1024
  container.removeEventListener("pointerleave", on_leave);
@@ -1640,6 +1643,48 @@ const IS_MODIFIER_KEY = {
1640
1643
  * live `<text>` element out from under the about-to-mount text surface. */
1641
1644
  const TEXT_EDIT_PENDING = { __pending: true };
1642
1645
  /**
1646
+ * Wire a web-font settle source to the editor's geometry channel.
1647
+ *
1648
+ * The DOM surface re-serializes the `<svg>` on every editor tick, but a
1649
+ * `<text>` / `<tspan>` bbox can change with NO attribute write: a web font
1650
+ * finishing load AFTER its `font-family` / `font-size` was already written.
1651
+ * The IR never sees that reflow, so nothing bumps `geometry_version` and
1652
+ * every bounds-keyed consumer (snap, HUD chrome, size meter) stays stuck at
1653
+ * the fallback-face metrics until the next real edit.
1654
+ *
1655
+ * Listens for `loadingdone` on `source` (a `FontFaceSet`, or any injected
1656
+ * `EventTarget` in tests) and calls `bump` once per settle. COARSE on
1657
+ * purpose: one bump clears the WHOLE bounds cache, not just text nodes —
1658
+ * consistent with the package's pessimistic-invalidation stance, and far
1659
+ * cheaper than scoping the bump to the (possibly many) reflowed runs.
1660
+ *
1661
+ * Also bumps once when `source.ready` resolves (when present): fonts that
1662
+ * settled before attach — a cache hit, or `font-display` resolving the same
1663
+ * tick the surface mounts — never re-fire `loadingdone`, so a document
1664
+ * mounted post-settle still needs one bump to re-read at the real metrics.
1665
+ *
1666
+ * Returns a teardown that removes the listener and neutralizes the pending
1667
+ * `ready` bump (leak guard) — call it on surface detach.
1668
+ *
1669
+ * Factored out of the surface so it can be unit-tested with a fake
1670
+ * `EventTarget` in the node-only test env (jsdom's `FontFaceSet` is
1671
+ * incomplete); never a real font / network.
1672
+ */
1673
+ function install_font_load_geometry_bump(source, bump) {
1674
+ if (!source) return () => {};
1675
+ const on_fonts_settled = () => bump();
1676
+ source.addEventListener("loadingdone", on_fonts_settled);
1677
+ let alive = true;
1678
+ const ready = source.ready;
1679
+ if (ready && typeof ready.then === "function") ready.then(() => {
1680
+ if (alive) bump();
1681
+ });
1682
+ return () => {
1683
+ alive = false;
1684
+ source.removeEventListener("loadingdone", on_fonts_settled);
1685
+ };
1686
+ }
1687
+ /**
1643
1688
  * Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
1644
1689
  * whose `detach()` is the inverse — DOM cleared, listeners removed,
1645
1690
  * gestures uninstalled.
@@ -1665,6 +1710,7 @@ var DomSurface = class DomSurface {
1665
1710
  this.svg_root = null;
1666
1711
  this.teardown = [];
1667
1712
  this.element_index = /* @__PURE__ */ new Map();
1713
+ this.rendered_doc_revision = -1;
1668
1714
  this.last_pointer = {
1669
1715
  x: 0,
1670
1716
  y: 0
@@ -1689,6 +1735,7 @@ var DomSurface = class DomSurface {
1689
1735
  this.container = options.container;
1690
1736
  const container = this.container;
1691
1737
  this.fit_on_attach = options.fit === true;
1738
+ this.clipboard_enabled = options.clipboard !== false;
1692
1739
  this.attention = create_attention_tracker(container);
1693
1740
  this.teardown.push(() => this.attention.dispose());
1694
1741
  if (process.env.NODE_ENV !== "production" && container.children.length > 0) console.warn("@grida/svg-editor: surface container is not empty at attach time. Render chrome (toolbars, layer lists, inspectors) as siblings of the container, not children — otherwise clicks on those children will silently break. See README §Surface.");
@@ -1696,6 +1743,8 @@ var DomSurface = class DomSurface {
1696
1743
  container.style.overflow = "hidden";
1697
1744
  container.style.userSelect = "none";
1698
1745
  container.style.webkitUserSelect = "none";
1746
+ container.tabIndex = -1;
1747
+ container.style.outline = "none";
1699
1748
  const translate_options = () => {
1700
1749
  const style = this.editor.style;
1701
1750
  const zoom = this.camera.zoom || 1;
@@ -1711,7 +1760,9 @@ var DomSurface = class DomSurface {
1711
1760
  open_preview: (label) => this.editor_internal().history.preview(label),
1712
1761
  open_snap: (ids) => this.open_snap_session_for(ids),
1713
1762
  options: translate_options,
1714
- project_delta: (id, d) => this._geometry_provider?.world_delta_to_local(id, d) ?? d
1763
+ project_delta: (id, d) => this._geometry_provider?.world_delta_to_local(id, d) ?? d,
1764
+ set_selection: (ids) => this.editor.commands.select(ids),
1765
+ on_clone_commit: (record) => this.editor_internal().seed_duplication(record)
1715
1766
  });
1716
1767
  const resize_options = () => {
1717
1768
  const style = this.editor.style;
@@ -1779,6 +1830,7 @@ var DomSurface = class DomSurface {
1779
1830
  shapeOf: (id) => this.shape_of(id),
1780
1831
  vectorOf: (id) => this.vector_of(id),
1781
1832
  onIntent: (i) => this.commit_intent(i),
1833
+ onTap: (t) => this.handle_tap(t),
1782
1834
  style: {
1783
1835
  chromeColor: editor.style.chrome_color,
1784
1836
  showRotationHandles: true
@@ -1838,7 +1890,7 @@ var DomSurface = class DomSurface {
1838
1890
  this.current_tool = editor.state.tool;
1839
1891
  this.hud.setVectorSelectionMode(this.current_tool.type === "lasso" ? "lasso" : "marquee");
1840
1892
  this.hud.setVectorBendMode(this.current_tool.type === "bend" ? "always" : "auto");
1841
- this.render();
1893
+ this.flush_dom();
1842
1894
  this.sync_surface_selection();
1843
1895
  this.hud.setPixelGrid({
1844
1896
  enabled: editor.style.pixel_grid,
@@ -1885,6 +1937,8 @@ var DomSurface = class DomSurface {
1885
1937
  win.addEventListener("resize", fn);
1886
1938
  this.teardown.push(() => win.removeEventListener("resize", fn));
1887
1939
  }
1940
+ const detach_font_listener = install_font_load_geometry_bump(options.font_load_source ?? container.ownerDocument.fonts ?? null, () => editor._internal.bump_geometry());
1941
+ this.teardown.push(detach_font_listener);
1888
1942
  this.wire_events();
1889
1943
  const internal = editor._internal;
1890
1944
  this.editor_hover_internal = internal;
@@ -1892,12 +1946,14 @@ var DomSurface = class DomSurface {
1892
1946
  this.teardown.push(() => internal.set_content_edit_driver(null));
1893
1947
  internal.set_computed_resolver({
1894
1948
  computed_property: (id, name) => {
1949
+ this.flush_dom();
1895
1950
  const el = this.element_index.get(id);
1896
1951
  if (!el) return null;
1897
1952
  const value = getComputedStyle(el).getPropertyValue(name);
1898
1953
  return value === "" ? null : value;
1899
1954
  },
1900
1955
  computed_paint: (id, channel) => {
1956
+ this.flush_dom();
1901
1957
  const el = this.element_index.get(id);
1902
1958
  if (!el) return null;
1903
1959
  const computed = getComputedStyle(el).getPropertyValue(channel);
@@ -1914,7 +1970,8 @@ var DomSurface = class DomSurface {
1914
1970
  root: () => this.svg_root,
1915
1971
  camera: () => this.camera,
1916
1972
  container: () => this.container,
1917
- pick_at_world: (p, allow_root) => this._pick_node_at_world(p, allow_root)
1973
+ pick_at_world: (p, allow_root) => this._pick_node_at_world(p, allow_root),
1974
+ flush: () => this.flush_dom()
1918
1975
  }), {
1919
1976
  subscribe_structure: (cb) => editor.subscribe_with_selector((s) => s.structure_version, () => cb()),
1920
1977
  subscribe_geometry: (cb) => editor.subscribe_geometry(cb)
@@ -2096,6 +2153,25 @@ var DomSurface = class DomSurface {
2096
2153
  detach_gestures() {
2097
2154
  this.gestures._dispose();
2098
2155
  }
2156
+ /**
2157
+ * Bring the live DOM up to date with the doc IR iff it is stale.
2158
+ *
2159
+ * Staleness contract: anything that reads the LIVE DOM as a proxy for
2160
+ * document state — the geometry driver (`getBBox` / `getCTM`), the
2161
+ * computed-style resolver — MUST call this first. Doc listeners (the
2162
+ * geometry channel, editor `subscribe`) fire synchronously inside the
2163
+ * mutation, BEFORE the surface's render listener has projected the new
2164
+ * attrs into the DOM; a read in that window returns the PREVIOUS
2165
+ * geometry, and through `MemoizedGeometryProvider` it would be cached as
2166
+ * if current — every later consumer (align, resize_to, snap) then plans
2167
+ * against one-mutation-stale bounds. Same model as CSS layout: reading
2168
+ * `offsetWidth` flushes pending layout; reading `bounds_of` flushes the
2169
+ * pending render.
2170
+ */
2171
+ flush_dom() {
2172
+ if (this.rendered_doc_revision === this.editor._internal.doc.revision) return;
2173
+ this.render();
2174
+ }
2099
2175
  render() {
2100
2176
  if (this.text_edit) return;
2101
2177
  const owner_doc = this.container.ownerDocument;
@@ -2122,6 +2198,7 @@ var DomSurface = class DomSurface {
2122
2198
  for (let c = el.firstElementChild; c; c = c.nextElementSibling) if (c instanceof SVGElement) tag_walk(c);
2123
2199
  };
2124
2200
  tag_walk(new_svg);
2201
+ this.rendered_doc_revision = doc.revision;
2125
2202
  }
2126
2203
  sync_canvas_size() {
2127
2204
  const cr = this.container.getBoundingClientRect();
@@ -2716,6 +2793,60 @@ var DomSurface = class DomSurface {
2716
2793
  });
2717
2794
  on(win, "blur", () => this.sync_modifiers(null));
2718
2795
  on(this.container, "contextmenu", (e) => e.preventDefault());
2796
+ if (this.clipboard_enabled) {
2797
+ on(owner_doc, "copy", (e) => this.on_copy_or_cut(e, "copy"));
2798
+ on(owner_doc, "cut", (e) => this.on_copy_or_cut(e, "cut"));
2799
+ on(owner_doc, "paste", (e) => this.on_paste(e));
2800
+ }
2801
+ }
2802
+ /**
2803
+ * Gate for claiming a native clipboard gesture. Deliberately STRICTER
2804
+ * than the keyboard attention gate: focus-based only — pointer-over is
2805
+ * a sufficient signal for a keystroke (worst case: a stolen scroll) but
2806
+ * not for clipboard (worst case: destroying what the user believed they
2807
+ * copied, or routing a paste meant for a host text field into the
2808
+ * document). A user with text selected in a sibling panel and the
2809
+ * pointer idly over the canvas must get their text copy.
2810
+ */
2811
+ claims_clipboard(kind) {
2812
+ if (this.text_edit) return false;
2813
+ if (this.editor.state.mode !== "select") return false;
2814
+ if (!this.attention.is_focus_within()) return false;
2815
+ if (require_model.is_text_input_focused()) return false;
2816
+ if (kind !== "paste") {
2817
+ const sel = this.container.ownerDocument.getSelection();
2818
+ if (sel && !sel.isCollapsed) return false;
2819
+ }
2820
+ return true;
2821
+ }
2822
+ /**
2823
+ * Act-then-claim: an empty selection returns without `preventDefault()`,
2824
+ * leaving the browser default (and the OS clipboard) untouched. The
2825
+ * buffer-only `_internal.clipboard` variants are used here — the event's
2826
+ * DataTransfer is this gesture's ONE external channel (the public
2827
+ * commands would additionally write the provider; one gesture, one
2828
+ * external write — FRD §Transport).
2829
+ */
2830
+ on_copy_or_cut(e, kind) {
2831
+ if (!this.claims_clipboard(kind)) return;
2832
+ if (!e.clipboardData) return;
2833
+ const internal = this.editor_internal();
2834
+ const payload = kind === "copy" ? internal.clipboard.copy() : internal.clipboard.cut();
2835
+ if (payload === null) return;
2836
+ e.clipboardData.setData("text/plain", payload);
2837
+ e.preventDefault();
2838
+ }
2839
+ /**
2840
+ * Claim-then-act (mirrors the keydown claim doctrine: swallow when the
2841
+ * gesture is aimed at the editor, not just when a handler consumed):
2842
+ * a refused paste — junk text — still claims; the suppressed default is
2843
+ * a no-op on a div anyway.
2844
+ */
2845
+ on_paste(e) {
2846
+ if (!this.claims_clipboard("paste")) return;
2847
+ e.preventDefault();
2848
+ const text = e.clipboardData?.getData("text/plain");
2849
+ if (text) this.editor.commands.paste(text);
2719
2850
  }
2720
2851
  /**
2721
2852
  * Master signal for modifier-driven UX consumers (measurement, future
@@ -2737,7 +2868,7 @@ var DomSurface = class DomSurface {
2737
2868
  kind: "modifiers",
2738
2869
  mods: next
2739
2870
  });
2740
- if (prev.shift !== next.shift && this.translate_orchestrator.has_active_session()) this.translate_orchestrator.redrive_modifiers(this.current_translate_modifiers());
2871
+ if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.translate_orchestrator.has_active_session()) this.translate_orchestrator.redrive_modifiers(this.current_translate_modifiers());
2741
2872
  if (prev.shift !== next.shift && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
2742
2873
  if (prev.shift !== next.shift && this.rotate_orchestrator.has_active_session()) this.rotate_orchestrator.redrive_modifiers(this.current_rotate_modifiers());
2743
2874
  this.redraw();
@@ -2758,6 +2889,7 @@ var DomSurface = class DomSurface {
2758
2889
  } else if (kind === "pointer_up") this.text_edit.pointerUp();
2759
2890
  return;
2760
2891
  }
2892
+ if (kind === "pointer_down") this.container.focus({ preventScroll: true });
2761
2893
  const cr = this.container.getBoundingClientRect();
2762
2894
  const x = e.clientX - cr.left;
2763
2895
  const y = e.clientY - cr.top;
@@ -2968,6 +3100,26 @@ var DomSurface = class DomSurface {
2968
3100
  if (this.editor.keymap.claims(e)) e.preventDefault();
2969
3101
  this.editor.keymap.dispatch(e);
2970
3102
  }
3103
+ /**
3104
+ * Re-express a HUD tap as an editor {@link PickEvent} and fan it out on the
3105
+ * editor's pick channel. The HUD already resolved everything that matters —
3106
+ * the pointer-down point, the hit node, and click-vs-drag — so this is a
3107
+ * pure translation (HUD `[x, y]` tuple → editor `{ x, y }` doc-space point)
3108
+ * with NO re-hit-testing. Taking the hit from the HUD (not a fresh
3109
+ * `node_at_point`) guarantees the pick and the selection it accompanies can
3110
+ * never disagree. Observe-only: this mutates no editor state.
3111
+ */
3112
+ handle_tap(tap) {
3113
+ this.editor._internal.push_pick({
3114
+ point: {
3115
+ x: tap.point[0],
3116
+ y: tap.point[1]
3117
+ },
3118
+ node_id: tap.hit,
3119
+ button: tap.button,
3120
+ mods: tap.mods
3121
+ });
3122
+ }
2971
3123
  commit_intent(intent) {
2972
3124
  switch (intent.kind) {
2973
3125
  case "select":
@@ -3054,13 +3206,15 @@ var DomSurface = class DomSurface {
3054
3206
  });
3055
3207
  if (intent.phase === "commit") this.request_redraw();
3056
3208
  }
3057
- /** Snapshot of HUD modifier state mapped to pipeline `TranslateModifiers`.
3209
+ /** Snapshot of HUD modifier state mapped to the orchestrator's `GestureModifiers`.
3058
3210
  * Pull-at-consume: HUD is the canonical store (see `sync_modifiers`),
3059
3211
  * read live so mid-drag Shift press/release reflects on the next pass. */
3060
3212
  current_translate_modifiers() {
3213
+ const mods = this.hud.modifiers();
3061
3214
  return {
3062
- axis_lock: this.hud.modifiers().shift ? "by_dominance" : "off",
3063
- force_disable_snap: false
3215
+ axis_lock: mods.shift ? "by_dominance" : "off",
3216
+ force_disable_snap: false,
3217
+ clone: mods.alt
3064
3218
  };
3065
3219
  }
3066
3220
  /** Snapshot of HUD modifier state mapped to `ResizeModifiers`. Same
@@ -4399,6 +4553,7 @@ var SvgGeometryDriver = class {
4399
4553
  this.accessors = accessors;
4400
4554
  }
4401
4555
  bounds_of(id) {
4556
+ this.accessors.flush();
4402
4557
  const el = this.accessors.element_for(id);
4403
4558
  if (!el) return null;
4404
4559
  if (el instanceof SVGSVGElement) return svg_viewport_bounds(el);
@@ -4450,6 +4605,7 @@ var SvgGeometryDriver = class {
4450
4605
  return out;
4451
4606
  }
4452
4607
  nodes_in_rect(rect) {
4608
+ this.accessors.flush();
4453
4609
  const root = this.accessors.root();
4454
4610
  if (!root) return [];
4455
4611
  const hits = [];
@@ -4462,6 +4618,7 @@ var SvgGeometryDriver = class {
4462
4618
  return hits;
4463
4619
  }
4464
4620
  node_at_point(p) {
4621
+ this.accessors.flush();
4465
4622
  return this.accessors.pick_at_world(p, true);
4466
4623
  }
4467
4624
  /** World→local delta projection. The frame an element's position is
@@ -4477,6 +4634,7 @@ var SvgGeometryDriver = class {
4477
4634
  * the local delta. Identity (→ delta unchanged) for flat frames,
4478
4635
  * top-level nodes, and any degenerate / unavailable matrix. */
4479
4636
  world_delta_to_local(id, delta) {
4637
+ this.accessors.flush();
4480
4638
  const parent = this.accessors.element_for(id)?.parentNode;
4481
4639
  const root = this.accessors.root();
4482
4640
  if (!(parent instanceof SVGGraphicsElement) || !root) return delta;
@@ -4547,6 +4705,12 @@ Object.defineProperty(exports, "attach_dom_surface", {
4547
4705
  return attach_dom_surface;
4548
4706
  }
4549
4707
  });
4708
+ Object.defineProperty(exports, "install_font_load_geometry_bump", {
4709
+ enumerable: true,
4710
+ get: function() {
4711
+ return install_font_load_geometry_bump;
4712
+ }
4713
+ });
4550
4714
  Object.defineProperty(exports, "inverse_project_rect", {
4551
4715
  enumerable: true,
4552
4716
  get: function() {
@@ -1,4 +1,4 @@
1
- import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-Dl7c0q5A.mjs";
1
+ import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-KqpIW1qm.mjs";
2
2
  import cmath from "@grida/cmath";
3
3
  import { guide } from "@grida/cmath/_snap";
4
4
 
@@ -13,6 +13,35 @@ type SnapOptions = {
13
13
  declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
14
14
  //#endregion
15
15
  //#region src/dom.d.ts
16
+ /**
17
+ * Wire a web-font settle source to the editor's geometry channel.
18
+ *
19
+ * The DOM surface re-serializes the `<svg>` on every editor tick, but a
20
+ * `<text>` / `<tspan>` bbox can change with NO attribute write: a web font
21
+ * finishing load AFTER its `font-family` / `font-size` was already written.
22
+ * The IR never sees that reflow, so nothing bumps `geometry_version` and
23
+ * every bounds-keyed consumer (snap, HUD chrome, size meter) stays stuck at
24
+ * the fallback-face metrics until the next real edit.
25
+ *
26
+ * Listens for `loadingdone` on `source` (a `FontFaceSet`, or any injected
27
+ * `EventTarget` in tests) and calls `bump` once per settle. COARSE on
28
+ * purpose: one bump clears the WHOLE bounds cache, not just text nodes —
29
+ * consistent with the package's pessimistic-invalidation stance, and far
30
+ * cheaper than scoping the bump to the (possibly many) reflowed runs.
31
+ *
32
+ * Also bumps once when `source.ready` resolves (when present): fonts that
33
+ * settled before attach — a cache hit, or `font-display` resolving the same
34
+ * tick the surface mounts — never re-fire `loadingdone`, so a document
35
+ * mounted post-settle still needs one bump to re-read at the real metrics.
36
+ *
37
+ * Returns a teardown that removes the listener and neutralizes the pending
38
+ * `ready` bump (leak guard) — call it on surface detach.
39
+ *
40
+ * Factored out of the surface so it can be unit-tested with a fake
41
+ * `EventTarget` in the node-only test env (jsdom's `FontFaceSet` is
42
+ * incomplete); never a real font / network.
43
+ */
44
+ declare function install_font_load_geometry_bump(source: EventTarget | null, bump: () => void): () => void;
16
45
  type DomSurfaceOptions = {
17
46
  /** Mount the SVG inside this container. */container: HTMLElement;
18
47
  /**
@@ -21,6 +50,18 @@ type DomSurfaceOptions = {
21
50
  * carte via `handle.gestures.bind(...)`.
22
51
  */
23
52
  gestures?: boolean;
53
+ /**
54
+ * Wire native ClipboardEvent transport — `copy` / `cut` / `paste`
55
+ * listeners on the owner document, gated by the clipboard attention
56
+ * discipline. Default `true`. Pass `false` to route ALL clipboard
57
+ * traffic through the `ClipboardProvider` seam instead (the
58
+ * configuration under which a host's paste-time screening governs
59
+ * every path) — see docs/wg/feat-svg-editor/clipboard.md §Transport
60
+ * "Host control over the native path". Focus management (the container
61
+ * focusing on pointerdown) stays either way — it is a general canvas
62
+ * mitigation, not a clipboard feature.
63
+ */
64
+ clipboard?: boolean;
24
65
  /**
25
66
  * Auto-fit the document into the viewport on initial attach. Default
26
67
  * `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
@@ -33,6 +74,19 @@ type DomSurfaceOptions = {
33
74
  * when `fit: true`.
34
75
  */
35
76
  initial_camera?: cmath.Transform;
77
+ /**
78
+ * Font-load settle source — the `EventTarget` whose `loadingdone` event
79
+ * signals "web fonts finished loading, text may have reflowed." Defaults
80
+ * to `container.ownerDocument.fonts` (the live `FontFaceSet`). The
81
+ * surface installs a `loadingdone` listener that advances the editor's
82
+ * geometry channel so text bounds re-read at the settled glyph metrics
83
+ * (see ../docs/geometry.md §Limitations "Text bbox depends on font").
84
+ *
85
+ * Injectable as a DOM seam: jsdom's `FontFaceSet` is incomplete, so
86
+ * tests pass a plain `EventTarget` stub and `dispatchEvent(new Event(
87
+ * "loadingdone"))` to simulate a settle without a real font / network.
88
+ */
89
+ font_load_source?: EventTarget;
36
90
  };
37
91
  /**
38
92
  * Surface handle for the DOM surface. Extends the editor's core
@@ -111,4 +165,4 @@ declare function inverse_project_rect(rect: {
111
165
  f: number;
112
166
  }, offset: readonly [number, number]): cmath.Rectangle | null;
113
167
  //#endregion
114
- export { project_delta_inverse_ctm as a, SnapOptions as c, inverse_project_rect as i, DomSurfaceOptions as n, project_point_through_ctm as o, attach_dom_surface as r, DEFAULT_SNAP_OPTIONS as s, DomSurfaceHandle as t };
168
+ export { inverse_project_rect as a, DEFAULT_SNAP_OPTIONS as c, install_font_load_geometry_bump as i, SnapOptions as l, DomSurfaceOptions as n, project_delta_inverse_ctm as o, attach_dom_surface as r, project_point_through_ctm as s, DomSurfaceHandle as t };
package/dist/dom.d.mts CHANGED
@@ -1,3 +1,3 @@
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-Dl7c0q5A.mjs";
2
- import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-CK6GlgFF.mjs";
3
- export { 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, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
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-KqpIW1qm.mjs";
2
+ import { a as inverse_project_rect, c as DEFAULT_SNAP_OPTIONS, i as install_font_load_geometry_bump, l as SnapOptions, n as DomSurfaceOptions, o as project_delta_inverse_ctm, r as attach_dom_surface, s as project_point_through_ctm, t as DomSurfaceHandle } from "./dom-TctdgRnn.mjs";
3
+ export { 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 };
package/dist/dom.d.ts CHANGED
@@ -1,3 +1,3 @@
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-BKoo9SPL.js";
2
- import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-CsKXTaNw.js";
3
- export { 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, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
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-BSxTUsW_.js";
2
+ import { a as inverse_project_rect, c as DEFAULT_SNAP_OPTIONS, i as install_font_load_geometry_bump, l as SnapOptions, n as DomSurfaceOptions, o as project_delta_inverse_ctm, r as attach_dom_surface, s as project_point_through_ctm, t as DomSurfaceHandle } from "./dom-BMzX1CXZ.js";
3
+ export { 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 };
package/dist/dom.js CHANGED
@@ -1,10 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dom = require("./dom-Dee6FtgZ.js");
2
+ const require_dom = require("./dom-CaByuo6C.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.install_font_load_geometry_bump = require_dom.install_font_load_geometry_bump;
8
9
  exports.inverse_project_rect = require_dom.inverse_project_rect;
9
10
  exports.project_delta_inverse_ctm = require_dom.project_delta_inverse_ctm;
10
11
  exports.project_point_through_ctm = require_dom.project_point_through_ctm;
package/dist/dom.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as Gestures, c as Camera, i as project_point_through_ctm, n as inverse_project_rect, o as DEFAULT_SNAP_OPTIONS, r as project_delta_inverse_ctm, s as MemoizedGeometryProvider, t as attach_dom_surface } from "./dom-DILY80j7.mjs";
2
- export { Camera, DEFAULT_SNAP_OPTIONS, Gestures, MemoizedGeometryProvider, attach_dom_surface, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
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-Bjj9xySE.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 };