@grida/svg-editor 1.0.0-alpha.13 → 1.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -42
- package/dist/dom-CK6GlgFF.d.mts +114 -0
- package/dist/dom-CsKXTaNw.d.ts +112 -0
- package/dist/{dom-BlMk07oX.mjs → dom-DILY80j7.mjs} +1622 -619
- package/dist/{dom-Cvm9Towu.js → dom-Dee6FtgZ.js} +1648 -626
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +4 -1
- package/dist/dom.mjs +2 -2
- package/dist/{editor-Bd4-VCEJ.d.ts → editor-BKoo9SPL.d.ts} +643 -25
- package/dist/{editor-DtuRIs-Q.mjs → editor-CvWpD5mu.mjs} +820 -322
- package/dist/{editor-BH03X8cX.d.mts → editor-Dl7c0q5A.d.mts} +643 -25
- package/dist/{editor-CdyC3uAe.js → editor-F8ckj9X1.js} +828 -330
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.mjs +3 -3
- package/dist/model-B2UWgViT.mjs +3729 -0
- package/dist/model-CJ1Ctq14.js +3875 -0
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +3 -3
- package/dist/presets.mjs +2 -2
- package/dist/react.d.mts +18 -6
- package/dist/react.d.ts +18 -6
- package/dist/react.js +24 -3
- package/dist/react.mjs +24 -3
- package/package.json +30 -8
- package/dist/dom-DCX-a8Kr.d.ts +0 -57
- package/dist/dom-DgB4f-TE.d.mts +0 -59
- package/dist/insertions-BJ-6o6o5.js +0 -2399
- package/dist/insertions-Okcuo-Ck.mjs +0 -2176
- /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cmath from "@grida/cmath";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import vn from "@grida/vn";
|
|
3
|
+
import { AnyNode, AttrToken } from "@grida/svg/parser";
|
|
4
4
|
import { SelectMode } from "@grida/hud";
|
|
5
5
|
import { Keybinding, Platform } from "@grida/keybinding";
|
|
6
6
|
|
|
@@ -24,12 +24,14 @@ type Rect = {
|
|
|
24
24
|
};
|
|
25
25
|
type Mode = "select" | "edit-content";
|
|
26
26
|
/**
|
|
27
|
-
* SVG element tags
|
|
28
|
-
* a new insertable tag requires a PR.
|
|
27
|
+
* SVG element tags inserted by the **drag-to-size** subsystem. Closed set;
|
|
28
|
+
* adding a new insertable tag requires a PR.
|
|
29
29
|
*
|
|
30
|
-
* `text` is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* `text` is intentionally NOT here. It is creatable (the `insert-text`
|
|
31
|
+
* tool — see {@link Tool}), but via a **click-only** gesture, not
|
|
32
|
+
* drag-to-size: `<text>` has no intrinsic size, so there is nothing for a
|
|
33
|
+
* drag to set. Its creation path mounts the inline content-editor
|
|
34
|
+
* immediately. Design: `docs/wg/feat-svg-editor/text-tool.md`.
|
|
33
35
|
*/
|
|
34
36
|
type InsertableTag = "rect" | "ellipse" | "line";
|
|
35
37
|
/**
|
|
@@ -48,6 +50,39 @@ type Tool = {
|
|
|
48
50
|
} | {
|
|
49
51
|
type: "insert";
|
|
50
52
|
tag: InsertableTag;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Text creation tool. A select-mode tool like `insert`, but **click-only**
|
|
56
|
+
* rather than drag-to-size: pointer-down creates a single-line `<text>` at
|
|
57
|
+
* the click point with default appearance and immediately enters
|
|
58
|
+
* content-edit (caret active). `<text>` has no intrinsic size, so the
|
|
59
|
+
* drag-to-size model doesn't apply; a drag box would mean SVG 2 wrapped
|
|
60
|
+
* text, which is a separate (out-of-scope) model. Reverts to `cursor`
|
|
61
|
+
* after the node is placed. Design:
|
|
62
|
+
* `docs/wg/feat-svg-editor/text-tool.md`.
|
|
63
|
+
*/
|
|
64
|
+
| {
|
|
65
|
+
type: "insert-text";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Vector content-edit lasso (Q). Empty-space drag draws a freeform
|
|
69
|
+
* polygon that picks vertices + tangents inside it (segments are NOT
|
|
70
|
+
* tested — matches the main editor decision; see `@grida/hud`
|
|
71
|
+
* `VectorSelectionMode`). Valid only while `state.mode === "edit-content"`
|
|
72
|
+
* on a path; tool reverts to cursor on path-content-edit exit.
|
|
73
|
+
*/
|
|
74
|
+
| {
|
|
75
|
+
type: "lasso";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Vector content-edit bend. Sticky version of holding Meta — every
|
|
79
|
+
* segment-body drag bends instead of translating, regardless of
|
|
80
|
+
* Meta state. See `@grida/hud` `VectorBendMode`. Valid only while
|
|
81
|
+
* `state.mode === "edit-content"` on a path; tool reverts to cursor
|
|
82
|
+
* on path-content-edit exit.
|
|
83
|
+
*/
|
|
84
|
+
| {
|
|
85
|
+
type: "bend";
|
|
51
86
|
};
|
|
52
87
|
declare const TOOL_CURSOR: Tool;
|
|
53
88
|
type Provenance = {
|
|
@@ -221,8 +256,24 @@ type EditorState = {
|
|
|
221
256
|
* any change — selection, history, mutation. NOT a good cache key for
|
|
222
257
|
* tree-shape views because it fires on attribute writes too (e.g. x/y
|
|
223
258
|
* during a drag).
|
|
259
|
+
*
|
|
260
|
+
* NOT a content fingerprint either: this bumps on UI-state emissions
|
|
261
|
+
* (selection, scope, mode, tool) that leave the serialized SVG
|
|
262
|
+
* unchanged. For "did the document content change?" use
|
|
263
|
+
* {@link content_version}.
|
|
224
264
|
*/
|
|
225
265
|
readonly version: number;
|
|
266
|
+
/**
|
|
267
|
+
* Bumps on every document mutation — insert, remove, reorder, attribute
|
|
268
|
+
* write, style write, undo, redo, load. Stable across pure UI-state
|
|
269
|
+
* emissions (selection, scope, mode, tool).
|
|
270
|
+
*
|
|
271
|
+
* The honest fingerprint for serialized content: if `content_version`
|
|
272
|
+
* is unchanged, `editor.serialize()` returns the same bytes. Use this
|
|
273
|
+
* — not `version` — as the freshness token when persisting, diffing,
|
|
274
|
+
* or hashing the document.
|
|
275
|
+
*/
|
|
276
|
+
readonly content_version: number;
|
|
226
277
|
/**
|
|
227
278
|
* Bumps only when the document's tree shape or display-label-affecting
|
|
228
279
|
* data changes — node added/removed/reordered, text content, or the
|
|
@@ -434,6 +485,91 @@ declare class Camera {
|
|
|
434
485
|
type AlignDirection = "left" | "right" | "top" | "bottom" | "horizontal_centers" | "vertical_centers";
|
|
435
486
|
//#endregion
|
|
436
487
|
//#region src/core/document.d.ts
|
|
488
|
+
/**
|
|
489
|
+
* What `is_vector_edit_target` returns when a node is eligible for
|
|
490
|
+
* vector (vertex) editing — a tag-discriminated snapshot of the authored
|
|
491
|
+
* geometry attributes at enter time.
|
|
492
|
+
*
|
|
493
|
+
* Consumed by `VectorEditSession` (which holds it as `source_attrs`) and by
|
|
494
|
+
* `PathModel` (whose `toNativeAttrs(source_tag)` decides on each commit
|
|
495
|
+
* whether the post-edit form is still expressible in the source tag, or
|
|
496
|
+
* whether the element must promote to `<path d="…">`).
|
|
497
|
+
*
|
|
498
|
+
* Geometry conventions:
|
|
499
|
+
* - All coordinates are in the element's own local space, exactly as
|
|
500
|
+
* authored. No `transform=` resolution, no parent CTM, no viewport
|
|
501
|
+
* remap.
|
|
502
|
+
* - `line` carries its two endpoints; `polyline` / `polygon` points are
|
|
503
|
+
* `[x, y]` tuples so the consumer can hand them straight to
|
|
504
|
+
* `vn.fromPolyline` / `vn.fromPolygon`.
|
|
505
|
+
* - `rect` / `circle` / `ellipse` carry their native geometry numbers.
|
|
506
|
+
* These geometry primitives have no addressable interior vertices in
|
|
507
|
+
* their native form, so editing one as vector geometry re-types the
|
|
508
|
+
* element to `<path>` (see `retype_to_path`). The document holds the
|
|
509
|
+
* native tag until that re-type is committed. Design:
|
|
510
|
+
* `docs/wg/feat-svg-editor/promote-to-path.md`.
|
|
511
|
+
*
|
|
512
|
+
* Re-type vs. native writeback is decided per edit, not per tag: an edit
|
|
513
|
+
* that the source tag can still express (a straight vertex move on
|
|
514
|
+
* `line` / `polyline` / `polygon`) writes back natively; one it cannot (a
|
|
515
|
+
* curve, or a topology change that leaves the tag's canonical form)
|
|
516
|
+
* re-types the element to `<path>`.
|
|
517
|
+
*/
|
|
518
|
+
type VectorEditSource = {
|
|
519
|
+
kind: "path";
|
|
520
|
+
d: string;
|
|
521
|
+
} | {
|
|
522
|
+
kind: "line";
|
|
523
|
+
x1: number;
|
|
524
|
+
y1: number;
|
|
525
|
+
x2: number;
|
|
526
|
+
y2: number;
|
|
527
|
+
} | {
|
|
528
|
+
kind: "polyline";
|
|
529
|
+
points: ReadonlyArray<readonly [number, number]>;
|
|
530
|
+
} | {
|
|
531
|
+
kind: "polygon";
|
|
532
|
+
points: ReadonlyArray<readonly [number, number]>;
|
|
533
|
+
} | {
|
|
534
|
+
kind: "rect";
|
|
535
|
+
x: number;
|
|
536
|
+
y: number;
|
|
537
|
+
width: number;
|
|
538
|
+
height: number; /** Corner radii; `0` when the rect has square corners. */
|
|
539
|
+
rx: number;
|
|
540
|
+
ry: number;
|
|
541
|
+
} | {
|
|
542
|
+
kind: "circle";
|
|
543
|
+
cx: number;
|
|
544
|
+
cy: number;
|
|
545
|
+
r: number;
|
|
546
|
+
} | {
|
|
547
|
+
kind: "ellipse";
|
|
548
|
+
cx: number;
|
|
549
|
+
cy: number;
|
|
550
|
+
rx: number;
|
|
551
|
+
ry: number;
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Opaque reversal token returned by `retype_to_path`. Callers hold it and
|
|
555
|
+
* hand it back to `revert_retype` to restore the original primitive
|
|
556
|
+
* byte-for-byte; they do not inspect it. All trivia / attribute-token
|
|
557
|
+
* knowledge stays inside `SvgDocument`.
|
|
558
|
+
*/
|
|
559
|
+
type RetypeRecord = {
|
|
560
|
+
readonly prev_local: string;
|
|
561
|
+
readonly prev_raw_tag: string;
|
|
562
|
+
/** Geometry attribute tokens removed on re-type, with their original
|
|
563
|
+
* index in the element's `attrs` array. Ascending by index so they can
|
|
564
|
+
* be spliced back in order. Typed as the document's internal attr token. */
|
|
565
|
+
readonly removed: ReadonlyArray<{
|
|
566
|
+
index: number;
|
|
567
|
+
token: AttrToken;
|
|
568
|
+
}>;
|
|
569
|
+
/** True iff the re-type added a synthetic `fill="none"` (the `<line>`
|
|
570
|
+
* fidelity guard — see `retype_to_path`). `revert_retype` removes it. */
|
|
571
|
+
readonly added_fill_none?: boolean;
|
|
572
|
+
};
|
|
437
573
|
interface DocumentEvents {
|
|
438
574
|
/** Fires after any structural mutation. */
|
|
439
575
|
on_change(fn: () => void): () => void;
|
|
@@ -462,7 +598,7 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
462
598
|
private _structure_version;
|
|
463
599
|
/** Bumps on writes that can shift world-space bounds (`GEOMETRY_ATTRS`,
|
|
464
600
|
* `set_text`, `insert`, `remove`). Cache key for `GeometryProvider`;
|
|
465
|
-
* see docs/
|
|
601
|
+
* see ../../docs/geometry.md. */
|
|
466
602
|
private _geometry_version;
|
|
467
603
|
constructor(svg: string);
|
|
468
604
|
static parse(svg: string): SvgDocument;
|
|
@@ -547,6 +683,94 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
547
683
|
* or its parent text.
|
|
548
684
|
*/
|
|
549
685
|
is_text_edit_target(id: NodeId): boolean;
|
|
686
|
+
/**
|
|
687
|
+
* Returns a tag-discriminated snapshot of the authored geometry attrs
|
|
688
|
+
* if this node is eligible for vector (vertex) editing — else `null`.
|
|
689
|
+
*
|
|
690
|
+
* Eligibility:
|
|
691
|
+
* - `<path>` — requires non-empty `d`.
|
|
692
|
+
* - `<line>` — requires two distinct finite user-unit endpoints.
|
|
693
|
+
* - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
|
|
694
|
+
* - `<polygon>` — same as polyline.
|
|
695
|
+
* - `<rect>` — requires finite user-unit `width`/`height` > 0.
|
|
696
|
+
* - `<circle>` — requires finite user-unit `r` > 0.
|
|
697
|
+
* - `<ellipse>` — requires finite user-unit `rx`/`ry` > 0.
|
|
698
|
+
*
|
|
699
|
+
* The vertex tags (`line` / `polyline` / `polygon`) write edits back to
|
|
700
|
+
* their native attributes while the geometry stays expressible there; an
|
|
701
|
+
* edit that escapes the native form (a curve, or a topology change that
|
|
702
|
+
* leaves the canonical chain) re-types the element to `<path>`. The
|
|
703
|
+
* geometry primitives (`rect` / `circle` / `ellipse`) have no native
|
|
704
|
+
* vector form, so any vector edit re-types them. In all cases the native
|
|
705
|
+
* tag is preserved byte-for-byte until the first re-typing edit commits
|
|
706
|
+
* (see `retype_to_path`). Design:
|
|
707
|
+
* `docs/wg/feat-svg-editor/promote-to-path.md`.
|
|
708
|
+
*
|
|
709
|
+
* Geometry that is not a plain user-unit number (`%`, `px`, `em`, …) is
|
|
710
|
+
* an out-of-scope gap, so such an element returns `null` rather than
|
|
711
|
+
* advertising an edit the editor cannot perform faithfully.
|
|
712
|
+
*
|
|
713
|
+
* Rejects `<image>` / `<use>` (raster / reference bounding boxes, no
|
|
714
|
+
* editable outline).
|
|
715
|
+
*/
|
|
716
|
+
/**
|
|
717
|
+
* Parse an optional SVG geometry coordinate (`x`/`y`, `cx`/`cy`, the line
|
|
718
|
+
* endpoints). An **absent** attribute takes the SVG default (`0`); a
|
|
719
|
+
* **present** attribute that is not a plain user-unit number (`%`, `px`,
|
|
720
|
+
* `em`, …) is out of scope and yields `null` so the caller refuses the
|
|
721
|
+
* element — the same gate required attrs (width / radius) already apply.
|
|
722
|
+
*
|
|
723
|
+
* The absent-vs-present distinction is the point: a bare `?? 0` would
|
|
724
|
+
* silently coerce an authored `x1="5px"` to `0`, then the first native
|
|
725
|
+
* writeback would overwrite that authored value. Refusing keeps the
|
|
726
|
+
* editor from misrepresenting geometry it cannot read faithfully.
|
|
727
|
+
*/
|
|
728
|
+
private optional_user_unit_coord;
|
|
729
|
+
is_vector_edit_target(id: NodeId): VectorEditSource | null;
|
|
730
|
+
/**
|
|
731
|
+
* Re-type a native vector element (`<line>` / `<polyline>` / `<polygon>` /
|
|
732
|
+
* `<rect>` / `<circle>` / `<ellipse>`) into a `<path>` in place, consuming
|
|
733
|
+
* its native geometry attributes and setting `d`. A structural mutation:
|
|
734
|
+
* this layer executes the re-type; it does not decide when one is
|
|
735
|
+
* warranted.
|
|
736
|
+
*
|
|
737
|
+
* Idempotent: returns `null` if `id` is not currently one of those tags
|
|
738
|
+
* (so it is safe to call repeatedly — once re-typed, e.g. already a
|
|
739
|
+
* `<path>`, further calls are no-ops). Otherwise mutates the node and
|
|
740
|
+
* returns an opaque {@link RetypeRecord} reversal token.
|
|
741
|
+
*
|
|
742
|
+
* Identity, children, `self_closing`, non-geometry attributes, and all
|
|
743
|
+
* source trivia are preserved unchanged — only the tag and the geometry
|
|
744
|
+
* attributes move. Pass the token to {@link revert_retype} to restore
|
|
745
|
+
* the original primitive byte-for-byte.
|
|
746
|
+
*
|
|
747
|
+
* (see test/svg-editor-vector-promote-to-path.md)
|
|
748
|
+
*/
|
|
749
|
+
retype_to_path(id: NodeId, d: string): RetypeRecord | null;
|
|
750
|
+
/**
|
|
751
|
+
* Reverse a {@link retype_to_path}: restore the original tag, remove the
|
|
752
|
+
* `d` attribute the promotion added, and splice the captured geometry
|
|
753
|
+
* attribute tokens back at their original positions (preserving their
|
|
754
|
+
* trivia, so a later `serialize()` is byte-equal to the pre-promotion
|
|
755
|
+
* source).
|
|
756
|
+
*/
|
|
757
|
+
revert_retype(id: NodeId, token: RetypeRecord): void;
|
|
758
|
+
/**
|
|
759
|
+
* True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
|
|
760
|
+
* per-glyph attribute (which conflicts with element-level rotation).
|
|
761
|
+
*/
|
|
762
|
+
has_glyph_rotate(id: NodeId): boolean;
|
|
763
|
+
/**
|
|
764
|
+
* True iff this element's inline `style=""` declares a `transform:`
|
|
765
|
+
* CSS property (which would shadow the editor's `transform=` writes).
|
|
766
|
+
*/
|
|
767
|
+
has_inline_css_transform(id: NodeId): boolean;
|
|
768
|
+
/**
|
|
769
|
+
* True iff this element has a direct `<animateTransform>` child
|
|
770
|
+
* (which produces a time-varying transform invisible to attribute writes).
|
|
771
|
+
* Only direct children are checked — nested cases attach to the nearer ancestor.
|
|
772
|
+
*/
|
|
773
|
+
has_animate_transform_child(id: NodeId): boolean;
|
|
550
774
|
text_of(id: NodeId): string;
|
|
551
775
|
/** Replace all direct text children with a single text node carrying `value`. */
|
|
552
776
|
set_text(id: NodeId, value: string): void;
|
|
@@ -558,10 +782,297 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
558
782
|
ns?: string | null;
|
|
559
783
|
}): NodeId;
|
|
560
784
|
serialize(): string;
|
|
785
|
+
/**
|
|
786
|
+
* Serialize a single element's subtree as an SVG **fragment**, using the
|
|
787
|
+
* same trivia-preserving rules as {@link serialize} (attribute order,
|
|
788
|
+
* quote style, whitespace, comments — emitted exactly as authored).
|
|
789
|
+
*
|
|
790
|
+
* This is NOT {@link serialize} scoped to a node — it is a deliberately
|
|
791
|
+
* weaker output (sdk-design D3, asymmetric outputs stay separate):
|
|
792
|
+
*
|
|
793
|
+
* - `serialize()` emits the whole document and carries the P1
|
|
794
|
+
* whole-document round-trip guarantee.
|
|
795
|
+
* - `serialize_node()` emits a fragment and does NOT. Namespace
|
|
796
|
+
* declarations that live on an ancestor (`xmlns:xlink` and friends,
|
|
797
|
+
* normally on the root `<svg>`) are NOT inlined — a node using
|
|
798
|
+
* `xlink:href` serializes without `xmlns:xlink`. The fragment is the
|
|
799
|
+
* element's markup as authored, not a standalone parseable document.
|
|
800
|
+
*
|
|
801
|
+
* Throws on an unknown id, a non-element node, or a node detached from
|
|
802
|
+
* the live tree: the contract is "the markup for a selected element,"
|
|
803
|
+
* selections are always live elements, and a string return of `""` for a
|
|
804
|
+
* bad id would hide consumer bugs. The detached case matters because
|
|
805
|
+
* `remove()` keeps the node in the id map for undo — a stale id from a
|
|
806
|
+
* removed node would otherwise serialize content no longer in the
|
|
807
|
+
* document, silently feeding a consumer deleted markup.
|
|
808
|
+
*/
|
|
809
|
+
serialize_node(id: NodeId): string;
|
|
561
810
|
private emit_node;
|
|
562
811
|
private emit_attr;
|
|
563
812
|
}
|
|
564
813
|
//#endregion
|
|
814
|
+
//#region src/core/vector-edit/model.d.ts
|
|
815
|
+
type Verb = "M" | "L" | "H" | "V" | "C" | "S" | "Q" | "T" | "A" | "Z";
|
|
816
|
+
type VertexId = number;
|
|
817
|
+
type SegmentId = number;
|
|
818
|
+
/** `[vertex_idx, 0]` = ta on segment whose `a === vertex_idx`; `[vertex_idx, 1]` = tb where `b === vertex_idx`. */
|
|
819
|
+
type TangentRef = readonly [VertexId, 0 | 1];
|
|
820
|
+
/**
|
|
821
|
+
* Tangent mirroring policy applied around a vertex when one tangent moves.
|
|
822
|
+
* Mirrors `vn.TangentMirroringMode`.
|
|
823
|
+
*
|
|
824
|
+
* - `auto` — infer from current state (smooth join → mirror angle+length;
|
|
825
|
+
* broken / asymmetric → don't mirror).
|
|
826
|
+
* - `none` — only move the chosen tangent. Other tangents at this vertex
|
|
827
|
+
* stay put.
|
|
828
|
+
* - `angle` — keep opposite tangent collinear (mirror angle), preserve its
|
|
829
|
+
* length. Used when the user wants a sharp-vs-smooth-but-asymmetric
|
|
830
|
+
* handle (Figma's "Mirror angle" mode).
|
|
831
|
+
* - `all` — mirror both angle and length. Standard "smooth" handle pair.
|
|
832
|
+
*/
|
|
833
|
+
type TangentMirrorMode = "auto" | "none" | "angle" | "all";
|
|
834
|
+
type SegmentView = {
|
|
835
|
+
a: VertexId;
|
|
836
|
+
b: VertexId;
|
|
837
|
+
ta: readonly [number, number];
|
|
838
|
+
tb: readonly [number, number];
|
|
839
|
+
source_verb?: Verb;
|
|
840
|
+
};
|
|
841
|
+
type PathSnapshot = {
|
|
842
|
+
vertices: ReadonlyArray<readonly [number, number]>;
|
|
843
|
+
segments: ReadonlyArray<SegmentView>;
|
|
844
|
+
};
|
|
845
|
+
type SubSelection = {
|
|
846
|
+
vertices: ReadonlyArray<VertexId>;
|
|
847
|
+
segments: ReadonlyArray<SegmentId>;
|
|
848
|
+
tangents: ReadonlyArray<TangentRef>;
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* Per-segment metadata maintained alongside vn's segment array.
|
|
852
|
+
* `meta[i]` corresponds to `network.segments[i]`.
|
|
853
|
+
*/
|
|
854
|
+
type SegmentMeta = {
|
|
855
|
+
/** The SVG verb that originally produced this segment, if known. */source_verb?: Verb; /** Arc-specific metadata for segments born from an `A` command. */
|
|
856
|
+
arc?: ArcMeta; /** True iff this segment was emitted by a `Z` command (closing the subpath). */
|
|
857
|
+
is_close_segment?: boolean;
|
|
858
|
+
};
|
|
859
|
+
/**
|
|
860
|
+
* When an `A` command is parsed, it decomposes to N cubic segments.
|
|
861
|
+
* All segments in the same arc share the same `group_id` and the same
|
|
862
|
+
* arc parameters, plus each carries a snapshot of its original tangents
|
|
863
|
+
* (used at emit time to detect "still an arc" vs "user has edited").
|
|
864
|
+
*/
|
|
865
|
+
type ArcMeta = {
|
|
866
|
+
group_id: number;
|
|
867
|
+
rx: number;
|
|
868
|
+
ry: number;
|
|
869
|
+
x_rot: number;
|
|
870
|
+
large_arc_flag: 0 | 1;
|
|
871
|
+
sweep_flag: 0 | 1; /** Snapshot of this segment's ta at parse time (relative). */
|
|
872
|
+
baseline_ta: cmath.Vector2; /** Snapshot of this segment's tb at parse time (relative). */
|
|
873
|
+
baseline_tb: cmath.Vector2; /** Snapshot of this segment's end-vertex absolute position at parse time. */
|
|
874
|
+
baseline_b_abs: cmath.Vector2; /** Sequence index within the arc group (0..N-1). */
|
|
875
|
+
seq: number; /** Total segments in the arc group. */
|
|
876
|
+
count: number;
|
|
877
|
+
/** Original SVG arc command's `(x, y)` endpoint. Only populated on the
|
|
878
|
+
* LAST segment of the arc group (seq === count - 1). Used by the emitter
|
|
879
|
+
* to write back the exact coordinate the author wrote, avoiding floating-
|
|
880
|
+
* point drift from arc-to-cubic decomposition. */
|
|
881
|
+
original_end?: cmath.Vector2;
|
|
882
|
+
};
|
|
883
|
+
/**
|
|
884
|
+
* Canonical vector-network model for a single SVG path's `d` string.
|
|
885
|
+
*
|
|
886
|
+
* `PathModel` is a self-contained geometry primitive — it parses an SVG
|
|
887
|
+
* path `d` into a vertex/segment graph (with verb hints preserved for
|
|
888
|
+
* round-trip honesty), exposes POJO observers, and serializes back to
|
|
889
|
+
* `d`. It does not hold or reference an `SvgDocument`, an editor
|
|
890
|
+
* instance, the DOM, or any host. It is safe to construct in any
|
|
891
|
+
* environment that can run the package.
|
|
892
|
+
*
|
|
893
|
+
* Public re-exported as a top-level Layer-A primitive from
|
|
894
|
+
* `@grida/svg-editor` for callers that want canonical path geometry
|
|
895
|
+
* without mounting an editor. The full mutation surface (translate /
|
|
896
|
+
* bend / set-tangent / split, etc.) is package-internal and may shift;
|
|
897
|
+
* the publicly-stable contract for external callers is the construction
|
|
898
|
+
* + serialization + observation methods documented at the entry point.
|
|
899
|
+
*
|
|
900
|
+
* @experimental Surface shape is v0; signatures may change before the
|
|
901
|
+
* package reaches semver stability.
|
|
902
|
+
*/
|
|
903
|
+
declare class PathModel {
|
|
904
|
+
private readonly _network;
|
|
905
|
+
private readonly _meta;
|
|
906
|
+
private constructor();
|
|
907
|
+
static fromSvgPathD(d: string): PathModel;
|
|
908
|
+
/** Construct from a vn network with no verb info (every segment defaults to undefined verb). */
|
|
909
|
+
static fromVectorNetwork(network: vn.VectorNetwork): PathModel;
|
|
910
|
+
toSvgPathD(): string;
|
|
911
|
+
snapshot(): PathSnapshot;
|
|
912
|
+
bbox(): cmath.Rectangle;
|
|
913
|
+
vertexCount(): number;
|
|
914
|
+
segmentCount(): number;
|
|
915
|
+
/**
|
|
916
|
+
* If the model's current geometry is still expressible in the source
|
|
917
|
+
* SVG tag's native attribute form, return the equivalent
|
|
918
|
+
* `VectorEditSource` (which is also the writeable shape) — else `null`.
|
|
919
|
+
*
|
|
920
|
+
* This is the decider that gates per-gesture native-attrs writeback in
|
|
921
|
+
* `VectorEditSession.apply_d`. `null` means "the user's edit cannot be
|
|
922
|
+
* faithfully written back to the source tag" — in v1 with no
|
|
923
|
+
* promotion, the gesture is refused; in v1.1+ with promotion, the
|
|
924
|
+
* element is rewritten to `<path d="…">`.
|
|
925
|
+
*
|
|
926
|
+
* v1 expressibility (all source kinds require every segment's `ta` and
|
|
927
|
+
* `tb` to be exactly zero — any tangent edit forces promotion):
|
|
928
|
+
*
|
|
929
|
+
* - **path** — always `null` (no native fallback; the canonical form
|
|
930
|
+
* IS `<path d>`, so callers should just write `d` directly).
|
|
931
|
+
* - **line** — exactly two vertices joined by one straight segment
|
|
932
|
+
* `0→1`. (Topology after a 2-point `vn.fromPolyline` and any sequence
|
|
933
|
+
* of endpoint translates.)
|
|
934
|
+
* - **polyline** — segments form the canonical open chain
|
|
935
|
+
* `0→1, 1→2, …, (n-2)→(n-1)`. (Topology after `vn.fromPolyline` and
|
|
936
|
+
* any sequence of vertex translates.)
|
|
937
|
+
* - **polygon** — segments form the canonical closed chain
|
|
938
|
+
* `0→1, 1→2, …, (n-1)→0`. (Topology after `vn.fromPolygon` and any
|
|
939
|
+
* sequence of vertex translates.)
|
|
940
|
+
* - **rect / circle / ellipse** — always `null`. These geometry
|
|
941
|
+
* primitives have no native writeback target; any vector gesture on
|
|
942
|
+
* them re-types the element to `<path>` (see `vector_apply` /
|
|
943
|
+
* `SvgDocument.retype_to_path`), so they never round-trip through here.
|
|
944
|
+
*
|
|
945
|
+
* Anything that changes segment topology (insert-vertex, delete-vertex,
|
|
946
|
+
* close/open shape) or introduces a curve leaves the canonical chain and
|
|
947
|
+
* returns `null` here; the caller re-types the element to `<path>`.
|
|
948
|
+
*/
|
|
949
|
+
toNativeAttrs(source_tag: VectorEditSource["kind"]): Extract<VectorEditSource, {
|
|
950
|
+
kind: "line" | "polyline" | "polygon";
|
|
951
|
+
}> | null;
|
|
952
|
+
/** Translate one vertex by `delta`. Connected segments follow because
|
|
953
|
+
* tangents are stored relative to vertices. Verb metadata is preserved
|
|
954
|
+
* as-is; emit-time honesty handles cases where the shape no longer
|
|
955
|
+
* matches the recorded verb (e.g. an H whose endpoint y-coord drifts). */
|
|
956
|
+
translateVertex(v: VertexId, delta: readonly [number, number]): PathModel;
|
|
957
|
+
/** Bulk-translate a set of vertices by the same delta. Atomic — either
|
|
958
|
+
* every move succeeds or none (input is validated up-front). */
|
|
959
|
+
translateVertices(indices: ReadonlyArray<VertexId>, delta: readonly [number, number]): PathModel;
|
|
960
|
+
/** Translate one segment by `delta` — moves both endpoints, dragging
|
|
961
|
+
* their tangents along (tangents are stored relative to vertices, so
|
|
962
|
+
* this is automatic). Other segments connected to the moved endpoints
|
|
963
|
+
* also follow at the shared vertex. */
|
|
964
|
+
translateSegment(seg: SegmentId, delta: readonly [number, number]): PathModel;
|
|
965
|
+
/**
|
|
966
|
+
* Bend a curve segment by dragging a point at parameter `ca` to `cb`
|
|
967
|
+
* (cb is in absolute doc-space). Delegates to vn's `bendSegment` —
|
|
968
|
+
* which solves for the new ta/tb that put `B(ca) === cb`, holding the
|
|
969
|
+
* endpoints fixed.
|
|
970
|
+
*
|
|
971
|
+
* The "frozen" snapshot of the segment at gesture start is the caller's
|
|
972
|
+
* responsibility. Convention: call this from a preview session where
|
|
973
|
+
* each frame replays from the baseline (same pattern as translate).
|
|
974
|
+
*/
|
|
975
|
+
bendSegment(seg: SegmentId, ca: number, cb: readonly [number, number], frozen: {
|
|
976
|
+
a: readonly [number, number];
|
|
977
|
+
b: readonly [number, number];
|
|
978
|
+
ta: readonly [number, number];
|
|
979
|
+
tb: readonly [number, number];
|
|
980
|
+
}): PathModel;
|
|
981
|
+
/**
|
|
982
|
+
* Move one tangent control point to a new absolute position. Mirror
|
|
983
|
+
* policy follows vn's `updateTangent`. The other tangent at the same
|
|
984
|
+
* vertex is updated according to the policy.
|
|
985
|
+
*
|
|
986
|
+
* Returns a new PathModel; verb metadata is preserved verbatim.
|
|
987
|
+
* `toSvgPathD` will demote (e.g. L → C) if the new tangents make the
|
|
988
|
+
* recorded verb no longer match the geometry.
|
|
989
|
+
*/
|
|
990
|
+
setTangent(t: TangentRef, abs_pos: readonly [number, number], mirror?: TangentMirrorMode): PathModel;
|
|
991
|
+
/**
|
|
992
|
+
* Split segment `seg` at parametric position `t ∈ [0,1]`, inserting a
|
|
993
|
+
* new vertex. Returns the new model and the **canonical (path-order)**
|
|
994
|
+
* index of the inserted vertex.
|
|
995
|
+
*
|
|
996
|
+
* Verb metadata for the split: the original segment's verb propagates
|
|
997
|
+
* to BOTH halves if it was a curve type (`C`/`S`/`Q`/`T`/`A`); for
|
|
998
|
+
* straight verbs (`L`/`H`/`V`), the split halves stay straight (their
|
|
999
|
+
* tangents are zero from vn's `preserveZero` path when both originals
|
|
1000
|
+
* were zero). Arc-group identity is dropped from the halves — the
|
|
1001
|
+
* arc is broken once split (the emitter will fall back to `C`/`L`).
|
|
1002
|
+
*
|
|
1003
|
+
* **Index space contract.** `VectorNetworkEditor.splitSegment` APPENDS
|
|
1004
|
+
* the new vertex at the end of the network's vertices array — its
|
|
1005
|
+
* index is the in-memory insertion order. But `toSvgPathD` / `fromSvgPathD`
|
|
1006
|
+
* canonicalize vertices in path order, so the same vertex gets a
|
|
1007
|
+
* DIFFERENT index in the d-derived model that consumers re-parse each
|
|
1008
|
+
* frame (e.g., the host's `handle_translate_vertices`). Returning the
|
|
1009
|
+
* insertion-order index causes the classic split-and-drag bug: the
|
|
1010
|
+
* surface holds index N (insertion-order) but the live model has
|
|
1011
|
+
* index M (path-order) at that position — drag moves the wrong vertex
|
|
1012
|
+
* and the user sees "split happened but the new vertex doesn't move".
|
|
1013
|
+
*
|
|
1014
|
+
* To prevent that, we round-trip the post-split model through
|
|
1015
|
+
* `toSvgPathD` → `fromSvgPathD` and return the canonical (path-order)
|
|
1016
|
+
* index of the new vertex. The returned `model` is the canonical
|
|
1017
|
+
* one, so any subsequent op on it uses the same index space the d
|
|
1018
|
+
* roundtrip exposes. See `__tests__/README.md` §"index identity
|
|
1019
|
+
* across the `d` round-trip" for the test pattern that pins this.
|
|
1020
|
+
*/
|
|
1021
|
+
splitSegment(seg: SegmentId, t: number): {
|
|
1022
|
+
model: PathModel;
|
|
1023
|
+
new_vertex: VertexId;
|
|
1024
|
+
};
|
|
1025
|
+
/**
|
|
1026
|
+
* Doc-space position of a tangent control point. `t` references a
|
|
1027
|
+
* segment and which end (`a` or `b`) the tangent belongs to; the
|
|
1028
|
+
* result is `vertex + tangent_value + origin`. Returns null if no
|
|
1029
|
+
* segment has this tangent (e.g. the vertex is isolated).
|
|
1030
|
+
*/
|
|
1031
|
+
tangentAbsolute(t: TangentRef, origin: readonly [number, number]): [number, number] | null;
|
|
1032
|
+
/**
|
|
1033
|
+
* Vertices "neighbouring" the current selection — these are the
|
|
1034
|
+
* vertices whose tangent handles should render in chrome.
|
|
1035
|
+
*
|
|
1036
|
+
* Two-phase, mirrors `editor/grida-canvas/reducers/methods/vector.ts`
|
|
1037
|
+
* `getUXNeighbouringVertices`:
|
|
1038
|
+
*
|
|
1039
|
+
* 1. Collect "active" vertices:
|
|
1040
|
+
* - every selected vertex
|
|
1041
|
+
* - every tangent-owning vertex
|
|
1042
|
+
* - both endpoints of every selected segment
|
|
1043
|
+
* 2. Expand uniformly to 1-hop neighbours (vertices sharing a segment
|
|
1044
|
+
* with any active vertex).
|
|
1045
|
+
*
|
|
1046
|
+
* Without phase 2 for tangent / segment selections, selecting only a
|
|
1047
|
+
* tangent would hide neighbouring-vertex tangents — the user loses
|
|
1048
|
+
* spatial context. Phase 2 makes the affordance symmetric: whatever
|
|
1049
|
+
* triggered selection, the 1-hop ring of tangent handles is visible.
|
|
1050
|
+
*
|
|
1051
|
+
* Sorted ascending; deduped.
|
|
1052
|
+
*/
|
|
1053
|
+
neighbouringVertices(sel: SubSelection): VertexId[];
|
|
1054
|
+
/**
|
|
1055
|
+
* True iff segment `seg`'s curve is entirely contained in the rect.
|
|
1056
|
+
* Delegates to `cmath.bezier.containedByRect`.
|
|
1057
|
+
*/
|
|
1058
|
+
segmentContainedByRect(seg: SegmentId, rect: cmath.Rectangle, origin?: readonly [number, number]): boolean;
|
|
1059
|
+
/** @internal */
|
|
1060
|
+
_rawNetwork(): vn.VectorNetwork;
|
|
1061
|
+
/** @internal */
|
|
1062
|
+
_rawMeta(): ReadonlyArray<SegmentMeta>;
|
|
1063
|
+
/**
|
|
1064
|
+
* Map a `TangentRef` to a concrete `(segment_index, control)` pair.
|
|
1065
|
+
*
|
|
1066
|
+
* `[v, 0]` → first segment whose `a === v` (its `ta`).
|
|
1067
|
+
* `[v, 1]` → first segment whose `b === v` (its `tb`).
|
|
1068
|
+
*
|
|
1069
|
+
* Y-junctions (multi-outgoing or multi-incoming) are uncommon for SVG
|
|
1070
|
+
* `<path>` content; v1 picks the first match. If we ever support those
|
|
1071
|
+
* cleanly, extend `TangentRef` to carry the segment id explicitly.
|
|
1072
|
+
*/
|
|
1073
|
+
private _locateTangent;
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
565
1076
|
//#region src/core/defs.d.ts
|
|
566
1077
|
interface GradientsApi {
|
|
567
1078
|
list(): ReadonlyArray<GradientEntry>;
|
|
@@ -630,6 +1141,21 @@ type KeymapBinding = {
|
|
|
630
1141
|
command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
|
|
631
1142
|
args?: unknown; /** Higher priorities run first in the chain. Default 0. */
|
|
632
1143
|
priority?: number;
|
|
1144
|
+
/**
|
|
1145
|
+
* Bypass the form-element focus guard. When `true`, this binding fires
|
|
1146
|
+
* even if a text input is focused (`<input>`, `<textarea>`, or any
|
|
1147
|
+
* `contentEditable` element).
|
|
1148
|
+
*
|
|
1149
|
+
* Default `false`. The platform's native text-editing shortcuts —
|
|
1150
|
+
* Cmd+A (select all), Cmd+Z/Y (input undo/redo), Cmd+C/V/X
|
|
1151
|
+
* (clipboard), arrow keys, Backspace, Tab, Enter — must win over
|
|
1152
|
+
* editor shortcuts while the user is typing.
|
|
1153
|
+
*
|
|
1154
|
+
* Opt in sparingly. Reasonable candidates: truly global app shortcuts
|
|
1155
|
+
* like Cmd+S (save). Unreasonable candidates: anything that has a
|
|
1156
|
+
* native text-editing meaning.
|
|
1157
|
+
*/
|
|
1158
|
+
allowInFormElement?: boolean;
|
|
633
1159
|
/**
|
|
634
1160
|
* Reserved for V2; not honored by the V1 dispatcher. When added, this
|
|
635
1161
|
* will be evaluated before the handler runs; if false, the binding is
|
|
@@ -675,8 +1201,9 @@ declare class Keymap {
|
|
|
675
1201
|
* bar even when the binding's handler rejects.
|
|
676
1202
|
*
|
|
677
1203
|
* Pure read; runs no handlers, no side effects. Honors the same
|
|
678
|
-
*
|
|
679
|
-
* keystroke isn't "claimed"
|
|
1204
|
+
* form-element focus guard `dispatch` uses, so a typing user's
|
|
1205
|
+
* keystroke isn't "claimed" — and the browser's native text-editing
|
|
1206
|
+
* default (Cmd+A select all, Cmd+Z undo, etc.) wins.
|
|
680
1207
|
*/
|
|
681
1208
|
claims(event: KeyboardEvent): boolean;
|
|
682
1209
|
/**
|
|
@@ -684,6 +1211,12 @@ declare class Keymap {
|
|
|
684
1211
|
* order. Returns `true` on the first handler that consumes; returns
|
|
685
1212
|
* `false` if nothing matched or all matches fell through.
|
|
686
1213
|
*
|
|
1214
|
+
* **Form-element focus guard.** When a text input is focused
|
|
1215
|
+
* (`<input>`, `<textarea>`, contentEditable), bindings are suppressed
|
|
1216
|
+
* by default so the platform's native shortcuts (Cmd+A, Cmd+Z, Cmd+C,
|
|
1217
|
+
* arrow nav, …) are preserved. A binding can opt out of this guard
|
|
1218
|
+
* with `allowInFormElement: true` — see `KeymapBinding`.
|
|
1219
|
+
*
|
|
687
1220
|
* `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
|
|
688
1221
|
* or touch the event in any way. The host decides what to do with the
|
|
689
1222
|
* platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
|
|
@@ -698,7 +1231,6 @@ declare class Keymap {
|
|
|
698
1231
|
* V1.
|
|
699
1232
|
*/
|
|
700
1233
|
private chunkKeysFor;
|
|
701
|
-
private has_safe_mod;
|
|
702
1234
|
}
|
|
703
1235
|
//#endregion
|
|
704
1236
|
//#region src/core/geometry.d.ts
|
|
@@ -731,6 +1263,20 @@ interface GeometryProvider {
|
|
|
731
1263
|
* is hit. "Topmost" is defined by the renderer's z-order.
|
|
732
1264
|
*/
|
|
733
1265
|
node_at_point(p: Vec2): NodeId | null;
|
|
1266
|
+
/**
|
|
1267
|
+
* Re-express a **world-space** delta vector in the frame a node's
|
|
1268
|
+
* position attributes are written in — its parent user-space. For a
|
|
1269
|
+
* node under a scaled/rotated `<g>` ancestor, or inside a nested
|
|
1270
|
+
* `<svg>` viewport that scales its user space, the local frame differs
|
|
1271
|
+
* from world by that linear transform; a translate must be written in
|
|
1272
|
+
* the local frame so the on-screen motion matches the world delta
|
|
1273
|
+
* (otherwise it moves `scale ×` too far).
|
|
1274
|
+
*
|
|
1275
|
+
* Optional: only DOM-backed providers (with a real layout engine) can
|
|
1276
|
+
* derive the frame. Providers that omit it imply the flat-doc identity
|
|
1277
|
+
* (world ≡ local), and callers fall back to the raw delta.
|
|
1278
|
+
*/
|
|
1279
|
+
world_delta_to_local?(id: NodeId, delta: Vec2): Vec2;
|
|
734
1280
|
}
|
|
735
1281
|
type GeometrySignals = {
|
|
736
1282
|
/** Fires when tree shape changes (insert/remove/reorder). */subscribe_structure: (cb: () => void) => Unsubscribe; /** Fires when any bounds-affecting change occurs. */
|
|
@@ -738,7 +1284,7 @@ type GeometrySignals = {
|
|
|
738
1284
|
};
|
|
739
1285
|
/**
|
|
740
1286
|
* Caches `bounds_of` results keyed on `NodeId`; full-clears on either
|
|
741
|
-
* `structure_version` or `geometry_version` bump. See docs/
|
|
1287
|
+
* `structure_version` or `geometry_version` bump. See ../../docs/geometry.md for
|
|
742
1288
|
* why the cache is load-bearing under the surface's per-tick re-render.
|
|
743
1289
|
*/
|
|
744
1290
|
declare class MemoizedGeometryProvider implements GeometryProvider {
|
|
@@ -755,6 +1301,10 @@ declare class MemoizedGeometryProvider implements GeometryProvider {
|
|
|
755
1301
|
*/
|
|
756
1302
|
nodes_in_rect(rect: Rect): NodeId[];
|
|
757
1303
|
node_at_point(p: Vec2): NodeId | null;
|
|
1304
|
+
/** Pass-through. Frame projection depends on live layout, not on the
|
|
1305
|
+
* bounds cache, so there is nothing to memoize. Falls back to the raw
|
|
1306
|
+
* delta when the driver can't resolve a frame. */
|
|
1307
|
+
world_delta_to_local(id: NodeId, delta: Vec2): Vec2;
|
|
758
1308
|
/** Unsubscribe from both signals. Call on surface detach. */
|
|
759
1309
|
dispose(): void;
|
|
760
1310
|
}
|
|
@@ -777,6 +1327,14 @@ type GestureContext = {
|
|
|
777
1327
|
camera: Camera; /** Editor for keymap dispatch / state reads. */
|
|
778
1328
|
editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
|
|
779
1329
|
handle: SurfaceHandle;
|
|
1330
|
+
/**
|
|
1331
|
+
* Predicate returning `true` iff the surface is currently "attended" —
|
|
1332
|
+
* focus inside the container subtree OR pointer over the container.
|
|
1333
|
+
* Gesture bindings whose keydown handlers call `preventDefault()` MUST
|
|
1334
|
+
* consult this before claiming, so the surface doesn't steal page-level
|
|
1335
|
+
* shortcuts when embedded in a larger document. See `util/attention.ts`.
|
|
1336
|
+
*/
|
|
1337
|
+
is_attended: () => boolean;
|
|
780
1338
|
};
|
|
781
1339
|
type GestureBinding = {
|
|
782
1340
|
/** Stable id used by `unbind` / `bindings()`. */id: GestureId;
|
|
@@ -835,11 +1393,41 @@ type CreateSvgEditorOptions = {
|
|
|
835
1393
|
providers?: Providers;
|
|
836
1394
|
style?: Partial<EditorStyle>;
|
|
837
1395
|
};
|
|
838
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* Internal-only members the package's own surfaces reach for. NOT part of
|
|
1398
|
+
* the published API — `_internal` is the surface↔editor bridge, `keymap`
|
|
1399
|
+
* is the keymap dispatcher the DOM surface forwards keyboard events to.
|
|
1400
|
+
* Lives on the runtime object for in-package callers; stripped from the
|
|
1401
|
+
* public `SvgEditor` type so the published `.d.ts` doesn't advertise them.
|
|
1402
|
+
*/
|
|
1403
|
+
type SvgEditorInternalMembers = "_internal" | "keymap";
|
|
1404
|
+
/** Internal handle. Use only inside `@grida/svg-editor`. */
|
|
1405
|
+
type SvgEditorInternal = ReturnType<typeof _create_svg_editor_internal>;
|
|
1406
|
+
/**
|
|
1407
|
+
* The published editor type. `Omit`s the in-package-only surfaces (see
|
|
1408
|
+
* `SvgEditorInternalMembers`) so consumers don't see them in IntelliSense
|
|
1409
|
+
* or the dist `.d.ts`. They still exist on the runtime object — casting
|
|
1410
|
+
* to `SvgEditorInternal` reaches them, but the contract is clear: stay
|
|
1411
|
+
* inside the package.
|
|
1412
|
+
*/
|
|
1413
|
+
type SvgEditor = Omit<SvgEditorInternal, SvgEditorInternalMembers>;
|
|
1414
|
+
/**
|
|
1415
|
+
* Host-provided rendering and input boundary. v0 contract is pure
|
|
1416
|
+
* lifecycle: the editor calls `dispose()` on `editor.detach()` /
|
|
1417
|
+
* `editor.dispose()`; the surface owns its own teardown there (event
|
|
1418
|
+
* listeners, DOM nodes, retained refs).
|
|
1419
|
+
*
|
|
1420
|
+
* **Why so narrow?** The cross-surface vocabulary — paint push, input
|
|
1421
|
+
* routing, hit-testing — isn't pinned yet. The DOM surface re-serializes
|
|
1422
|
+
* the document via `editor.subscribe`, attaches its own listeners, and
|
|
1423
|
+
* owns its own pick. The editor reaches into the DOM surface through
|
|
1424
|
+
* the in-package `_internal` channel (`SurfaceBridge` in
|
|
1425
|
+
* `core/surface-bridge.ts`), not through the public `Surface` type. A
|
|
1426
|
+
* cross-surface paint / input / hit-test contract is deferred until a
|
|
1427
|
+
* second surface implementation arrives and pins each shape (P6 —
|
|
1428
|
+
* public only after dogfooding).
|
|
1429
|
+
*/
|
|
839
1430
|
type Surface = {
|
|
840
|
-
paint(snapshot: unknown): void;
|
|
841
|
-
hit_test(x: number, y: number): NodeId | null;
|
|
842
|
-
on_input(listener: (event: unknown) => void): Unsubscribe;
|
|
843
1431
|
dispose(): void;
|
|
844
1432
|
};
|
|
845
1433
|
type SurfaceHandle = {
|
|
@@ -1021,7 +1609,13 @@ type Commands = {
|
|
|
1021
1609
|
invoke(id: CommandId, args?: unknown): boolean; /** Whether an id has a registered handler. */
|
|
1022
1610
|
has(id: CommandId): boolean;
|
|
1023
1611
|
};
|
|
1024
|
-
|
|
1612
|
+
/**
|
|
1613
|
+
* Wide internal factory — returns the full object including the
|
|
1614
|
+
* `_internal` / `keymap` surfaces in its inferred type. Stays private.
|
|
1615
|
+
* The public `createSvgEditor` below wraps this and narrows the return
|
|
1616
|
+
* to `SvgEditor` so the published `.d.ts` doesn't advertise internals.
|
|
1617
|
+
*/
|
|
1618
|
+
declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
1025
1619
|
/**
|
|
1026
1620
|
* Low-level IR handle. Mutating directly bypasses history; prefer
|
|
1027
1621
|
* `editor.commands` for app code.
|
|
@@ -1110,6 +1704,19 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
|
|
|
1110
1704
|
set_style: (partial: Partial<EditorStyle>) => void;
|
|
1111
1705
|
load: (svg: string) => void;
|
|
1112
1706
|
serialize: () => string;
|
|
1707
|
+
/**
|
|
1708
|
+
* Serialize a single element's subtree as an SVG **fragment**, using the
|
|
1709
|
+
* same trivia-preserving rules as {@link serialize} — for handing "the
|
|
1710
|
+
* markup of the element the user selected" to a downstream consumer
|
|
1711
|
+
* (e.g. an AI agent) without re-serializing the whole document.
|
|
1712
|
+
*
|
|
1713
|
+
* Fragment, not document (see `SvgDocument.serialize_node`): it does NOT
|
|
1714
|
+
* carry `serialize()`'s whole-document round-trip guarantee. Namespace
|
|
1715
|
+
* declarations on an ancestor (`xmlns:xlink`, normally on the root
|
|
1716
|
+
* `<svg>`) are NOT inlined — a node using `xlink:href` serializes without
|
|
1717
|
+
* `xmlns:xlink`. Throws on an unknown id or a non-element node.
|
|
1718
|
+
*/
|
|
1719
|
+
serialize_node(id: NodeId): string;
|
|
1113
1720
|
reset: () => void;
|
|
1114
1721
|
attach: (surface: Surface) => SurfaceHandle;
|
|
1115
1722
|
detach: () => void;
|
|
@@ -1118,22 +1725,33 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
|
|
|
1118
1725
|
_internal: {
|
|
1119
1726
|
doc: SvgDocument;
|
|
1120
1727
|
history: {
|
|
1121
|
-
preview: (label: string) =>
|
|
1728
|
+
preview: (label: string) => import("@grida/history").Preview;
|
|
1729
|
+
};
|
|
1730
|
+
insert_text_preview: (initial: Readonly<Record<string, string>>, opts?: {
|
|
1731
|
+
parent?: NodeId;
|
|
1732
|
+
}) => {
|
|
1733
|
+
id: NodeId;
|
|
1734
|
+
commit(): void;
|
|
1735
|
+
discard(): void;
|
|
1122
1736
|
};
|
|
1123
1737
|
emit: () => void;
|
|
1124
|
-
|
|
1125
|
-
* `commands.translate`. The nudge-dwell watcher subscribes here. */
|
|
1126
|
-
subscribe_translate_commit(cb: () => void): () => void; /** Drag-commit publisher; nudge/translate commands publish directly. */
|
|
1738
|
+
subscribe_translate_commit(cb: () => void): () => void;
|
|
1127
1739
|
notify_translate_commit: () => void;
|
|
1128
1740
|
set_content_edit_driver(fn: ((target: NodeId) => boolean) | null): void;
|
|
1129
|
-
/** Fires the driver immediately with the current override so the
|
|
1130
|
-
* surface can sync state on attach. */
|
|
1131
1741
|
set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
|
|
1132
1742
|
push_surface_hover(id: NodeId | null): void;
|
|
1133
|
-
set_computed_resolver(fn: DomComputedResolver | null): void;
|
|
1743
|
+
set_computed_resolver(fn: DomComputedResolver | null): void;
|
|
1134
1744
|
set_geometry(p: GeometryProvider | null): void;
|
|
1135
1745
|
};
|
|
1136
1746
|
keymap: Keymap;
|
|
1137
1747
|
};
|
|
1748
|
+
/**
|
|
1749
|
+
* Construct a headless SVG editor. The returned object is the public
|
|
1750
|
+
* editor surface — observation (`state`, `subscribe`), commands
|
|
1751
|
+
* (`commands.*`), lifecycle (`attach` / `dispose`), and the typed-read
|
|
1752
|
+
* caches (`node_paint`, `node_properties`). Surfaces (DOM, headless)
|
|
1753
|
+
* attach later via `editor.attach(surface)`.
|
|
1754
|
+
*/
|
|
1755
|
+
declare function createSvgEditor(opts: CreateSvgEditorOptions): SvgEditor;
|
|
1138
1756
|
//#endregion
|
|
1139
|
-
export {
|
|
1757
|
+
export { ReorderDirection as $, EditorState as A, LinearGradientDefinition as B, BoundsResolver as C, ClipboardProvider as D, CameraOptions as E, GradientEntry as F, PaintPreviewSession as G, NodeId as H, GradientStop as I, PropertyValue as J, PaintValue as K, InsertPreviewSession as L, FileIOProvider as M, FontResolver as N, Color as O, GradientDefinition as P, Rect as Q, InsertableTag as R, AlignDirection as S, CameraConstraints as T, Paint as U, Mode as V, PaintFallback as W, Providers as X, Provenance as Y, RadialGradientDefinition as Z, PathModel as _, SelectMode as a, Verb as b, SvgEditor as c, GestureContext as d, TOOL_CURSOR as et, GestureId as f, MemoizedGeometryProvider as g, GeometrySignals as h, DomComputedResolver as i, EditorStyle as j, DEFAULT_STYLE as k, createSvgEditor as l, GeometryProvider as m, CreateSvgEditorOptions as n, Unsubscribe as nt, Surface as o, Gestures as p, PreviewSession as q, DomComputedPaint as r, Vec2 as rt, SurfaceHandle as s, Commands as t, Tool as tt, GestureBinding as u, PathSnapshot as v, Camera as w, VertexId as x, SegmentId as y, InvalidComputedValue as z };
|