@grida/svg-editor 1.0.0-alpha.14 → 1.0.0-alpha.16
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 +59 -0
- package/dist/{dom-Dz_V6q0Y.d.mts → dom-98AUOfsP.d.mts} +44 -2
- package/dist/{dom-D4dy6kq5.d.ts → dom-BO2-E9oK.d.ts} +44 -2
- package/dist/{dom-DSjfCllZ.mjs → dom-DOvcMvl4.mjs} +295 -176
- package/dist/{dom-BuD8TKmL.js → dom-U6ae5fQF.js} +300 -175
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +2 -1
- package/dist/dom.mjs +2 -2
- package/dist/{editor-BHHU_Nvz.js → editor-C6Lj1In-.js} +433 -582
- package/dist/{editor-CJ2KuRh5.d.ts → editor-CYoGJ3Hf.d.ts} +500 -24
- package/dist/{editor-YQwdWHBb.d.mts → editor-D2eQe8lB.d.mts} +500 -24
- package/dist/{editor-B6pchGYk.mjs → editor-DKQOIKuU.mjs} +432 -582
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{model-DqGqV1H4.js → model-D0nU_EkL.js} +1245 -79
- package/dist/{model-DIzZmeyf.mjs → model-L3t9ixT_.mjs} +1240 -80
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +2 -2
- package/dist/presets.mjs +1 -1
- package/dist/react.d.mts +20 -3
- package/dist/react.d.ts +20 -3
- package/dist/react.js +25 -2
- package/dist/react.mjs +25 -3
- package/package.json +29 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cmath from "@grida/cmath";
|
|
2
2
|
import vn from "@grida/vn";
|
|
3
|
-
import { AnyNode } from "@grida/svg/parser";
|
|
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
|
|
|
@@ -22,6 +22,54 @@ type Rect = {
|
|
|
22
22
|
width: number;
|
|
23
23
|
height: number;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* A 2×3 affine transform in SVG `matrix(a b c d e f)` order — the same
|
|
27
|
+
* six-number tuple the SVG `transform="matrix(...)"` function takes.
|
|
28
|
+
*
|
|
29
|
+
* Applied to a point `(x, y)`:
|
|
30
|
+
* x' = a·x + c·y + e
|
|
31
|
+
* y' = b·x + d·y + f
|
|
32
|
+
*
|
|
33
|
+
* This is the wire shape `commands.transform` accepts. Examples:
|
|
34
|
+
* - `[-1, 0, 0, 1, 0, 0]` — horizontal flip (mirror x about the origin)
|
|
35
|
+
* - `[1, 0, 0, -1, 0, 0]` — vertical flip (mirror y about the origin)
|
|
36
|
+
* - `[1, 0, 0, 1, 0, 0]` — identity (no-op)
|
|
37
|
+
*
|
|
38
|
+
* `commands.transform` re-centers this about a pivot, so the bare flip
|
|
39
|
+
* tuples become in-place flips about the selection center.
|
|
40
|
+
*/
|
|
41
|
+
type Matrix2D = readonly [a: number, b: number, c: number, d: number, e: number, f: number];
|
|
42
|
+
/**
|
|
43
|
+
* Observe-only outcome of a discrete pointer **tap** on the canvas: the user
|
|
44
|
+
* pressed and released within the drag threshold, without dragging. Delivered
|
|
45
|
+
* through {@link SvgEditor.subscribe_pick} — a transient event, never part of
|
|
46
|
+
* `EditorState` (it would be stale on the next snapshot).
|
|
47
|
+
*
|
|
48
|
+
* A pick is deliberately **separate from selection**. Selection answers "what
|
|
49
|
+
* do commands target"; a pick answers "what did the user just click, and
|
|
50
|
+
* where". A primary tap on a node both selects it and emits a pick; a tap on
|
|
51
|
+
* empty canvas emits a pick with `node_id: null` (distinguishable from "nothing
|
|
52
|
+
* is selected"); a secondary (right-button) tap emits a pick and does NOT
|
|
53
|
+
* change selection. This is what a click-driven host tool (annotation, context
|
|
54
|
+
* menu, custom selection) needs and selection alone cannot express.
|
|
55
|
+
*
|
|
56
|
+
* Observe-only: a pick reports a click that already happened. It cannot
|
|
57
|
+
* prevent or replace the editor's own selection handling.
|
|
58
|
+
*
|
|
59
|
+
* @unstable Shape is provisional until ≥2 consumers exercise it. Fields may
|
|
60
|
+
* change without a semver bump until then.
|
|
61
|
+
*/
|
|
62
|
+
type PickEvent = {
|
|
63
|
+
/** Document-space point the tap resolved against (the pointer-DOWN point). */point: Vec2; /** Topmost node under `point`, or `null` for empty canvas / background. */
|
|
64
|
+
node_id: NodeId | null; /** Which button produced the tap. `"middle"` is pan and never taps. */
|
|
65
|
+
button: "primary" | "secondary"; /** Modifier snapshot at press time. */
|
|
66
|
+
mods: {
|
|
67
|
+
shift: boolean;
|
|
68
|
+
alt: boolean;
|
|
69
|
+
meta: boolean;
|
|
70
|
+
ctrl: boolean;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
25
73
|
type Mode = "select" | "edit-content";
|
|
26
74
|
/**
|
|
27
75
|
* SVG element tags inserted by the **drag-to-size** subsystem. Closed set;
|
|
@@ -499,18 +547,76 @@ type AlignDirection = "left" | "right" | "top" | "bottom" | "horizontal_centers"
|
|
|
499
547
|
* - All coordinates are in the element's own local space, exactly as
|
|
500
548
|
* authored. No `transform=` resolution, no parent CTM, no viewport
|
|
501
549
|
* remap.
|
|
502
|
-
* - `polyline` / `polygon` points are
|
|
503
|
-
* can hand them straight to
|
|
550
|
+
* - `line` carries its two endpoints; `polyline` / `polygon` points are
|
|
551
|
+
* `[x, y]` tuples so the consumer can hand them straight to
|
|
552
|
+
* `vn.fromPolyline` / `vn.fromPolygon`.
|
|
553
|
+
* - `rect` / `circle` / `ellipse` carry their native geometry numbers.
|
|
554
|
+
* These geometry primitives have no addressable interior vertices in
|
|
555
|
+
* their native form, so editing one as vector geometry re-types the
|
|
556
|
+
* element to `<path>` (see `retype_to_path`). The document holds the
|
|
557
|
+
* native tag until that re-type is committed. Design:
|
|
558
|
+
* `docs/wg/feat-svg-editor/promote-to-path.md`.
|
|
559
|
+
*
|
|
560
|
+
* Re-type vs. native writeback is decided per edit, not per tag: an edit
|
|
561
|
+
* that the source tag can still express (a straight vertex move on
|
|
562
|
+
* `line` / `polyline` / `polygon`) writes back natively; one it cannot (a
|
|
563
|
+
* curve, or a topology change that leaves the tag's canonical form)
|
|
564
|
+
* re-types the element to `<path>`.
|
|
504
565
|
*/
|
|
505
566
|
type VectorEditSource = {
|
|
506
567
|
kind: "path";
|
|
507
568
|
d: string;
|
|
569
|
+
} | {
|
|
570
|
+
kind: "line";
|
|
571
|
+
x1: number;
|
|
572
|
+
y1: number;
|
|
573
|
+
x2: number;
|
|
574
|
+
y2: number;
|
|
508
575
|
} | {
|
|
509
576
|
kind: "polyline";
|
|
510
577
|
points: ReadonlyArray<readonly [number, number]>;
|
|
511
578
|
} | {
|
|
512
579
|
kind: "polygon";
|
|
513
580
|
points: ReadonlyArray<readonly [number, number]>;
|
|
581
|
+
} | {
|
|
582
|
+
kind: "rect";
|
|
583
|
+
x: number;
|
|
584
|
+
y: number;
|
|
585
|
+
width: number;
|
|
586
|
+
height: number; /** Corner radii; `0` when the rect has square corners. */
|
|
587
|
+
rx: number;
|
|
588
|
+
ry: number;
|
|
589
|
+
} | {
|
|
590
|
+
kind: "circle";
|
|
591
|
+
cx: number;
|
|
592
|
+
cy: number;
|
|
593
|
+
r: number;
|
|
594
|
+
} | {
|
|
595
|
+
kind: "ellipse";
|
|
596
|
+
cx: number;
|
|
597
|
+
cy: number;
|
|
598
|
+
rx: number;
|
|
599
|
+
ry: number;
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* Opaque reversal token returned by `retype_to_path`. Callers hold it and
|
|
603
|
+
* hand it back to `revert_retype` to restore the original primitive
|
|
604
|
+
* byte-for-byte; they do not inspect it. All trivia / attribute-token
|
|
605
|
+
* knowledge stays inside `SvgDocument`.
|
|
606
|
+
*/
|
|
607
|
+
type RetypeRecord = {
|
|
608
|
+
readonly prev_local: string;
|
|
609
|
+
readonly prev_raw_tag: string;
|
|
610
|
+
/** Geometry attribute tokens removed on re-type, with their original
|
|
611
|
+
* index in the element's `attrs` array. Ascending by index so they can
|
|
612
|
+
* be spliced back in order. Typed as the document's internal attr token. */
|
|
613
|
+
readonly removed: ReadonlyArray<{
|
|
614
|
+
index: number;
|
|
615
|
+
token: AttrToken;
|
|
616
|
+
}>;
|
|
617
|
+
/** True iff the re-type added a synthetic `fill="none"` (the `<line>`
|
|
618
|
+
* fidelity guard — see `retype_to_path`). `revert_retype` removes it. */
|
|
619
|
+
readonly added_fill_none?: boolean;
|
|
514
620
|
};
|
|
515
621
|
interface DocumentEvents {
|
|
516
622
|
/** Fires after any structural mutation. */
|
|
@@ -553,6 +659,24 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
553
659
|
get structure_version(): number;
|
|
554
660
|
/** See `_geometry_version` for what this counter signals. */
|
|
555
661
|
get geometry_version(): number;
|
|
662
|
+
/**
|
|
663
|
+
* Advance `_geometry_version` by exactly 1 WITHOUT touching the tree,
|
|
664
|
+
* any attribute, `structure_version`, or the `on_change` listeners.
|
|
665
|
+
*
|
|
666
|
+
* The one geometry mutation with no attribute write: a `<text>` /
|
|
667
|
+
* `<tspan>` reflow the IR cannot see — a web font finishing load AFTER
|
|
668
|
+
* the `font-family` / `font-size` write was already serialized. The DOM
|
|
669
|
+
* surface observes the reflow (`document.fonts` `loadingdone`) and asks
|
|
670
|
+
* the geometry channel to advance so the bounds cache re-reads the
|
|
671
|
+
* settled glyph metrics. See ../../docs/geometry.md §Limitations.
|
|
672
|
+
*
|
|
673
|
+
* Deliberately does NOT call `emit()`: this is not a document edit, so
|
|
674
|
+
* it must not bump `doc_version` / mark the doc dirty / touch undo
|
|
675
|
+
* (the editor's `on_change` handler does all three). The editor's
|
|
676
|
+
* `_internal.bump_geometry` advances `geometry_version` here and fans
|
|
677
|
+
* out the geometry listeners itself.
|
|
678
|
+
*/
|
|
679
|
+
bump_geometry(): void;
|
|
556
680
|
private emit;
|
|
557
681
|
/** Notify subscribers — for callers that mutate directly via setAttr/etc. */
|
|
558
682
|
notify(): void;
|
|
@@ -629,24 +753,74 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
629
753
|
* Returns a tag-discriminated snapshot of the authored geometry attrs
|
|
630
754
|
* if this node is eligible for vector (vertex) editing — else `null`.
|
|
631
755
|
*
|
|
632
|
-
*
|
|
756
|
+
* Eligibility:
|
|
633
757
|
* - `<path>` — requires non-empty `d`.
|
|
758
|
+
* - `<line>` — requires two distinct finite user-unit endpoints.
|
|
634
759
|
* - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
|
|
635
760
|
* - `<polygon>` — same as polyline.
|
|
761
|
+
* - `<rect>` — requires finite user-unit `width`/`height` > 0.
|
|
762
|
+
* - `<circle>` — requires finite user-unit `r` > 0.
|
|
763
|
+
* - `<ellipse>` — requires finite user-unit `rx`/`ry` > 0.
|
|
764
|
+
*
|
|
765
|
+
* The vertex tags (`line` / `polyline` / `polygon`) write edits back to
|
|
766
|
+
* their native attributes while the geometry stays expressible there; an
|
|
767
|
+
* edit that escapes the native form (a curve, or a topology change that
|
|
768
|
+
* leaves the canonical chain) re-types the element to `<path>`. The
|
|
769
|
+
* geometry primitives (`rect` / `circle` / `ellipse`) have no native
|
|
770
|
+
* vector form, so any vector edit re-types them. In all cases the native
|
|
771
|
+
* tag is preserved byte-for-byte until the first re-typing edit commits
|
|
772
|
+
* (see `retype_to_path`). Design:
|
|
773
|
+
* `docs/wg/feat-svg-editor/promote-to-path.md`.
|
|
636
774
|
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* (which would have to promote it to `<path>`). Both promotions are
|
|
641
|
-
* out of scope for v1, so opening a `<line>` in vector-edit mode would
|
|
642
|
-
* advertise capabilities that don't work.
|
|
775
|
+
* Geometry that is not a plain user-unit number (`%`, `px`, `em`, …) is
|
|
776
|
+
* an out-of-scope gap, so such an element returns `null` rather than
|
|
777
|
+
* advertising an edit the editor cannot perform faithfully.
|
|
643
778
|
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* transfer, cross-cutting attr carry, DOM-element swap, history-bracket
|
|
647
|
-
* changes) that v1 keeps out of scope.
|
|
779
|
+
* Rejects `<image>` / `<use>` (raster / reference bounding boxes, no
|
|
780
|
+
* editable outline).
|
|
648
781
|
*/
|
|
782
|
+
/**
|
|
783
|
+
* Parse an optional SVG geometry coordinate (`x`/`y`, `cx`/`cy`, the line
|
|
784
|
+
* endpoints). An **absent** attribute takes the SVG default (`0`); a
|
|
785
|
+
* **present** attribute that is not a plain user-unit number (`%`, `px`,
|
|
786
|
+
* `em`, …) is out of scope and yields `null` so the caller refuses the
|
|
787
|
+
* element — the same gate required attrs (width / radius) already apply.
|
|
788
|
+
*
|
|
789
|
+
* The absent-vs-present distinction is the point: a bare `?? 0` would
|
|
790
|
+
* silently coerce an authored `x1="5px"` to `0`, then the first native
|
|
791
|
+
* writeback would overwrite that authored value. Refusing keeps the
|
|
792
|
+
* editor from misrepresenting geometry it cannot read faithfully.
|
|
793
|
+
*/
|
|
794
|
+
private optional_user_unit_coord;
|
|
649
795
|
is_vector_edit_target(id: NodeId): VectorEditSource | null;
|
|
796
|
+
/**
|
|
797
|
+
* Re-type a native vector element (`<line>` / `<polyline>` / `<polygon>` /
|
|
798
|
+
* `<rect>` / `<circle>` / `<ellipse>`) into a `<path>` in place, consuming
|
|
799
|
+
* its native geometry attributes and setting `d`. A structural mutation:
|
|
800
|
+
* this layer executes the re-type; it does not decide when one is
|
|
801
|
+
* warranted.
|
|
802
|
+
*
|
|
803
|
+
* Idempotent: returns `null` if `id` is not currently one of those tags
|
|
804
|
+
* (so it is safe to call repeatedly — once re-typed, e.g. already a
|
|
805
|
+
* `<path>`, further calls are no-ops). Otherwise mutates the node and
|
|
806
|
+
* returns an opaque {@link RetypeRecord} reversal token.
|
|
807
|
+
*
|
|
808
|
+
* Identity, children, `self_closing`, non-geometry attributes, and all
|
|
809
|
+
* source trivia are preserved unchanged — only the tag and the geometry
|
|
810
|
+
* attributes move. Pass the token to {@link revert_retype} to restore
|
|
811
|
+
* the original primitive byte-for-byte.
|
|
812
|
+
*
|
|
813
|
+
* (see test/svg-editor-vector-promote-to-path.md)
|
|
814
|
+
*/
|
|
815
|
+
retype_to_path(id: NodeId, d: string): RetypeRecord | null;
|
|
816
|
+
/**
|
|
817
|
+
* Reverse a {@link retype_to_path}: restore the original tag, remove the
|
|
818
|
+
* `d` attribute the promotion added, and splice the captured geometry
|
|
819
|
+
* attribute tokens back at their original positions (preserving their
|
|
820
|
+
* trivia, so a later `serialize()` is byte-equal to the pre-promotion
|
|
821
|
+
* source).
|
|
822
|
+
*/
|
|
823
|
+
revert_retype(id: NodeId, token: RetypeRecord): void;
|
|
650
824
|
/**
|
|
651
825
|
* True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
|
|
652
826
|
* per-glyph attribute (which conflicts with element-level rotation).
|
|
@@ -673,7 +847,115 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
673
847
|
prefix?: string | null;
|
|
674
848
|
ns?: string | null;
|
|
675
849
|
}): NodeId;
|
|
850
|
+
/** Fresh internal NodeId, guaranteed unique within this document's node
|
|
851
|
+
* map. Shared by `create_element` and fragment adoption — collisions
|
|
852
|
+
* matter for the latter because the parser assigns sequential per-parse
|
|
853
|
+
* ids that a second parse would repeat. */
|
|
854
|
+
private fresh_node_id;
|
|
855
|
+
/**
|
|
856
|
+
* Parse an SVG **fragment** string and adopt its element subtrees into
|
|
857
|
+
* this document's node store — registered like {@link create_element}
|
|
858
|
+
* but NOT inserted into the tree (no version bump, no emit). Callers
|
|
859
|
+
* attach the returned roots via {@link insert}; the editor's
|
|
860
|
+
* `commands.insert_fragment` is the history-bracketed consumer.
|
|
861
|
+
*
|
|
862
|
+
* Input shapes:
|
|
863
|
+
* - A **bare fragment** — one or more sibling elements
|
|
864
|
+
* (`<path …/><path …/>`, or a single `<g>…</g>`). The top-level
|
|
865
|
+
* elements become the returned roots, in source order.
|
|
866
|
+
* - A **full SVG document** — when the input's only top-level element
|
|
867
|
+
* is an `<svg>`, that element is treated as a document SHELL, not
|
|
868
|
+
* content: its element children become the roots and the shell
|
|
869
|
+
* itself (viewBox, width/height, prolog, doctype) is discarded. Its
|
|
870
|
+
* `xmlns:*` prefix declarations are harvested into `xmlns` so the
|
|
871
|
+
* caller can re-declare prefixes the adopted content still uses.
|
|
872
|
+
* An `<svg>` that appears as one of SEVERAL top-level elements (or
|
|
873
|
+
* anywhere below the top level) is content, adopted as-is.
|
|
874
|
+
*
|
|
875
|
+
* Top-level non-element nodes (whitespace between roots, comments, PIs,
|
|
876
|
+
* doctype) are dropped — adoption takes elements, and the host
|
|
877
|
+
* document's own trivia stays untouched. WITHIN each adopted subtree
|
|
878
|
+
* every byte of source trivia survives verbatim (attribute order, quote
|
|
879
|
+
* styles, whitespace, comments), so the inserted markup serializes back
|
|
880
|
+
* exactly as authored — same rules as the initial parse.
|
|
881
|
+
*
|
|
882
|
+
* Authored `id=""` attributes are adopted verbatim — never rewritten,
|
|
883
|
+
* even when they collide with ids already in the document. Silent id
|
|
884
|
+
* renaming is exactly the proprietary noise this editor refuses (README
|
|
885
|
+
* "What clean means" §3); deduplication belongs to the explicit Tidy
|
|
886
|
+
* command. Internal NodeIds ARE freshly assigned (see
|
|
887
|
+
* {@link fresh_node_id}) so adopted nodes never collide in the id map.
|
|
888
|
+
*
|
|
889
|
+
* Throws `TypeError` on a non-string input and `Error` on markup the
|
|
890
|
+
* parser rejects (unclosed / mismatched tags, malformed attributes). An
|
|
891
|
+
* input with no top-level elements (empty string, whitespace, comments
|
|
892
|
+
* only) returns `{ roots: [], xmlns: [] }`.
|
|
893
|
+
*/
|
|
894
|
+
create_fragment(markup: string): {
|
|
895
|
+
roots: NodeId[];
|
|
896
|
+
xmlns: ReadonlyArray<{
|
|
897
|
+
prefix: string;
|
|
898
|
+
uri: string;
|
|
899
|
+
}>;
|
|
900
|
+
};
|
|
901
|
+
/**
|
|
902
|
+
* Register `node` and its whole subtree (from a foreign parse) into this
|
|
903
|
+
* document's node map under fresh NodeIds. The parser assigns sequential
|
|
904
|
+
* per-parse ids (`n0`, `n1`, …), so adopting without a remap would
|
|
905
|
+
* collide with this document's own nodes. Children links are rewritten;
|
|
906
|
+
* the subtree root arrives detached (`parent: null`), like
|
|
907
|
+
* `create_element`. Mutates the parsed nodes in place — a parse result
|
|
908
|
+
* is single-use.
|
|
909
|
+
*/
|
|
910
|
+
private adopt_parsed_subtree;
|
|
911
|
+
/**
|
|
912
|
+
* Namespace prefixes USED within `id`'s subtree (element tags and
|
|
913
|
+
* attribute names) that are not DECLARED within the subtree itself —
|
|
914
|
+
* i.e. prefixes the subtree borrows from ancestor scope. `xml` and
|
|
915
|
+
* `xmlns` are excluded (bound by the XML spec, never declared).
|
|
916
|
+
* Declaration scoping is honored per use-site: a prefix declared on the
|
|
917
|
+
* using element or any of its ancestors up to (and including) the
|
|
918
|
+
* subtree root counts as declared.
|
|
919
|
+
*
|
|
920
|
+
* Structural fact only — the caller decides what an unbound prefix
|
|
921
|
+
* means (e.g. `commands.insert_fragment` hoists a resolvable
|
|
922
|
+
* declaration onto the document root).
|
|
923
|
+
*/
|
|
924
|
+
undeclared_ns_prefixes(id: NodeId): ReadonlySet<string>;
|
|
925
|
+
/**
|
|
926
|
+
* Declare a namespace prefix on the ROOT element: appends
|
|
927
|
+
* `xmlns:<prefix>="<uri>"` when the root doesn't already declare that
|
|
928
|
+
* prefix. An authored declaration always wins — this never rebinds.
|
|
929
|
+
* Policy wrapper over {@link set_attr} in the `XMLNS_NS` space; removal
|
|
930
|
+
* works through `set_attr(root, prefix, null, XMLNS_NS)` as usual.
|
|
931
|
+
*/
|
|
932
|
+
declare_xmlns(prefix: string, uri: string): void;
|
|
676
933
|
serialize(): string;
|
|
934
|
+
/**
|
|
935
|
+
* Serialize a single element's subtree as an SVG **fragment**, using the
|
|
936
|
+
* same trivia-preserving rules as {@link serialize} (attribute order,
|
|
937
|
+
* quote style, whitespace, comments — emitted exactly as authored).
|
|
938
|
+
*
|
|
939
|
+
* This is NOT {@link serialize} scoped to a node — it is a deliberately
|
|
940
|
+
* weaker output (sdk-design D3, asymmetric outputs stay separate):
|
|
941
|
+
*
|
|
942
|
+
* - `serialize()` emits the whole document and carries the P1
|
|
943
|
+
* whole-document round-trip guarantee.
|
|
944
|
+
* - `serialize_node()` emits a fragment and does NOT. Namespace
|
|
945
|
+
* declarations that live on an ancestor (`xmlns:xlink` and friends,
|
|
946
|
+
* normally on the root `<svg>`) are NOT inlined — a node using
|
|
947
|
+
* `xlink:href` serializes without `xmlns:xlink`. The fragment is the
|
|
948
|
+
* element's markup as authored, not a standalone parseable document.
|
|
949
|
+
*
|
|
950
|
+
* Throws on an unknown id, a non-element node, or a node detached from
|
|
951
|
+
* the live tree: the contract is "the markup for a selected element,"
|
|
952
|
+
* selections are always live elements, and a string return of `""` for a
|
|
953
|
+
* bad id would hide consumer bugs. The detached case matters because
|
|
954
|
+
* `remove()` keeps the node in the id map for undo — a stale id from a
|
|
955
|
+
* removed node would otherwise serialize content no longer in the
|
|
956
|
+
* document, silently feeding a consumer deleted markup.
|
|
957
|
+
*/
|
|
958
|
+
serialize_node(id: NodeId): string;
|
|
677
959
|
private emit_node;
|
|
678
960
|
private emit_attr;
|
|
679
961
|
}
|
|
@@ -795,20 +1077,26 @@ declare class PathModel {
|
|
|
795
1077
|
*
|
|
796
1078
|
* - **path** — always `null` (no native fallback; the canonical form
|
|
797
1079
|
* IS `<path d>`, so callers should just write `d` directly).
|
|
1080
|
+
* - **line** — exactly two vertices joined by one straight segment
|
|
1081
|
+
* `0→1`. (Topology after a 2-point `vn.fromPolyline` and any sequence
|
|
1082
|
+
* of endpoint translates.)
|
|
798
1083
|
* - **polyline** — segments form the canonical open chain
|
|
799
1084
|
* `0→1, 1→2, …, (n-2)→(n-1)`. (Topology after `vn.fromPolyline` and
|
|
800
1085
|
* any sequence of vertex translates.)
|
|
801
1086
|
* - **polygon** — segments form the canonical closed chain
|
|
802
1087
|
* `0→1, 1→2, …, (n-1)→0`. (Topology after `vn.fromPolygon` and any
|
|
803
1088
|
* sequence of vertex translates.)
|
|
1089
|
+
* - **rect / circle / ellipse** — always `null`. These geometry
|
|
1090
|
+
* primitives have no native writeback target; any vector gesture on
|
|
1091
|
+
* them re-types the element to `<path>` (see `vector_apply` /
|
|
1092
|
+
* `SvgDocument.retype_to_path`), so they never round-trip through here.
|
|
804
1093
|
*
|
|
805
1094
|
* Anything that changes segment topology (insert-vertex, delete-vertex,
|
|
806
|
-
* close/open shape) leaves the canonical chain and
|
|
807
|
-
*
|
|
808
|
-
* (intra-Vertex or to-path).
|
|
1095
|
+
* close/open shape) or introduces a curve leaves the canonical chain and
|
|
1096
|
+
* returns `null` here; the caller re-types the element to `<path>`.
|
|
809
1097
|
*/
|
|
810
|
-
toNativeAttrs(source_tag: VectorEditSource["kind"]):
|
|
811
|
-
kind: "
|
|
1098
|
+
toNativeAttrs(source_tag: VectorEditSource["kind"]): Extract<VectorEditSource, {
|
|
1099
|
+
kind: "line" | "polyline" | "polygon";
|
|
812
1100
|
}> | null;
|
|
813
1101
|
/** Translate one vertex by `delta`. Connected segments follow because
|
|
814
1102
|
* tangents are stored relative to vertices. Verb metadata is preserved
|
|
@@ -1124,6 +1412,20 @@ interface GeometryProvider {
|
|
|
1124
1412
|
* is hit. "Topmost" is defined by the renderer's z-order.
|
|
1125
1413
|
*/
|
|
1126
1414
|
node_at_point(p: Vec2): NodeId | null;
|
|
1415
|
+
/**
|
|
1416
|
+
* Re-express a **world-space** delta vector in the frame a node's
|
|
1417
|
+
* position attributes are written in — its parent user-space. For a
|
|
1418
|
+
* node under a scaled/rotated `<g>` ancestor, or inside a nested
|
|
1419
|
+
* `<svg>` viewport that scales its user space, the local frame differs
|
|
1420
|
+
* from world by that linear transform; a translate must be written in
|
|
1421
|
+
* the local frame so the on-screen motion matches the world delta
|
|
1422
|
+
* (otherwise it moves `scale ×` too far).
|
|
1423
|
+
*
|
|
1424
|
+
* Optional: only DOM-backed providers (with a real layout engine) can
|
|
1425
|
+
* derive the frame. Providers that omit it imply the flat-doc identity
|
|
1426
|
+
* (world ≡ local), and callers fall back to the raw delta.
|
|
1427
|
+
*/
|
|
1428
|
+
world_delta_to_local?(id: NodeId, delta: Vec2): Vec2;
|
|
1127
1429
|
}
|
|
1128
1430
|
type GeometrySignals = {
|
|
1129
1431
|
/** Fires when tree shape changes (insert/remove/reorder). */subscribe_structure: (cb: () => void) => Unsubscribe; /** Fires when any bounds-affecting change occurs. */
|
|
@@ -1148,6 +1450,10 @@ declare class MemoizedGeometryProvider implements GeometryProvider {
|
|
|
1148
1450
|
*/
|
|
1149
1451
|
nodes_in_rect(rect: Rect): NodeId[];
|
|
1150
1452
|
node_at_point(p: Vec2): NodeId | null;
|
|
1453
|
+
/** Pass-through. Frame projection depends on live layout, not on the
|
|
1454
|
+
* bounds cache, so there is nothing to memoize. Falls back to the raw
|
|
1455
|
+
* delta when the driver can't resolve a frame. */
|
|
1456
|
+
world_delta_to_local(id: NodeId, delta: Vec2): Vec2;
|
|
1151
1457
|
/** Unsubscribe from both signals. Call on surface detach. */
|
|
1152
1458
|
dispose(): void;
|
|
1153
1459
|
}
|
|
@@ -1325,16 +1631,46 @@ type Commands = {
|
|
|
1325
1631
|
* member.
|
|
1326
1632
|
*
|
|
1327
1633
|
* The default selection is `state.selection`. Pass `opts.ids` to
|
|
1328
|
-
* override. Members
|
|
1329
|
-
* (e.g. `<g>`)
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1634
|
+
* override. Members that are not resizable are skipped silently: this
|
|
1635
|
+
* means both an unresizable tag (e.g. `<g>`) AND a resizable tag carrying
|
|
1636
|
+
* a non-trivial transform (rotate-without-pivot, matrix, scale, skew),
|
|
1637
|
+
* which can't be resized in local space without breaking round-trip — the
|
|
1638
|
+
* same `is_resizable_node` gate the resize HUD applies. The gesture is a
|
|
1639
|
+
* no-op when no resizable member remains. Returns `true` when a history
|
|
1640
|
+
* step was pushed. `opts.label` overrides the atomic history label
|
|
1641
|
+
* (default `"resize-to"`).
|
|
1332
1642
|
*/
|
|
1333
1643
|
resize_to(target: {
|
|
1334
1644
|
x: number;
|
|
1335
1645
|
y: number;
|
|
1336
1646
|
width: number;
|
|
1337
1647
|
height: number;
|
|
1648
|
+
}, opts?: {
|
|
1649
|
+
ids?: ReadonlyArray<NodeId>;
|
|
1650
|
+
label?: string;
|
|
1651
|
+
}): boolean;
|
|
1652
|
+
/**
|
|
1653
|
+
* Resize the selection by a delta — PER-ELEMENT: each selected member
|
|
1654
|
+
* grows/shrinks around its OWN NW corner, so members keep their positions
|
|
1655
|
+
* relative to one another (NOT a union/group resize — contrast
|
|
1656
|
+
* {@link resize_to}, which scales the whole selection around the shared
|
|
1657
|
+
* union origin and so translates off-origin members). `delta.dw` /
|
|
1658
|
+
* `delta.dh` are applied additively to each member (clamped to >= 0). The
|
|
1659
|
+
* core verb behind keyboard nudge-resize.
|
|
1660
|
+
*
|
|
1661
|
+
* ALL-OR-NOTHING gate: refuses (returns `false`, no history step) unless
|
|
1662
|
+
* EVERY member passes `is_resizable_node` — the same tag + transform-class
|
|
1663
|
+
* check the resize HUD uses, applied wholesale (a mixed selection is
|
|
1664
|
+
* refused, not partially resized — matches a HUD handle-drag, which is
|
|
1665
|
+
* rejected when any member is unsafe). Also refuses on empty selection or
|
|
1666
|
+
* when no geometry provider (DOM surface) is attached.
|
|
1667
|
+
*
|
|
1668
|
+
* Per-tag constraints (circle uniform, text edge no-op) apply per member.
|
|
1669
|
+
* The default selection is `state.selection`; pass `opts.ids` to override.
|
|
1670
|
+
*/
|
|
1671
|
+
resize_by(delta: {
|
|
1672
|
+
dw: number;
|
|
1673
|
+
dh: number;
|
|
1338
1674
|
}, opts?: {
|
|
1339
1675
|
ids?: ReadonlyArray<NodeId>;
|
|
1340
1676
|
}): boolean;
|
|
@@ -1370,6 +1706,39 @@ type Commands = {
|
|
|
1370
1706
|
y: number;
|
|
1371
1707
|
};
|
|
1372
1708
|
}): boolean;
|
|
1709
|
+
/**
|
|
1710
|
+
* Compose an arbitrary 2×3 affine onto the selection, **relative** and
|
|
1711
|
+
* applied in **world space about a pivot**. `matrix` is in SVG
|
|
1712
|
+
* `matrix(a b c d e f)` order (see {@link Matrix2D}).
|
|
1713
|
+
*
|
|
1714
|
+
* Semantics: the effective affine written to each member is
|
|
1715
|
+
* `E = T(pivot) · matrix · T(-pivot)`, so the bare flip tuples become
|
|
1716
|
+
* in-place flips about the pivot. Pivot defaults to the selection
|
|
1717
|
+
* union-bbox center (via the attached surface's `geometry_provider`);
|
|
1718
|
+
* pass `opts.pivot` to override.
|
|
1719
|
+
*
|
|
1720
|
+
* Round-trip: `E` is folded onto each member's transform list as a
|
|
1721
|
+
* single LEADING `matrix` op — existing `rotate`/`translate` tokens are
|
|
1722
|
+
* preserved after it, repeated applies collapse into one matrix, and a
|
|
1723
|
+
* net-identity leading matrix is dropped (so flip-then-flip restores
|
|
1724
|
+
* the original). One atomic history step labelled `"transform"`.
|
|
1725
|
+
*
|
|
1726
|
+
* Refusal (returns `false`, no-op, no history): empty selection, no
|
|
1727
|
+
* `geometry_provider`, or any member failing `is_rotatable` (the same
|
|
1728
|
+
* non-trivial-transform / `<text rotate>` / CSS-property / animated
|
|
1729
|
+
* gate `rotate` uses). All-or-nothing — no partial writes.
|
|
1730
|
+
*
|
|
1731
|
+
* Flat-doc limitation: only each element's OWN transform is folded;
|
|
1732
|
+
* the pivot is treated as world ≡ parent space. Nested transformed
|
|
1733
|
+
* ancestors (`<g transform=…>`) are out of scope.
|
|
1734
|
+
*/
|
|
1735
|
+
transform(matrix: Matrix2D, opts?: {
|
|
1736
|
+
ids?: ReadonlyArray<NodeId>;
|
|
1737
|
+
pivot?: {
|
|
1738
|
+
x: number;
|
|
1739
|
+
y: number;
|
|
1740
|
+
};
|
|
1741
|
+
}): boolean;
|
|
1373
1742
|
/**
|
|
1374
1743
|
* Collapse each selected member's `transform=` to a single `matrix(...)`
|
|
1375
1744
|
* token, baking accumulated translates / rotates / scales / skews into
|
|
@@ -1404,6 +1773,30 @@ type Commands = {
|
|
|
1404
1773
|
* rejected the call.
|
|
1405
1774
|
*/
|
|
1406
1775
|
group(): boolean;
|
|
1776
|
+
/**
|
|
1777
|
+
* Dissolve the selected `<g>` (or `opts.id`), hoisting its children
|
|
1778
|
+
* into the group's parent at the group's z-position. Returns `true`
|
|
1779
|
+
* when a history step was pushed (children hoisted, group removed, the
|
|
1780
|
+
* former children selected); `false` when the call was refused.
|
|
1781
|
+
*
|
|
1782
|
+
* Only the **safe clean-structural subset** is accepted (see
|
|
1783
|
+
* `core/group.ts:plan_ungroup` and `../docs/grouping.md` §Ungrouping).
|
|
1784
|
+
* Refused — with NO mutation and NO history entry — when: the target
|
|
1785
|
+
* is not a single `<g>`; the group is inside `<defs>`; the group has
|
|
1786
|
+
* no element children; the group carries any own attribute beyond
|
|
1787
|
+
* `{ transform, id, data-grida-id }` (i.e. any visual / cascade state
|
|
1788
|
+
* such as `opacity` / `class` / `style` / `filter` / `clip-path` /
|
|
1789
|
+
* `mask` / `fill`); the group's `id` is referenced by a `<use>`; a
|
|
1790
|
+
* direct child is an SMIL animation element; or — when the group has a
|
|
1791
|
+
* `transform` — any child's own transform is unparseable.
|
|
1792
|
+
*
|
|
1793
|
+
* When the group has a `transform`, it is BAKED into each child by
|
|
1794
|
+
* prepending the group's parsed ops to the child's (clean token
|
|
1795
|
+
* compose, not a matrix collapse), so paint output round-trips.
|
|
1796
|
+
*/
|
|
1797
|
+
ungroup(opts?: {
|
|
1798
|
+
id?: NodeId;
|
|
1799
|
+
}): boolean;
|
|
1407
1800
|
/**
|
|
1408
1801
|
* Atomic one-shot insertion. Creates a new element of the given SVG
|
|
1409
1802
|
* tag with the supplied attributes (merged on top of the package's
|
|
@@ -1420,6 +1813,63 @@ type Commands = {
|
|
|
1420
1813
|
index?: number;
|
|
1421
1814
|
select?: boolean;
|
|
1422
1815
|
}): NodeId;
|
|
1816
|
+
/**
|
|
1817
|
+
* Atomic insertion of a pre-authored SVG **fragment** — one or more
|
|
1818
|
+
* sibling elements as markup (`"<g …><path …/></g>"`), or a full
|
|
1819
|
+
* `<svg>` document whose element children are taken as the content
|
|
1820
|
+
* (the `<svg>` shell — viewBox, width/height, prolog, doctype — is
|
|
1821
|
+
* discarded; an `<svg>` that is one of several top-level elements is
|
|
1822
|
+
* content and inserted as-is). The element subtrees are adopted
|
|
1823
|
+
* verbatim — every byte of trivia inside each element survives
|
|
1824
|
+
* (attribute order, quote styles, whitespace, comments) — inserted
|
|
1825
|
+
* contiguously in source order at `opts.parent` / `opts.index`, and
|
|
1826
|
+
* selected. ONE history step regardless of fragment size; a single
|
|
1827
|
+
* `undo()` restores the exact pre-insert serialization. Returns the
|
|
1828
|
+
* inserted top-level ids in document order.
|
|
1829
|
+
*
|
|
1830
|
+
* This is the markup-shaped sibling of {@link insert} — the primitive
|
|
1831
|
+
* paste and asset-stamping flows compose. Use `insert` for a tag +
|
|
1832
|
+
* attrs; use `insert_fragment` for markup.
|
|
1833
|
+
*
|
|
1834
|
+
* **Position is authored content.** There is deliberately no placement
|
|
1835
|
+
* opt: to land a fragment at a document-space point, author the
|
|
1836
|
+
* position into the markup before inserting — wrap it in
|
|
1837
|
+
* `<g transform="translate(x y)">…</g>` or set the elements' own
|
|
1838
|
+
* geometry attrs. Placement then round-trips as ordinary markup and
|
|
1839
|
+
* the whole drop is the same single undo step.
|
|
1840
|
+
*
|
|
1841
|
+
* **`id` collisions:** authored `id=""` attributes are inserted
|
|
1842
|
+
* verbatim, NEVER rewritten — silent id renaming is proprietary noise
|
|
1843
|
+
* (P1; README "What clean means" §3). When a fragment id collides
|
|
1844
|
+
* with an existing one, reference resolution (`url(#…)`, `href`)
|
|
1845
|
+
* follows the document-order rules of the host renderer; resolving
|
|
1846
|
+
* the duplication is the explicit Tidy command's job, not insertion's.
|
|
1847
|
+
*
|
|
1848
|
+
* **Namespaces:** when the fragment uses a prefix the document root
|
|
1849
|
+
* doesn't declare, the declaration is hoisted onto the root as part
|
|
1850
|
+
* of the same history step — `xlink` (well-known URI) and any prefix
|
|
1851
|
+
* the discarded `<svg>` shell declared. A prefix whose URI is not
|
|
1852
|
+
* discoverable is left as authored (the input was equally unbound as
|
|
1853
|
+
* a standalone document). An authored root declaration always wins —
|
|
1854
|
+
* never rebound.
|
|
1855
|
+
*
|
|
1856
|
+
* **Refusals:** an input with no top-level elements (empty /
|
|
1857
|
+
* whitespace / comments-only) returns `[]` with NO history step.
|
|
1858
|
+
* Throws on malformed markup (parser errors propagate), on a
|
|
1859
|
+
* non-string input, and on an `opts.parent` that isn't a live element
|
|
1860
|
+
* of the current document — a silent no-op there would hide consumer
|
|
1861
|
+
* bugs (same stance as `serialize_node`).
|
|
1862
|
+
*
|
|
1863
|
+
* `opts.parent` defaults to root; `opts.index` (position in the
|
|
1864
|
+
* parent's element-children list; the whole fragment lands
|
|
1865
|
+
* contiguously at it) defaults to append; `opts.select` defaults to
|
|
1866
|
+
* `true`.
|
|
1867
|
+
*/
|
|
1868
|
+
insert_fragment(svg: string, opts?: {
|
|
1869
|
+
parent?: NodeId;
|
|
1870
|
+
index?: number;
|
|
1871
|
+
select?: boolean;
|
|
1872
|
+
}): NodeId[];
|
|
1423
1873
|
/**
|
|
1424
1874
|
* Preview-bracketed insertion for drag-to-size gestures. Creates and
|
|
1425
1875
|
* inserts the node immediately (so HUD selection chrome renders);
|
|
@@ -1528,6 +1978,17 @@ declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
|
1528
1978
|
* Cheap channel — does NOT bump `state.version`.
|
|
1529
1979
|
*/
|
|
1530
1980
|
subscribe_surface_hover(cb: () => void): () => void;
|
|
1981
|
+
/**
|
|
1982
|
+
* Subscribe to pick (tap) outcomes — a discrete click on the canvas,
|
|
1983
|
+
* reporting the document-space point and the node under it (`null` for
|
|
1984
|
+
* empty canvas), plus the button and modifier snapshot. Fires once per
|
|
1985
|
+
* tap, after the editor's own selection handling. Observe-only: a pick
|
|
1986
|
+
* cannot alter selection, and the channel does NOT bump `state.version`.
|
|
1987
|
+
* See {@link PickEvent}.
|
|
1988
|
+
*
|
|
1989
|
+
* @unstable
|
|
1990
|
+
*/
|
|
1991
|
+
subscribe_pick(cb: (e: PickEvent) => void): Unsubscribe;
|
|
1531
1992
|
/**
|
|
1532
1993
|
* Subscribe to bounds-affecting changes. Fires when any document
|
|
1533
1994
|
* mutation advances `state.geometry_version` — drag, resize, text
|
|
@@ -1547,6 +2008,19 @@ declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
|
1547
2008
|
set_style: (partial: Partial<EditorStyle>) => void;
|
|
1548
2009
|
load: (svg: string) => void;
|
|
1549
2010
|
serialize: () => string;
|
|
2011
|
+
/**
|
|
2012
|
+
* Serialize a single element's subtree as an SVG **fragment**, using the
|
|
2013
|
+
* same trivia-preserving rules as {@link serialize} — for handing "the
|
|
2014
|
+
* markup of the element the user selected" to a downstream consumer
|
|
2015
|
+
* (e.g. an AI agent) without re-serializing the whole document.
|
|
2016
|
+
*
|
|
2017
|
+
* Fragment, not document (see `SvgDocument.serialize_node`): it does NOT
|
|
2018
|
+
* carry `serialize()`'s whole-document round-trip guarantee. Namespace
|
|
2019
|
+
* declarations on an ancestor (`xmlns:xlink`, normally on the root
|
|
2020
|
+
* `<svg>`) are NOT inlined — a node using `xlink:href` serializes without
|
|
2021
|
+
* `xmlns:xlink`. Throws on an unknown id or a non-element node.
|
|
2022
|
+
*/
|
|
2023
|
+
serialize_node(id: NodeId): string;
|
|
1550
2024
|
reset: () => void;
|
|
1551
2025
|
attach: (surface: Surface) => SurfaceHandle;
|
|
1552
2026
|
detach: () => void;
|
|
@@ -1570,8 +2044,10 @@ declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
|
1570
2044
|
set_content_edit_driver(fn: ((target: NodeId) => boolean) | null): void;
|
|
1571
2045
|
set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
|
|
1572
2046
|
push_surface_hover(id: NodeId | null): void;
|
|
2047
|
+
push_pick(e: PickEvent): void;
|
|
1573
2048
|
set_computed_resolver(fn: DomComputedResolver | null): void;
|
|
1574
2049
|
set_geometry(p: GeometryProvider | null): void;
|
|
2050
|
+
bump_geometry(): void;
|
|
1575
2051
|
};
|
|
1576
2052
|
keymap: Keymap;
|
|
1577
2053
|
};
|
|
@@ -1584,4 +2060,4 @@ declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
|
1584
2060
|
*/
|
|
1585
2061
|
declare function createSvgEditor(opts: CreateSvgEditorOptions): SvgEditor;
|
|
1586
2062
|
//#endregion
|
|
1587
|
-
export {
|
|
2063
|
+
export { RadialGradientDefinition as $, EditorState as A, LinearGradientDefinition as B, BoundsResolver as C, ClipboardProvider as D, CameraOptions as E, GradientEntry as F, PaintFallback as G, Mode as H, GradientStop as I, PickEvent as J, PaintPreviewSession as K, InsertPreviewSession as L, FileIOProvider as M, FontResolver as N, Color as O, GradientDefinition as P, Providers as Q, InsertableTag as R, AlignDirection as S, CameraConstraints as T, NodeId as U, Matrix2D as V, Paint as W, PropertyValue as X, PreviewSession as Y, Provenance as Z, PathModel as _, SelectMode as a, Vec2 as at, Verb as b, SvgEditor as c, GestureContext as d, Rect as et, GestureId as f, MemoizedGeometryProvider as g, GeometrySignals as h, DomComputedResolver as i, Unsubscribe as it, EditorStyle as j, DEFAULT_STYLE as k, createSvgEditor as l, GeometryProvider as m, CreateSvgEditorOptions as n, TOOL_CURSOR as nt, Surface as o, Gestures as p, PaintValue as q, DomComputedPaint as r, Tool as rt, SurfaceHandle as s, Commands as t, ReorderDirection as tt, GestureBinding as u, PathSnapshot as v, Camera as w, VertexId as x, SegmentId as y, InvalidComputedValue as z };
|