@mosaicoo/svg-engine 0.1.0

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.
@@ -0,0 +1,628 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { Type } from '@angular/core';
3
+ import { SvgNode, BoundingBox, SvgDocument, EllipseNode, ImageNode, LineNode, SvgStyle, TextRun, PathNode, PolygonNode, PolylineNode, RectNode, SymbolUseNode, TextNode, Transform, Point } from '@mosaicoo/svg-engine/core';
4
+ import * as _mosaicoo_svg_engine_render from '@mosaicoo/svg-engine/render';
5
+
6
+ /**
7
+ * Top-level SVG renderer. Read-only — does **not** handle selection,
8
+ * editing or interaction (those live in `svg-engine/edit`). Suitable as
9
+ * an embedded viewer in third-party applications (D-017 headless boundary:
10
+ * zero deps on `@angular/material`).
11
+ *
12
+ * **Inputs**:
13
+ * - `tree` (required): the {@link SvgNode} to render. Typically a
14
+ * {@link SvgDocument} `root`, but any node is valid (single-node
15
+ * preview, etc.).
16
+ * - `viewBox` (optional): **seed** for {@link ViewportService.contentBox}.
17
+ * The value is mirrored into the viewport on every change. The actual
18
+ * viewBox attribute on the `<svg>` element is **always** derived from
19
+ * `viewport.viewBox()`, so pan/zoom calls on `ViewportService` always
20
+ * take effect regardless of whether this input is provided.
21
+ * - `width` / `height` (optional): CSS pixel dimensions of the rendered
22
+ * `<svg>` element. When both are omitted the SVG is sized by its CSS
23
+ * container (the component sets `:host` and `svg` to `100%/100%` by
24
+ * default).
25
+ * - `ariaLabel` (optional): accessibility label for the `<svg role="img">`.
26
+ *
27
+ * **Pan/zoom semantics**: at the default viewport state (`zoom=1`,
28
+ * `pan=0`), `viewport.viewBox()` equals `viewport.contentBox()`, so the
29
+ * rendered SVG matches the seed exactly. After a `viewport.zoomIn()`
30
+ * (etc.) call, the rendered viewBox reflects the new state — the
31
+ * displayed content scales/pans accordingly.
32
+ */
33
+ declare class SvgeRenderer {
34
+ private readonly viewport;
35
+ private readonly svgRoot;
36
+ /**
37
+ * **AUDIT-FIX P9** — typed accessor for the rendered `<svg>` element
38
+ * scoped to **this** renderer instance.
39
+ *
40
+ * Returns `null` when the view hasn't been attached yet (early calls
41
+ * during ngOnInit, SSR, jsdom without layout). Consumers should
42
+ * defensively check for null.
43
+ *
44
+ * **Why expose it**: consumers that need to perform DOM measurements
45
+ * on the rendered shapes (bbox, hit-testing, pointer→doc conversion)
46
+ * previously had to call `document.querySelector('svge-renderer svg')`.
47
+ * That global selector breaks the moment a host mounts more than one
48
+ * `<svge-renderer>` on the same page — the first match wins, leaking
49
+ * measurements between editor instances. Using
50
+ * `@ViewChild(SvgeRenderer)` + `svgElement()` keeps the lookup scoped
51
+ * to the component's own tree.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * @Component({ ... })
56
+ * export class MyHost {
57
+ * private readonly renderer = viewChild(SvgeRenderer);
58
+ *
59
+ * measureNode(id: NodeId): BoundingBox | null {
60
+ * const svg = this.renderer()?.svgElement();
61
+ * return svg === null ? null : getRenderedNodeBBox(svg, id);
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ svgElement(): SVGSVGElement | null;
67
+ readonly tree: _angular_core.InputSignal<SvgNode>;
68
+ readonly viewBox: _angular_core.InputSignal<BoundingBox | null>;
69
+ readonly width: _angular_core.InputSignal<number | null>;
70
+ readonly height: _angular_core.InputSignal<number | null>;
71
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
72
+ /**
73
+ * Optional reusable-definitions fragment ({@link SvgDocument.defs}).
74
+ * When non-empty, the renderer injects the fragment as the first
75
+ * child of `<svg>` via `insertAdjacentHTML('afterbegin', ...)` so
76
+ * `url(#id)` references inside the tree resolve at paint time.
77
+ *
78
+ * **Why insertAdjacentHTML and not the Angular template**: the
79
+ * fragment can include arbitrary SVG markup (`<linearGradient>`,
80
+ * `<clipPath>`, …) with namespaces and id attributes. Embedding via
81
+ * `[innerHTML]` would run through Angular's sanitizer which can
82
+ * mangle SVG-specific constructs. The fragment was already sanitized
83
+ * at import time (script, event handlers, javascript: hrefs all
84
+ * removed); we trust it as opaque markup here.
85
+ */
86
+ readonly defs: _angular_core.InputSignal<string | null>;
87
+ /**
88
+ * Rendered viewBox attribute. **Always** derived from
89
+ * `viewport.viewBox()` — which itself is `contentBox` transformed by
90
+ * current zoom and pan. The optional `viewBox` input feeds the
91
+ * viewport's `contentBox` (via the constructor effect), making it the
92
+ * starting point that pan/zoom act upon.
93
+ *
94
+ * This single-source-of-truth approach guarantees that any caller of
95
+ * `ViewportService.zoomIn()`/`pan()`/etc. produces a visible change
96
+ * regardless of whether `viewBox` was supplied as an input.
97
+ */
98
+ protected readonly viewBoxAttr: _angular_core.Signal<string>;
99
+ /**
100
+ * Sentinel data attribute applied to the injected `<defs>` block so
101
+ * re-runs can find and replace it idempotently instead of stacking
102
+ * duplicates.
103
+ */
104
+ private static readonly DEFS_MARKER_ATTR;
105
+ constructor();
106
+ /**
107
+ * Idempotently materialize the current `defs()` value as the first
108
+ * child of `<svg>`. Sequence of operations:
109
+ *
110
+ * 1. Locate (and remove) any previous `<defs data-svge-injected-defs="1">`
111
+ * we wrote on an earlier effect tick.
112
+ * 2. If the new fragment is non-empty, build a fresh `<defs>` element
113
+ * and set its inner XML via `insertAdjacentHTML`. The marker attr
114
+ * distinguishes our injection from consumer-provided defs.
115
+ *
116
+ * Safe in jsdom (no-ops when the viewChild ref hasn't resolved yet),
117
+ * SSR (typeof document gate via insertAdjacentHTML availability),
118
+ * and worker contexts (effect simply never runs DOM ops without the
119
+ * viewChild element).
120
+ */
121
+ private syncDefs;
122
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeRenderer, never>;
123
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvgeRenderer, "svge-renderer", never, { "tree": { "alias": "tree"; "required": true; "isSignal": true; }; "viewBox": { "alias": "viewBox"; "required": false; "isSignal": true; }; "width": { "alias": "width"; "required": false; "isSignal": true; }; "height": { "alias": "height"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "defs": { "alias": "defs"; "required": false; "isSignal": true; }; }, {}, never, ["[svgeBehind]", "*"], true, never>;
124
+ }
125
+ /**
126
+ * Convenience helper for consumers that hold a {@link SvgDocument}: returns
127
+ * its `root` and `viewBox` ready to bind to {@link SvgeRenderer}.
128
+ *
129
+ * ```html
130
+ * <svge-renderer [tree]="doc().root" [viewBox]="doc().viewBox" />
131
+ * ```
132
+ */
133
+ declare function projectDocumentToRenderer(doc: SvgDocument): {
134
+ readonly tree: SvgNode;
135
+ readonly viewBox: BoundingBox;
136
+ };
137
+
138
+ /** Apply to `<svg:ellipse>` to bind attributes from an {@link EllipseNode}. */
139
+ declare class SvgeEllipseDirective {
140
+ readonly node: _angular_core.InputSignal<EllipseNode>;
141
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeEllipseDirective, never>;
142
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeEllipseDirective, "[svgeEllipse]", never, { "node": { "alias": "svgeEllipse"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
143
+ }
144
+
145
+ /** Apply to `<svg:image>` to bind attributes from an {@link ImageNode}. */
146
+ declare class SvgeImageDirective {
147
+ readonly node: _angular_core.InputSignal<ImageNode>;
148
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeImageDirective, never>;
149
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeImageDirective, "[svgeImage]", never, { "node": { "alias": "svgeImage"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
150
+ }
151
+
152
+ /** Apply to `<svg:line>` to bind attributes from a {@link LineNode}. */
153
+ declare class SvgeLineDirective {
154
+ readonly node: _angular_core.InputSignal<LineNode>;
155
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeLineDirective, never>;
156
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeLineDirective, "[svgeLine]", never, { "node": { "alias": "svgeLine"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
157
+ }
158
+
159
+ /**
160
+ * Dispatch a single {@link SvgNode} to the correct attribute-binding
161
+ * directive on the appropriate SVG element.
162
+ *
163
+ * **Why an attribute selector** (`g[svgeNode]`):
164
+ *
165
+ * The dispatcher's host is itself a `<svg:g>` (the wrapper that carries
166
+ * `data-node-id` and the node's `transform`). Using a custom-element
167
+ * selector like `<svge-node>` would inject a non-SVG element into the
168
+ * SVG render tree, and SVG painters do not paint through unknown
169
+ * non-SVG elements — geometry inside would silently fail to render.
170
+ * Attribute selector + `<svg:g>` host keeps the entire DOM in the SVG
171
+ * namespace.
172
+ *
173
+ * Built-in dispatch:
174
+ * - 8 leaf types are bound by per-type directives (`[svgeRect]`,
175
+ * `[svgeEllipse]`, …) on their native SVG element.
176
+ * - `group` is handled inline (recursive case) — each child becomes a
177
+ * nested `<svg:g svgeNode>`.
178
+ * - Unknown types fall through to {@link NodeRendererRegistry} (D-020
179
+ * plugin extensibility).
180
+ *
181
+ * Usage (top-level):
182
+ * ```html
183
+ * <svg:g svgeNode [node]="root" />
184
+ * ```
185
+ */
186
+ declare class SvgeNodeRenderer {
187
+ private readonly registry;
188
+ readonly node: _angular_core.InputSignal<SvgNode>;
189
+ /** Host-bound transform attr; runs once per input change (OnPush). */
190
+ protected readonly transformAttr: _angular_core.Signal<string | null>;
191
+ /**
192
+ * Custom-component lookup stays computed because the
193
+ * {@link NodeRendererRegistry} signal can change at runtime (plugin
194
+ * install/uninstall) and we want the @switch default branch to react.
195
+ */
196
+ protected readonly customComponent: _angular_core.Signal<_mosaicoo_svg_engine_render.SvgNodeRendererComponent | null>;
197
+ /**
198
+ * **GROUP-STYLE-FIX (A)** — the node's style ONLY when it's a group,
199
+ * else `null`. Drives the group-wrapper paint/filter/opacity host
200
+ * bindings (see the `host` block). Leaf nodes return `null` here so
201
+ * those bindings emit no attribute on the leaf wrapper (the per-type
202
+ * directive paints the leaf's style on its inner element instead).
203
+ */
204
+ protected readonly groupStyle: _angular_core.Signal<SvgStyle | null>;
205
+ /**
206
+ * `stroke-dasharray` for the group wrapper, formatted as the SVG
207
+ * space-separated string. `null` (no attribute) when this isn't a
208
+ * group or the group has no dash pattern. Mirrors the exporter's
209
+ * `strokeDasharray.map(fmt).join(' ')`.
210
+ */
211
+ protected groupDashArray(): string | null;
212
+ /**
213
+ * Text content access for the `@case ('text')` branch. Returns `''`
214
+ * for non-text nodes (never reached at runtime; appeases the template
215
+ * type checker without scattering `$any` for the string interpolation).
216
+ */
217
+ protected textContent(): string;
218
+ /**
219
+ * **D-100** — `true` when the text node has a non-empty `runs` array.
220
+ * Gates the rich-text (inline styled tspans) render branch. Sits below
221
+ * the `textPathRef` branch in precedence (text on a path with per-run
222
+ * styling is out of scope) and above the multi-line `\n` path.
223
+ */
224
+ protected textHasRuns(): boolean;
225
+ /** Runs for the rich-text branch; `[]` when the node has none. */
226
+ protected textRuns(): readonly TextRun[];
227
+ /**
228
+ * `true` when the text node's content contains newline characters —
229
+ * the renderer switches to the multi-line tspan path. Single-line
230
+ * text keeps the simpler plain interpolation.
231
+ */
232
+ protected textIsMultiLine(): boolean;
233
+ /**
234
+ * Split text content into lines for the multi-line tspan path.
235
+ * Returns `[]` for non-text nodes (defensive — branch is gated by
236
+ * `textIsMultiLine`).
237
+ */
238
+ protected textLines(): readonly string[];
239
+ /**
240
+ * X position of the text node — every tspan inherits this value
241
+ * via its own `x` attribute (the default behaviour of tspan without
242
+ * explicit `x` is to continue from the previous tspan's end, which
243
+ * is NOT what we want for multi-line — we want each line to start
244
+ * at the same horizontal anchor).
245
+ */
246
+ protected textX(): number;
247
+ /**
248
+ * **D-069** — `dy` value (in `em`s) for non-first tspans in the
249
+ * multi-line text path. Reads `node().lineHeight` and falls back to
250
+ * `1.2` (the pre-D-069 hardcoded default) when undefined — preserves
251
+ * backward compatibility for existing docs/specs.
252
+ *
253
+ * Returned as a CSS-style string with `em` unit so it composes with
254
+ * the `font-size` already on the parent `<text>` (the tspan inherits).
255
+ * Choosing `em` over `px` keeps line spacing proportional when the
256
+ * user later changes the font size — same behavior as CSS
257
+ * `line-height: <number>` (unitless multiplier).
258
+ */
259
+ protected textLineDy(): string;
260
+ protected textPathHref(): string | null;
261
+ protected textPathStartOffset(): string | null;
262
+ /**
263
+ * `<textPath>` does NOT honor `\n` — embedded newlines render as
264
+ * a single literal space (per SVG spec: whitespace collapse). We
265
+ * pre-collapse so the user sees what they get instead of a stretched
266
+ * " word " gap from the raw newline character.
267
+ */
268
+ protected textPathFlattened(): string;
269
+ /**
270
+ * Children iteration for the `@case ('group')` branch. Same template-
271
+ * type-checker rationale as {@link textContent}. Empty array for
272
+ * non-group nodes (never reached).
273
+ */
274
+ protected groupChildren(): readonly SvgNode[];
275
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeNodeRenderer, never>;
276
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvgeNodeRenderer, "g[svgeNode]", never, { "node": { "alias": "node"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
277
+ }
278
+
279
+ /**
280
+ * Apply to `<svg:path>` to bind attributes from a {@link PathNode}.
281
+ *
282
+ * **D-055 (Live Corners)**: when `node.cornerRadius > 0`, the
283
+ * rendered `d` is the result of {@link roundPathCorners}(authored d,
284
+ * radius) — sharp interior vertices get smoothly rounded with that
285
+ * radius. Authored `d` is left untouched (preserved on the model
286
+ * for later edits / radius slider changes). When `cornerRadius` is
287
+ * `0` or `undefined`, the authored `d` is emitted verbatim — same
288
+ * behavior as before D-055.
289
+ *
290
+ * **D-068 follow-up — `id` host binding**: paths are the only shape
291
+ * type a `<textPath href="#…">` (D-053) can reference, so the path
292
+ * needs a DOM `id` matching `node.id`. Without this, `textPathRef` set
293
+ * by the Inspector resolves to no element and the text silently
294
+ * disappears (bbox in place, zero characters painted). The wrapper
295
+ * `<g>` already carries `data-node-id` for selection lookup, but
296
+ * `data-*` doesn't satisfy `#id` href lookups — only the standard
297
+ * `id` attribute does. Cost is one attribute per path, negligible.
298
+ * No collision risk because `NodeId`s are UUIDs (globally unique
299
+ * across documents and editors). If a future use-case wants to opt
300
+ * out of paint-target ids, gate this behind a flag — keeping it
301
+ * unconditional makes textPath, future `<use href>` fallbacks, and
302
+ * DOM inspector debugging all easier.
303
+ */
304
+ declare class SvgePathDirective {
305
+ readonly node: _angular_core.InputSignal<PathNode>;
306
+ protected readonly effectiveD: _angular_core.Signal<string>;
307
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgePathDirective, never>;
308
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgePathDirective, "[svgePath]", never, { "node": { "alias": "svgePath"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
309
+ }
310
+
311
+ /** Apply to `<svg:polygon>` to bind attributes from a {@link PolygonNode}. */
312
+ declare class SvgePolygonDirective {
313
+ readonly node: _angular_core.InputSignal<PolygonNode>;
314
+ protected readonly pointsAttr: _angular_core.Signal<string>;
315
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgePolygonDirective, never>;
316
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgePolygonDirective, "[svgePolygon]", never, { "node": { "alias": "svgePolygon"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
317
+ }
318
+
319
+ /** Apply to `<svg:polyline>` to bind attributes from a {@link PolylineNode}. */
320
+ declare class SvgePolylineDirective {
321
+ readonly node: _angular_core.InputSignal<PolylineNode>;
322
+ protected readonly pointsAttr: _angular_core.Signal<string>;
323
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgePolylineDirective, never>;
324
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgePolylineDirective, "[svgePolyline]", never, { "node": { "alias": "svgePolyline"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
325
+ }
326
+
327
+ /**
328
+ * Apply to a `<svg:rect>` element to bind its attributes from a
329
+ * {@link RectNode}. Use the directive as both selector and value binding:
330
+ *
331
+ * ```html
332
+ * <svg:rect [svgeRect]="rectNode" />
333
+ * ```
334
+ *
335
+ * Why a directive (not a component): components inject a host element
336
+ * created by `document.createElement(selector)` which lives in the HTML
337
+ * namespace. Inside an `<svg>`, an HTML wrapper element breaks the SVG
338
+ * render tree — the SVG painter does not traverse non-SVG elements. A
339
+ * directive applied to an actual `<svg:rect>` element keeps the host in
340
+ * the SVG namespace so geometry attributes paint correctly.
341
+ */
342
+ declare class SvgeRectDirective {
343
+ readonly node: _angular_core.InputSignal<RectNode>;
344
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeRectDirective, never>;
345
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeRectDirective, "[svgeRect]", never, { "node": { "alias": "svgeRect"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
346
+ }
347
+
348
+ /**
349
+ * Apply to `<svg:use>` to bind attributes from a {@link SymbolUseNode}
350
+ * (D-059). The `<symbol id="{symbolId}">` definition that this `<use>`
351
+ * references is contributed to `<defs>` by `ActiveSymbolsService`
352
+ * (in `svg-engine/edit`).
353
+ *
354
+ * **Style inheritance**: `fill`/`stroke`/`opacity` set on the `<use>`
355
+ * cascade into the shadow tree (the master's contents) for any inner
356
+ * element that didn't specify those values explicitly. Editing the
357
+ * master overrides everywhere; editing the instance customizes a
358
+ * single occurrence.
359
+ *
360
+ * **Filter on `<use>`** applies to the rendered output, AFTER the
361
+ * symbol expansion — useful for "glow that one instance" without
362
+ * touching the master.
363
+ *
364
+ * **width/height optional**: when omitted, the symbol renders at its
365
+ * master's natural size (from the `<symbol>`'s `viewBox`).
366
+ */
367
+ declare class SvgeSymbolUseDirective {
368
+ readonly node: _angular_core.InputSignal<SymbolUseNode>;
369
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeSymbolUseDirective, never>;
370
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeSymbolUseDirective, "[svgeSymbolUse]", never, { "node": { "alias": "svgeSymbolUse"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
371
+ }
372
+
373
+ /**
374
+ * Apply to `<svg:text>` to bind attributes from a {@link TextNode}.
375
+ *
376
+ * The `<svg:text>` element's text **content** is set in the dispatcher's
377
+ * template, because directives cannot inject child nodes. The dispatcher
378
+ * splits `node.content` on `\n` and emits one `<tspan dy>` per line, so
379
+ * multi-line text is rendered by simply embedding line breaks in the
380
+ * `content` field. Explicit `<tspan>` runs with per-run styling are
381
+ * still not modelled at the data layer — that would require an extra
382
+ * `runs` field on `TextNode`.
383
+ */
384
+ declare class SvgeTextDirective {
385
+ readonly node: _angular_core.InputSignal<TextNode>;
386
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvgeTextDirective, never>;
387
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvgeTextDirective, "[svgeText]", never, { "node": { "alias": "svgeText"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
388
+ }
389
+
390
+ /**
391
+ * Viewport state for the SVG renderer: zoom level and pan offset over a
392
+ * base "content" {@link BoundingBox}. Exposed as Angular signals so the
393
+ * renderer can recompute the displayed `viewBox` reactively.
394
+ *
395
+ * Coordinate model:
396
+ * - `contentBox` is the original document space ("what to show at zoom 1").
397
+ * - `zoom` scales the visible window: `>1` zooms in (smaller window),
398
+ * `<1` zooms out (larger window).
399
+ * - `panX`/`panY` translate the visible window in **content units**.
400
+ *
401
+ * The resulting visible {@link BoundingBox} is exposed via {@link viewBox},
402
+ * suitable for binding to `<svg [attr.viewBox]>`.
403
+ */
404
+ declare class ViewportService {
405
+ private readonly _contentBox;
406
+ private readonly _zoom;
407
+ private readonly _panX;
408
+ private readonly _panY;
409
+ private readonly _minZoom;
410
+ private readonly _maxZoom;
411
+ /**
412
+ * **D-106** — CSS-pixel size of the rendered canvas viewport (the `<svg>`
413
+ * element's client box). `{0,0}` until a consumer reports it (the
414
+ * `[svgeCanvasGestures]` directive does, via a ResizeObserver). Needed to
415
+ * relate the internal `zoom` (which is relative to `contentBox`) to a
416
+ * **physical** on-screen scale — see {@link displayScale}.
417
+ */
418
+ private readonly _viewportSize;
419
+ readonly contentBox: _angular_core.Signal<BoundingBox>;
420
+ readonly zoom: _angular_core.Signal<number>;
421
+ readonly panX: _angular_core.Signal<number>;
422
+ readonly panY: _angular_core.Signal<number>;
423
+ readonly minZoom: _angular_core.Signal<number>;
424
+ readonly maxZoom: _angular_core.Signal<number>;
425
+ readonly viewportSize: _angular_core.Signal<{
426
+ readonly width: number;
427
+ readonly height: number;
428
+ }>;
429
+ /**
430
+ * Visible window in content coordinates, derived from `contentBox`,
431
+ * `zoom`, `panX`, `panY`. Bind directly to `<svg [attr.viewBox]>`.
432
+ */
433
+ readonly viewBox: _angular_core.Signal<BoundingBox>;
434
+ /**
435
+ * **D-106** — physical scale (CSS px per document unit) that the WHOLE
436
+ * `contentBox` occupies at `zoom === 1` — i.e. the "fit to window" factor.
437
+ * `preserveAspectRatio="xMidYMid meet"` applies a uniform scale, so it's the
438
+ * limiting (min) of the two axis ratios. `null` when the viewport size or
439
+ * content box is unknown/degenerate (e.g. headless tests with no layout).
440
+ */
441
+ readonly fitScale: _angular_core.Signal<number | null>;
442
+ /**
443
+ * **D-106** — true on-screen scale: how many CSS pixels one document unit
444
+ * occupies right now. `displayScale === 1` means real 1:1 ("100% / Actual
445
+ * Size", the market convention). Because the SVG renders the visible
446
+ * `viewBox` (`contentBox.size / zoom`) stretched to fill the viewport, the
447
+ * physical scale is `zoom × fitScale`.
448
+ *
449
+ * **Fallback**: when {@link fitScale} is unknown (no measured viewport, e.g.
450
+ * headless rendering) this returns the raw `zoom` — preserving the old
451
+ * "zoom = percent" meaning so non-DOM consumers/tests keep working.
452
+ */
453
+ readonly displayScale: _angular_core.Signal<number>;
454
+ /** Replace the base content box (e.g., when loading a new document). */
455
+ setContentBox(box: BoundingBox): void;
456
+ /**
457
+ * **D-106** — report the rendered canvas size (CSS px). Drives
458
+ * {@link fitScale}/{@link displayScale}. Ignores non-finite/negative input.
459
+ */
460
+ setViewportSize(width: number, height: number): void;
461
+ /**
462
+ * **D-106** — set the internal `zoom` so the on-screen {@link displayScale}
463
+ * equals `scale` (e.g. `1` for true 1:1). When the viewport hasn't been
464
+ * measured ({@link fitScale} null), falls back to setting `zoom` directly so
465
+ * the call still does something sensible.
466
+ */
467
+ setDisplayScale(scale: number): void;
468
+ /** **D-106** — "Actual Size" (100%): pin the on-screen scale to true 1:1. */
469
+ actualSize(): void;
470
+ /**
471
+ * **D-107** — initial framing for a FRESH document: show it at true 100%
472
+ * (1:1) when the whole content box fits the viewport, otherwise fit it to the
473
+ * window. Pan resets to origin (centered). This is the "Fit on Screen"
474
+ * convention (Photoshop): documents that fit open at 100%; larger ones fit so
475
+ * the user isn't dropped into a corner. Falls back to fit (zoom 1) when the
476
+ * viewport hasn't been measured ({@link fitScale} null).
477
+ *
478
+ * (Opening a saved `.svge`/`.svgez` does NOT use this — it restores the
479
+ * viewport persisted in the file.)
480
+ */
481
+ frameNewDocument(): void;
482
+ /** Replace zoom directly (clamped to `[minZoom, maxZoom]`). */
483
+ setZoom(zoom: number): void;
484
+ /** Multiply current zoom by `factor` (clamped). */
485
+ multiplyZoom(factor: number): void;
486
+ /** Zoom in by the configured step factor. */
487
+ zoomIn(step?: number): void;
488
+ /** Zoom out by the configured step factor. */
489
+ zoomOut(step?: number): void;
490
+ /** Replace pan offset directly (in content units). */
491
+ setPan(x: number, y: number): void;
492
+ /** Translate the current pan by `(dx, dy)` in content units. */
493
+ pan(dx: number, dy: number): void;
494
+ /**
495
+ * Zoom by `factor` while keeping `anchor` (in document coordinates)
496
+ * stationary on screen. Used by wheel-zoom (anchor = cursor) and
497
+ * "zoom to selection" (anchor = selection centre).
498
+ *
499
+ * **Math**: pre-zoom relative position of the anchor inside the
500
+ * visible viewBox must equal post-zoom relative position. Solving
501
+ * for the new pan and applying both `zoom` and `pan` atomically
502
+ * makes the visible viewBox preserve the anchor.
503
+ *
504
+ * **No-op-safe**: if `factor` would push zoom past min/max limits,
505
+ * the clamped zoom may equal the current zoom — pan adjusts to a
506
+ * zero delta, anchor stays put (no observable change).
507
+ */
508
+ zoomAt(factor: number, anchor: {
509
+ readonly x: number;
510
+ readonly y: number;
511
+ }): void;
512
+ /** Reset zoom to 1 and pan to origin (centered on content). */
513
+ reset(): void;
514
+ /**
515
+ * Reset to fit the content box exactly. Identical to {@link reset}
516
+ * today; reserved for future "fit to selection" / "fit to bounds"
517
+ * variants.
518
+ */
519
+ fit(): void;
520
+ /**
521
+ * **D-118** — frame `target` (in document/content coordinates) in the visible
522
+ * viewBox: zoom so the box fits with `paddingFraction` margin on each side,
523
+ * and pan so it's centered. Powers `View ▸ Zoom ▸ Fit Selection`.
524
+ *
525
+ * The visible viewBox always keeps the content aspect ratio, so the box is
526
+ * fit "meet"-style — the limiting dimension touches the padded edge, the other
527
+ * has extra room. Zoom is clamped to `[minZoom, maxZoom]`. A degenerate
528
+ * (zero-area) target only re-centers, preserving the current zoom.
529
+ */
530
+ fitBox(target: BoundingBox, paddingFraction?: number): void;
531
+ /** Configure clamping bounds for zoom. Throws on invalid input. */
532
+ setZoomLimits(min: number, max: number): void;
533
+ private clampZoom;
534
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ViewportService, never>;
535
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<ViewportService>;
536
+ }
537
+
538
+ /**
539
+ * Renderer component class. The dispatcher (`<svge-node>`) mounts these
540
+ * via `*ngComponentOutlet` and binds `inputs: { node: <SvgNode> }`. The
541
+ * concrete shape of the component is not enforced statically (Angular's
542
+ * `input.required<T>()` returns an `InputSignal<T>`, which does not match
543
+ * a plain `T` field) — the runtime contract is:
544
+ *
545
+ * - The component declares `node` as an Angular `input()` (preferred)
546
+ * or a plain settable property accepting an `SvgNode`.
547
+ * - The component is `standalone: true`.
548
+ */
549
+ type SvgNodeRendererComponent = Type<unknown>;
550
+ /**
551
+ * Registry of **custom** renderer components, keyed by `SvgNode['type']`.
552
+ *
553
+ * Built-in node types (`rect`, `ellipse`, `line`, `polygon`, `polyline`,
554
+ * `path`, `text`, `image`, `group`) are dispatched by the dispatcher's own
555
+ * `@switch` and do **not** go through this registry. The registry is the
556
+ * extension point for plugins (D-020) that introduce new `type`
557
+ * discriminators (e.g., `'star'`, `'chart-bar'`).
558
+ *
559
+ * Plugin install code:
560
+ * ```ts
561
+ * inject(NodeRendererRegistry).register('star', SvgeStarRenderer);
562
+ * ```
563
+ *
564
+ * The dispatcher resolves the registry's component for unknown types,
565
+ * mounts it via `*ngComponentOutlet` and binds `[node]` automatically.
566
+ */
567
+ declare class NodeRendererRegistry {
568
+ private readonly entries;
569
+ /**
570
+ * Register a renderer component for a custom node type. Throws when the
571
+ * `type` is already registered (use {@link unregister} first to override).
572
+ */
573
+ register(type: string, component: SvgNodeRendererComponent): void;
574
+ /** Remove a previously registered renderer. No-op when absent. */
575
+ unregister(type: string): void;
576
+ /** Resolve the renderer for the given type, or `null` if unregistered. */
577
+ resolve(type: string): SvgNodeRendererComponent | null;
578
+ /** All currently registered type discriminators (for inspection/tooling). */
579
+ registeredTypes(): readonly string[];
580
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NodeRendererRegistry, never>;
581
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<NodeRendererRegistry>;
582
+ }
583
+
584
+ /**
585
+ * Serialize a {@link Transform} for the SVG `transform` attribute, e.g.
586
+ * `matrix(1 0 0 1 10 20)`. Returns `null` for the identity transform so
587
+ * callers can bind directly with `[attr.transform]` and omit the attribute
588
+ * when it would be a no-op.
589
+ */
590
+ declare function renderTransformAttr(transform: Transform): string | null;
591
+
592
+ /**
593
+ * Project a client (screen) pixel coordinate into the document's
594
+ * user-space coordinate system by inverting the SVG element's live
595
+ * `getScreenCTM()`.
596
+ *
597
+ * **Why a shared util**: this is the single most duplicated helper
598
+ * across the codebase. Until this util, the same 10-line block lived
599
+ * in 5 different files (`custom-editor` (ex-`playground-home`), `selection-overlay`,
600
+ * `canvas-gestures.directive`, `guides-overlay`, `rulers`) — every new
601
+ * pointer-driven component had to re-implement the CTM dance with the
602
+ * same defensive guards (jsdom missing `getScreenCTM`/`createSVGPoint`,
603
+ * SSR-detached SVG returning a `null` CTM, etc.). Centralizing prevents
604
+ * subtle drift (e.g., one site forgetting the `createSVGPoint` guard
605
+ * and crashing under jsdom).
606
+ *
607
+ * **What it handles**:
608
+ * - `svg === null` → returns `null` (caller didn't have an SVG ref yet)
609
+ * - `getScreenCTM` not implemented → returns `null` (jsdom / SSR)
610
+ * - `getScreenCTM` returns `null` → returns `null` (detached SVG)
611
+ * - `createSVGPoint` not available → returns `null` (older SVG impls)
612
+ *
613
+ * **What it does NOT do**:
614
+ * - Locate the `<svg>` element for you. Callers supply it via
615
+ * `ElementRef.ownerSVGElement`, `document.querySelector`, or a saved
616
+ * `viewChild` ref. Each caller knows the right way to reach its own
617
+ * SVG; baking a strategy here would couple this util to component DI.
618
+ *
619
+ * @param svg The `<svg>` element whose CTM defines the projection.
620
+ * Pass `null` to short-circuit (returns `null`).
621
+ * @param clientX `clientX` from a `MouseEvent` / `PointerEvent`.
622
+ * @param clientY `clientY` from a `MouseEvent` / `PointerEvent`.
623
+ * @returns Doc-space `{ x, y }` or `null` when projection is impossible.
624
+ */
625
+ declare function screenToDoc(svg: SVGSVGElement | null, clientX: number, clientY: number): Point | null;
626
+
627
+ export { NodeRendererRegistry, SvgeEllipseDirective, SvgeImageDirective, SvgeLineDirective, SvgeNodeRenderer, SvgePathDirective, SvgePolygonDirective, SvgePolylineDirective, SvgeRectDirective, SvgeRenderer, SvgeSymbolUseDirective, SvgeTextDirective, ViewportService, projectDocumentToRenderer, renderTransformAttr, screenToDoc };
628
+ export type { SvgNodeRendererComponent };