@grida/svg-editor 1.0.0-alpha.13 → 1.0.0-alpha.14
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 +88 -42
- package/dist/{dom-Cvm9Towu.js → dom-BuD8TKmL.js} +1592 -624
- package/dist/dom-D4dy6kq5.d.ts +112 -0
- package/dist/{dom-BlMk07oX.mjs → dom-DSjfCllZ.mjs} +1566 -617
- package/dist/dom-Dz_V6q0Y.d.mts +114 -0
- 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-DtuRIs-Q.mjs → editor-B6pchGYk.mjs} +519 -321
- package/dist/{editor-CdyC3uAe.js → editor-BHHU_Nvz.js} +527 -329
- package/dist/{editor-Bd4-VCEJ.d.ts → editor-CJ2KuRh5.d.ts} +472 -24
- package/dist/{editor-BH03X8cX.d.mts → editor-YQwdWHBb.d.mts} +472 -24
- 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-DIzZmeyf.mjs +3677 -0
- package/dist/model-DqGqV1H4.js +3823 -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 +9 -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 vn from "@grida/vn";
|
|
2
3
|
import { AnyNode } from "@grida/svg/parser";
|
|
3
|
-
import * as _$_grida_history0 from "@grida/history";
|
|
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,33 @@ 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
|
+
* - `polyline` / `polygon` points are `[x, y]` tuples so the consumer
|
|
503
|
+
* can hand them straight to `vn.fromPolyline` / `vn.fromPolygon`.
|
|
504
|
+
*/
|
|
505
|
+
type VectorEditSource = {
|
|
506
|
+
kind: "path";
|
|
507
|
+
d: string;
|
|
508
|
+
} | {
|
|
509
|
+
kind: "polyline";
|
|
510
|
+
points: ReadonlyArray<readonly [number, number]>;
|
|
511
|
+
} | {
|
|
512
|
+
kind: "polygon";
|
|
513
|
+
points: ReadonlyArray<readonly [number, number]>;
|
|
514
|
+
};
|
|
437
515
|
interface DocumentEvents {
|
|
438
516
|
/** Fires after any structural mutation. */
|
|
439
517
|
on_change(fn: () => void): () => void;
|
|
@@ -462,7 +540,7 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
462
540
|
private _structure_version;
|
|
463
541
|
/** Bumps on writes that can shift world-space bounds (`GEOMETRY_ATTRS`,
|
|
464
542
|
* `set_text`, `insert`, `remove`). Cache key for `GeometryProvider`;
|
|
465
|
-
* see docs/
|
|
543
|
+
* see ../../docs/geometry.md. */
|
|
466
544
|
private _geometry_version;
|
|
467
545
|
constructor(svg: string);
|
|
468
546
|
static parse(svg: string): SvgDocument;
|
|
@@ -547,6 +625,44 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
547
625
|
* or its parent text.
|
|
548
626
|
*/
|
|
549
627
|
is_text_edit_target(id: NodeId): boolean;
|
|
628
|
+
/**
|
|
629
|
+
* Returns a tag-discriminated snapshot of the authored geometry attrs
|
|
630
|
+
* if this node is eligible for vector (vertex) editing — else `null`.
|
|
631
|
+
*
|
|
632
|
+
* v1 eligibility:
|
|
633
|
+
* - `<path>` — requires non-empty `d`.
|
|
634
|
+
* - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
|
|
635
|
+
* - `<polygon>` — same as polyline.
|
|
636
|
+
*
|
|
637
|
+
* Deliberately rejects `<line>` in v1: the only useful vertex-edit
|
|
638
|
+
* gestures on a `<line>` are (a) introducing a new vertex (which would
|
|
639
|
+
* have to promote it to `<polyline>`) and (b) bending it with a tangent
|
|
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.
|
|
643
|
+
*
|
|
644
|
+
* Also rejects `<rect>`, `<circle>`, `<ellipse>`, `<image>`, `<use>` —
|
|
645
|
+
* those would force the same promotion-to-`<path>` machinery (trivia
|
|
646
|
+
* transfer, cross-cutting attr carry, DOM-element swap, history-bracket
|
|
647
|
+
* changes) that v1 keeps out of scope.
|
|
648
|
+
*/
|
|
649
|
+
is_vector_edit_target(id: NodeId): VectorEditSource | null;
|
|
650
|
+
/**
|
|
651
|
+
* True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
|
|
652
|
+
* per-glyph attribute (which conflicts with element-level rotation).
|
|
653
|
+
*/
|
|
654
|
+
has_glyph_rotate(id: NodeId): boolean;
|
|
655
|
+
/**
|
|
656
|
+
* True iff this element's inline `style=""` declares a `transform:`
|
|
657
|
+
* CSS property (which would shadow the editor's `transform=` writes).
|
|
658
|
+
*/
|
|
659
|
+
has_inline_css_transform(id: NodeId): boolean;
|
|
660
|
+
/**
|
|
661
|
+
* True iff this element has a direct `<animateTransform>` child
|
|
662
|
+
* (which produces a time-varying transform invisible to attribute writes).
|
|
663
|
+
* Only direct children are checked — nested cases attach to the nearer ancestor.
|
|
664
|
+
*/
|
|
665
|
+
has_animate_transform_child(id: NodeId): boolean;
|
|
550
666
|
text_of(id: NodeId): string;
|
|
551
667
|
/** Replace all direct text children with a single text node carrying `value`. */
|
|
552
668
|
set_text(id: NodeId, value: string): void;
|
|
@@ -562,6 +678,262 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
562
678
|
private emit_attr;
|
|
563
679
|
}
|
|
564
680
|
//#endregion
|
|
681
|
+
//#region src/core/vector-edit/model.d.ts
|
|
682
|
+
type Verb = "M" | "L" | "H" | "V" | "C" | "S" | "Q" | "T" | "A" | "Z";
|
|
683
|
+
type VertexId = number;
|
|
684
|
+
type SegmentId = number;
|
|
685
|
+
/** `[vertex_idx, 0]` = ta on segment whose `a === vertex_idx`; `[vertex_idx, 1]` = tb where `b === vertex_idx`. */
|
|
686
|
+
type TangentRef = readonly [VertexId, 0 | 1];
|
|
687
|
+
/**
|
|
688
|
+
* Tangent mirroring policy applied around a vertex when one tangent moves.
|
|
689
|
+
* Mirrors `vn.TangentMirroringMode`.
|
|
690
|
+
*
|
|
691
|
+
* - `auto` — infer from current state (smooth join → mirror angle+length;
|
|
692
|
+
* broken / asymmetric → don't mirror).
|
|
693
|
+
* - `none` — only move the chosen tangent. Other tangents at this vertex
|
|
694
|
+
* stay put.
|
|
695
|
+
* - `angle` — keep opposite tangent collinear (mirror angle), preserve its
|
|
696
|
+
* length. Used when the user wants a sharp-vs-smooth-but-asymmetric
|
|
697
|
+
* handle (Figma's "Mirror angle" mode).
|
|
698
|
+
* - `all` — mirror both angle and length. Standard "smooth" handle pair.
|
|
699
|
+
*/
|
|
700
|
+
type TangentMirrorMode = "auto" | "none" | "angle" | "all";
|
|
701
|
+
type SegmentView = {
|
|
702
|
+
a: VertexId;
|
|
703
|
+
b: VertexId;
|
|
704
|
+
ta: readonly [number, number];
|
|
705
|
+
tb: readonly [number, number];
|
|
706
|
+
source_verb?: Verb;
|
|
707
|
+
};
|
|
708
|
+
type PathSnapshot = {
|
|
709
|
+
vertices: ReadonlyArray<readonly [number, number]>;
|
|
710
|
+
segments: ReadonlyArray<SegmentView>;
|
|
711
|
+
};
|
|
712
|
+
type SubSelection = {
|
|
713
|
+
vertices: ReadonlyArray<VertexId>;
|
|
714
|
+
segments: ReadonlyArray<SegmentId>;
|
|
715
|
+
tangents: ReadonlyArray<TangentRef>;
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* Per-segment metadata maintained alongside vn's segment array.
|
|
719
|
+
* `meta[i]` corresponds to `network.segments[i]`.
|
|
720
|
+
*/
|
|
721
|
+
type SegmentMeta = {
|
|
722
|
+
/** The SVG verb that originally produced this segment, if known. */source_verb?: Verb; /** Arc-specific metadata for segments born from an `A` command. */
|
|
723
|
+
arc?: ArcMeta; /** True iff this segment was emitted by a `Z` command (closing the subpath). */
|
|
724
|
+
is_close_segment?: boolean;
|
|
725
|
+
};
|
|
726
|
+
/**
|
|
727
|
+
* When an `A` command is parsed, it decomposes to N cubic segments.
|
|
728
|
+
* All segments in the same arc share the same `group_id` and the same
|
|
729
|
+
* arc parameters, plus each carries a snapshot of its original tangents
|
|
730
|
+
* (used at emit time to detect "still an arc" vs "user has edited").
|
|
731
|
+
*/
|
|
732
|
+
type ArcMeta = {
|
|
733
|
+
group_id: number;
|
|
734
|
+
rx: number;
|
|
735
|
+
ry: number;
|
|
736
|
+
x_rot: number;
|
|
737
|
+
large_arc_flag: 0 | 1;
|
|
738
|
+
sweep_flag: 0 | 1; /** Snapshot of this segment's ta at parse time (relative). */
|
|
739
|
+
baseline_ta: cmath.Vector2; /** Snapshot of this segment's tb at parse time (relative). */
|
|
740
|
+
baseline_tb: cmath.Vector2; /** Snapshot of this segment's end-vertex absolute position at parse time. */
|
|
741
|
+
baseline_b_abs: cmath.Vector2; /** Sequence index within the arc group (0..N-1). */
|
|
742
|
+
seq: number; /** Total segments in the arc group. */
|
|
743
|
+
count: number;
|
|
744
|
+
/** Original SVG arc command's `(x, y)` endpoint. Only populated on the
|
|
745
|
+
* LAST segment of the arc group (seq === count - 1). Used by the emitter
|
|
746
|
+
* to write back the exact coordinate the author wrote, avoiding floating-
|
|
747
|
+
* point drift from arc-to-cubic decomposition. */
|
|
748
|
+
original_end?: cmath.Vector2;
|
|
749
|
+
};
|
|
750
|
+
/**
|
|
751
|
+
* Canonical vector-network model for a single SVG path's `d` string.
|
|
752
|
+
*
|
|
753
|
+
* `PathModel` is a self-contained geometry primitive — it parses an SVG
|
|
754
|
+
* path `d` into a vertex/segment graph (with verb hints preserved for
|
|
755
|
+
* round-trip honesty), exposes POJO observers, and serializes back to
|
|
756
|
+
* `d`. It does not hold or reference an `SvgDocument`, an editor
|
|
757
|
+
* instance, the DOM, or any host. It is safe to construct in any
|
|
758
|
+
* environment that can run the package.
|
|
759
|
+
*
|
|
760
|
+
* Public re-exported as a top-level Layer-A primitive from
|
|
761
|
+
* `@grida/svg-editor` for callers that want canonical path geometry
|
|
762
|
+
* without mounting an editor. The full mutation surface (translate /
|
|
763
|
+
* bend / set-tangent / split, etc.) is package-internal and may shift;
|
|
764
|
+
* the publicly-stable contract for external callers is the construction
|
|
765
|
+
* + serialization + observation methods documented at the entry point.
|
|
766
|
+
*
|
|
767
|
+
* @experimental Surface shape is v0; signatures may change before the
|
|
768
|
+
* package reaches semver stability.
|
|
769
|
+
*/
|
|
770
|
+
declare class PathModel {
|
|
771
|
+
private readonly _network;
|
|
772
|
+
private readonly _meta;
|
|
773
|
+
private constructor();
|
|
774
|
+
static fromSvgPathD(d: string): PathModel;
|
|
775
|
+
/** Construct from a vn network with no verb info (every segment defaults to undefined verb). */
|
|
776
|
+
static fromVectorNetwork(network: vn.VectorNetwork): PathModel;
|
|
777
|
+
toSvgPathD(): string;
|
|
778
|
+
snapshot(): PathSnapshot;
|
|
779
|
+
bbox(): cmath.Rectangle;
|
|
780
|
+
vertexCount(): number;
|
|
781
|
+
segmentCount(): number;
|
|
782
|
+
/**
|
|
783
|
+
* If the model's current geometry is still expressible in the source
|
|
784
|
+
* SVG tag's native attribute form, return the equivalent
|
|
785
|
+
* `VectorEditSource` (which is also the writeable shape) — else `null`.
|
|
786
|
+
*
|
|
787
|
+
* This is the decider that gates per-gesture native-attrs writeback in
|
|
788
|
+
* `VectorEditSession.apply_d`. `null` means "the user's edit cannot be
|
|
789
|
+
* faithfully written back to the source tag" — in v1 with no
|
|
790
|
+
* promotion, the gesture is refused; in v1.1+ with promotion, the
|
|
791
|
+
* element is rewritten to `<path d="…">`.
|
|
792
|
+
*
|
|
793
|
+
* v1 expressibility (all source kinds require every segment's `ta` and
|
|
794
|
+
* `tb` to be exactly zero — any tangent edit forces promotion):
|
|
795
|
+
*
|
|
796
|
+
* - **path** — always `null` (no native fallback; the canonical form
|
|
797
|
+
* IS `<path d>`, so callers should just write `d` directly).
|
|
798
|
+
* - **polyline** — segments form the canonical open chain
|
|
799
|
+
* `0→1, 1→2, …, (n-2)→(n-1)`. (Topology after `vn.fromPolyline` and
|
|
800
|
+
* any sequence of vertex translates.)
|
|
801
|
+
* - **polygon** — segments form the canonical closed chain
|
|
802
|
+
* `0→1, 1→2, …, (n-1)→0`. (Topology after `vn.fromPolygon` and any
|
|
803
|
+
* sequence of vertex translates.)
|
|
804
|
+
*
|
|
805
|
+
* Anything that changes segment topology (insert-vertex, delete-vertex,
|
|
806
|
+
* close/open shape) leaves the canonical chain and returns `null` here;
|
|
807
|
+
* the higher layer is responsible for routing those to tag-promotion
|
|
808
|
+
* (intra-Vertex or to-path).
|
|
809
|
+
*/
|
|
810
|
+
toNativeAttrs(source_tag: VectorEditSource["kind"]): Exclude<VectorEditSource, {
|
|
811
|
+
kind: "path";
|
|
812
|
+
}> | null;
|
|
813
|
+
/** Translate one vertex by `delta`. Connected segments follow because
|
|
814
|
+
* tangents are stored relative to vertices. Verb metadata is preserved
|
|
815
|
+
* as-is; emit-time honesty handles cases where the shape no longer
|
|
816
|
+
* matches the recorded verb (e.g. an H whose endpoint y-coord drifts). */
|
|
817
|
+
translateVertex(v: VertexId, delta: readonly [number, number]): PathModel;
|
|
818
|
+
/** Bulk-translate a set of vertices by the same delta. Atomic — either
|
|
819
|
+
* every move succeeds or none (input is validated up-front). */
|
|
820
|
+
translateVertices(indices: ReadonlyArray<VertexId>, delta: readonly [number, number]): PathModel;
|
|
821
|
+
/** Translate one segment by `delta` — moves both endpoints, dragging
|
|
822
|
+
* their tangents along (tangents are stored relative to vertices, so
|
|
823
|
+
* this is automatic). Other segments connected to the moved endpoints
|
|
824
|
+
* also follow at the shared vertex. */
|
|
825
|
+
translateSegment(seg: SegmentId, delta: readonly [number, number]): PathModel;
|
|
826
|
+
/**
|
|
827
|
+
* Bend a curve segment by dragging a point at parameter `ca` to `cb`
|
|
828
|
+
* (cb is in absolute doc-space). Delegates to vn's `bendSegment` —
|
|
829
|
+
* which solves for the new ta/tb that put `B(ca) === cb`, holding the
|
|
830
|
+
* endpoints fixed.
|
|
831
|
+
*
|
|
832
|
+
* The "frozen" snapshot of the segment at gesture start is the caller's
|
|
833
|
+
* responsibility. Convention: call this from a preview session where
|
|
834
|
+
* each frame replays from the baseline (same pattern as translate).
|
|
835
|
+
*/
|
|
836
|
+
bendSegment(seg: SegmentId, ca: number, cb: readonly [number, number], frozen: {
|
|
837
|
+
a: readonly [number, number];
|
|
838
|
+
b: readonly [number, number];
|
|
839
|
+
ta: readonly [number, number];
|
|
840
|
+
tb: readonly [number, number];
|
|
841
|
+
}): PathModel;
|
|
842
|
+
/**
|
|
843
|
+
* Move one tangent control point to a new absolute position. Mirror
|
|
844
|
+
* policy follows vn's `updateTangent`. The other tangent at the same
|
|
845
|
+
* vertex is updated according to the policy.
|
|
846
|
+
*
|
|
847
|
+
* Returns a new PathModel; verb metadata is preserved verbatim.
|
|
848
|
+
* `toSvgPathD` will demote (e.g. L → C) if the new tangents make the
|
|
849
|
+
* recorded verb no longer match the geometry.
|
|
850
|
+
*/
|
|
851
|
+
setTangent(t: TangentRef, abs_pos: readonly [number, number], mirror?: TangentMirrorMode): PathModel;
|
|
852
|
+
/**
|
|
853
|
+
* Split segment `seg` at parametric position `t ∈ [0,1]`, inserting a
|
|
854
|
+
* new vertex. Returns the new model and the **canonical (path-order)**
|
|
855
|
+
* index of the inserted vertex.
|
|
856
|
+
*
|
|
857
|
+
* Verb metadata for the split: the original segment's verb propagates
|
|
858
|
+
* to BOTH halves if it was a curve type (`C`/`S`/`Q`/`T`/`A`); for
|
|
859
|
+
* straight verbs (`L`/`H`/`V`), the split halves stay straight (their
|
|
860
|
+
* tangents are zero from vn's `preserveZero` path when both originals
|
|
861
|
+
* were zero). Arc-group identity is dropped from the halves — the
|
|
862
|
+
* arc is broken once split (the emitter will fall back to `C`/`L`).
|
|
863
|
+
*
|
|
864
|
+
* **Index space contract.** `VectorNetworkEditor.splitSegment` APPENDS
|
|
865
|
+
* the new vertex at the end of the network's vertices array — its
|
|
866
|
+
* index is the in-memory insertion order. But `toSvgPathD` / `fromSvgPathD`
|
|
867
|
+
* canonicalize vertices in path order, so the same vertex gets a
|
|
868
|
+
* DIFFERENT index in the d-derived model that consumers re-parse each
|
|
869
|
+
* frame (e.g., the host's `handle_translate_vertices`). Returning the
|
|
870
|
+
* insertion-order index causes the classic split-and-drag bug: the
|
|
871
|
+
* surface holds index N (insertion-order) but the live model has
|
|
872
|
+
* index M (path-order) at that position — drag moves the wrong vertex
|
|
873
|
+
* and the user sees "split happened but the new vertex doesn't move".
|
|
874
|
+
*
|
|
875
|
+
* To prevent that, we round-trip the post-split model through
|
|
876
|
+
* `toSvgPathD` → `fromSvgPathD` and return the canonical (path-order)
|
|
877
|
+
* index of the new vertex. The returned `model` is the canonical
|
|
878
|
+
* one, so any subsequent op on it uses the same index space the d
|
|
879
|
+
* roundtrip exposes. See `__tests__/README.md` §"index identity
|
|
880
|
+
* across the `d` round-trip" for the test pattern that pins this.
|
|
881
|
+
*/
|
|
882
|
+
splitSegment(seg: SegmentId, t: number): {
|
|
883
|
+
model: PathModel;
|
|
884
|
+
new_vertex: VertexId;
|
|
885
|
+
};
|
|
886
|
+
/**
|
|
887
|
+
* Doc-space position of a tangent control point. `t` references a
|
|
888
|
+
* segment and which end (`a` or `b`) the tangent belongs to; the
|
|
889
|
+
* result is `vertex + tangent_value + origin`. Returns null if no
|
|
890
|
+
* segment has this tangent (e.g. the vertex is isolated).
|
|
891
|
+
*/
|
|
892
|
+
tangentAbsolute(t: TangentRef, origin: readonly [number, number]): [number, number] | null;
|
|
893
|
+
/**
|
|
894
|
+
* Vertices "neighbouring" the current selection — these are the
|
|
895
|
+
* vertices whose tangent handles should render in chrome.
|
|
896
|
+
*
|
|
897
|
+
* Two-phase, mirrors `editor/grida-canvas/reducers/methods/vector.ts`
|
|
898
|
+
* `getUXNeighbouringVertices`:
|
|
899
|
+
*
|
|
900
|
+
* 1. Collect "active" vertices:
|
|
901
|
+
* - every selected vertex
|
|
902
|
+
* - every tangent-owning vertex
|
|
903
|
+
* - both endpoints of every selected segment
|
|
904
|
+
* 2. Expand uniformly to 1-hop neighbours (vertices sharing a segment
|
|
905
|
+
* with any active vertex).
|
|
906
|
+
*
|
|
907
|
+
* Without phase 2 for tangent / segment selections, selecting only a
|
|
908
|
+
* tangent would hide neighbouring-vertex tangents — the user loses
|
|
909
|
+
* spatial context. Phase 2 makes the affordance symmetric: whatever
|
|
910
|
+
* triggered selection, the 1-hop ring of tangent handles is visible.
|
|
911
|
+
*
|
|
912
|
+
* Sorted ascending; deduped.
|
|
913
|
+
*/
|
|
914
|
+
neighbouringVertices(sel: SubSelection): VertexId[];
|
|
915
|
+
/**
|
|
916
|
+
* True iff segment `seg`'s curve is entirely contained in the rect.
|
|
917
|
+
* Delegates to `cmath.bezier.containedByRect`.
|
|
918
|
+
*/
|
|
919
|
+
segmentContainedByRect(seg: SegmentId, rect: cmath.Rectangle, origin?: readonly [number, number]): boolean;
|
|
920
|
+
/** @internal */
|
|
921
|
+
_rawNetwork(): vn.VectorNetwork;
|
|
922
|
+
/** @internal */
|
|
923
|
+
_rawMeta(): ReadonlyArray<SegmentMeta>;
|
|
924
|
+
/**
|
|
925
|
+
* Map a `TangentRef` to a concrete `(segment_index, control)` pair.
|
|
926
|
+
*
|
|
927
|
+
* `[v, 0]` → first segment whose `a === v` (its `ta`).
|
|
928
|
+
* `[v, 1]` → first segment whose `b === v` (its `tb`).
|
|
929
|
+
*
|
|
930
|
+
* Y-junctions (multi-outgoing or multi-incoming) are uncommon for SVG
|
|
931
|
+
* `<path>` content; v1 picks the first match. If we ever support those
|
|
932
|
+
* cleanly, extend `TangentRef` to carry the segment id explicitly.
|
|
933
|
+
*/
|
|
934
|
+
private _locateTangent;
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
565
937
|
//#region src/core/defs.d.ts
|
|
566
938
|
interface GradientsApi {
|
|
567
939
|
list(): ReadonlyArray<GradientEntry>;
|
|
@@ -630,6 +1002,21 @@ type KeymapBinding = {
|
|
|
630
1002
|
command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
|
|
631
1003
|
args?: unknown; /** Higher priorities run first in the chain. Default 0. */
|
|
632
1004
|
priority?: number;
|
|
1005
|
+
/**
|
|
1006
|
+
* Bypass the form-element focus guard. When `true`, this binding fires
|
|
1007
|
+
* even if a text input is focused (`<input>`, `<textarea>`, or any
|
|
1008
|
+
* `contentEditable` element).
|
|
1009
|
+
*
|
|
1010
|
+
* Default `false`. The platform's native text-editing shortcuts —
|
|
1011
|
+
* Cmd+A (select all), Cmd+Z/Y (input undo/redo), Cmd+C/V/X
|
|
1012
|
+
* (clipboard), arrow keys, Backspace, Tab, Enter — must win over
|
|
1013
|
+
* editor shortcuts while the user is typing.
|
|
1014
|
+
*
|
|
1015
|
+
* Opt in sparingly. Reasonable candidates: truly global app shortcuts
|
|
1016
|
+
* like Cmd+S (save). Unreasonable candidates: anything that has a
|
|
1017
|
+
* native text-editing meaning.
|
|
1018
|
+
*/
|
|
1019
|
+
allowInFormElement?: boolean;
|
|
633
1020
|
/**
|
|
634
1021
|
* Reserved for V2; not honored by the V1 dispatcher. When added, this
|
|
635
1022
|
* will be evaluated before the handler runs; if false, the binding is
|
|
@@ -675,8 +1062,9 @@ declare class Keymap {
|
|
|
675
1062
|
* bar even when the binding's handler rejects.
|
|
676
1063
|
*
|
|
677
1064
|
* Pure read; runs no handlers, no side effects. Honors the same
|
|
678
|
-
*
|
|
679
|
-
* keystroke isn't "claimed"
|
|
1065
|
+
* form-element focus guard `dispatch` uses, so a typing user's
|
|
1066
|
+
* keystroke isn't "claimed" — and the browser's native text-editing
|
|
1067
|
+
* default (Cmd+A select all, Cmd+Z undo, etc.) wins.
|
|
680
1068
|
*/
|
|
681
1069
|
claims(event: KeyboardEvent): boolean;
|
|
682
1070
|
/**
|
|
@@ -684,6 +1072,12 @@ declare class Keymap {
|
|
|
684
1072
|
* order. Returns `true` on the first handler that consumes; returns
|
|
685
1073
|
* `false` if nothing matched or all matches fell through.
|
|
686
1074
|
*
|
|
1075
|
+
* **Form-element focus guard.** When a text input is focused
|
|
1076
|
+
* (`<input>`, `<textarea>`, contentEditable), bindings are suppressed
|
|
1077
|
+
* by default so the platform's native shortcuts (Cmd+A, Cmd+Z, Cmd+C,
|
|
1078
|
+
* arrow nav, …) are preserved. A binding can opt out of this guard
|
|
1079
|
+
* with `allowInFormElement: true` — see `KeymapBinding`.
|
|
1080
|
+
*
|
|
687
1081
|
* `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
|
|
688
1082
|
* or touch the event in any way. The host decides what to do with the
|
|
689
1083
|
* platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
|
|
@@ -698,7 +1092,6 @@ declare class Keymap {
|
|
|
698
1092
|
* V1.
|
|
699
1093
|
*/
|
|
700
1094
|
private chunkKeysFor;
|
|
701
|
-
private has_safe_mod;
|
|
702
1095
|
}
|
|
703
1096
|
//#endregion
|
|
704
1097
|
//#region src/core/geometry.d.ts
|
|
@@ -738,7 +1131,7 @@ type GeometrySignals = {
|
|
|
738
1131
|
};
|
|
739
1132
|
/**
|
|
740
1133
|
* Caches `bounds_of` results keyed on `NodeId`; full-clears on either
|
|
741
|
-
* `structure_version` or `geometry_version` bump. See docs/
|
|
1134
|
+
* `structure_version` or `geometry_version` bump. See ../../docs/geometry.md for
|
|
742
1135
|
* why the cache is load-bearing under the surface's per-tick re-render.
|
|
743
1136
|
*/
|
|
744
1137
|
declare class MemoizedGeometryProvider implements GeometryProvider {
|
|
@@ -777,6 +1170,14 @@ type GestureContext = {
|
|
|
777
1170
|
camera: Camera; /** Editor for keymap dispatch / state reads. */
|
|
778
1171
|
editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
|
|
779
1172
|
handle: SurfaceHandle;
|
|
1173
|
+
/**
|
|
1174
|
+
* Predicate returning `true` iff the surface is currently "attended" —
|
|
1175
|
+
* focus inside the container subtree OR pointer over the container.
|
|
1176
|
+
* Gesture bindings whose keydown handlers call `preventDefault()` MUST
|
|
1177
|
+
* consult this before claiming, so the surface doesn't steal page-level
|
|
1178
|
+
* shortcuts when embedded in a larger document. See `util/attention.ts`.
|
|
1179
|
+
*/
|
|
1180
|
+
is_attended: () => boolean;
|
|
780
1181
|
};
|
|
781
1182
|
type GestureBinding = {
|
|
782
1183
|
/** Stable id used by `unbind` / `bindings()`. */id: GestureId;
|
|
@@ -835,11 +1236,41 @@ type CreateSvgEditorOptions = {
|
|
|
835
1236
|
providers?: Providers;
|
|
836
1237
|
style?: Partial<EditorStyle>;
|
|
837
1238
|
};
|
|
838
|
-
|
|
1239
|
+
/**
|
|
1240
|
+
* Internal-only members the package's own surfaces reach for. NOT part of
|
|
1241
|
+
* the published API — `_internal` is the surface↔editor bridge, `keymap`
|
|
1242
|
+
* is the keymap dispatcher the DOM surface forwards keyboard events to.
|
|
1243
|
+
* Lives on the runtime object for in-package callers; stripped from the
|
|
1244
|
+
* public `SvgEditor` type so the published `.d.ts` doesn't advertise them.
|
|
1245
|
+
*/
|
|
1246
|
+
type SvgEditorInternalMembers = "_internal" | "keymap";
|
|
1247
|
+
/** Internal handle. Use only inside `@grida/svg-editor`. */
|
|
1248
|
+
type SvgEditorInternal = ReturnType<typeof _create_svg_editor_internal>;
|
|
1249
|
+
/**
|
|
1250
|
+
* The published editor type. `Omit`s the in-package-only surfaces (see
|
|
1251
|
+
* `SvgEditorInternalMembers`) so consumers don't see them in IntelliSense
|
|
1252
|
+
* or the dist `.d.ts`. They still exist on the runtime object — casting
|
|
1253
|
+
* to `SvgEditorInternal` reaches them, but the contract is clear: stay
|
|
1254
|
+
* inside the package.
|
|
1255
|
+
*/
|
|
1256
|
+
type SvgEditor = Omit<SvgEditorInternal, SvgEditorInternalMembers>;
|
|
1257
|
+
/**
|
|
1258
|
+
* Host-provided rendering and input boundary. v0 contract is pure
|
|
1259
|
+
* lifecycle: the editor calls `dispose()` on `editor.detach()` /
|
|
1260
|
+
* `editor.dispose()`; the surface owns its own teardown there (event
|
|
1261
|
+
* listeners, DOM nodes, retained refs).
|
|
1262
|
+
*
|
|
1263
|
+
* **Why so narrow?** The cross-surface vocabulary — paint push, input
|
|
1264
|
+
* routing, hit-testing — isn't pinned yet. The DOM surface re-serializes
|
|
1265
|
+
* the document via `editor.subscribe`, attaches its own listeners, and
|
|
1266
|
+
* owns its own pick. The editor reaches into the DOM surface through
|
|
1267
|
+
* the in-package `_internal` channel (`SurfaceBridge` in
|
|
1268
|
+
* `core/surface-bridge.ts`), not through the public `Surface` type. A
|
|
1269
|
+
* cross-surface paint / input / hit-test contract is deferred until a
|
|
1270
|
+
* second surface implementation arrives and pins each shape (P6 —
|
|
1271
|
+
* public only after dogfooding).
|
|
1272
|
+
*/
|
|
839
1273
|
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
1274
|
dispose(): void;
|
|
844
1275
|
};
|
|
845
1276
|
type SurfaceHandle = {
|
|
@@ -1021,7 +1452,13 @@ type Commands = {
|
|
|
1021
1452
|
invoke(id: CommandId, args?: unknown): boolean; /** Whether an id has a registered handler. */
|
|
1022
1453
|
has(id: CommandId): boolean;
|
|
1023
1454
|
};
|
|
1024
|
-
|
|
1455
|
+
/**
|
|
1456
|
+
* Wide internal factory — returns the full object including the
|
|
1457
|
+
* `_internal` / `keymap` surfaces in its inferred type. Stays private.
|
|
1458
|
+
* The public `createSvgEditor` below wraps this and narrows the return
|
|
1459
|
+
* to `SvgEditor` so the published `.d.ts` doesn't advertise internals.
|
|
1460
|
+
*/
|
|
1461
|
+
declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
|
|
1025
1462
|
/**
|
|
1026
1463
|
* Low-level IR handle. Mutating directly bypasses history; prefer
|
|
1027
1464
|
* `editor.commands` for app code.
|
|
@@ -1118,22 +1555,33 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
|
|
|
1118
1555
|
_internal: {
|
|
1119
1556
|
doc: SvgDocument;
|
|
1120
1557
|
history: {
|
|
1121
|
-
preview: (label: string) =>
|
|
1558
|
+
preview: (label: string) => import("@grida/history").Preview;
|
|
1559
|
+
};
|
|
1560
|
+
insert_text_preview: (initial: Readonly<Record<string, string>>, opts?: {
|
|
1561
|
+
parent?: NodeId;
|
|
1562
|
+
}) => {
|
|
1563
|
+
id: NodeId;
|
|
1564
|
+
commit(): void;
|
|
1565
|
+
discard(): void;
|
|
1122
1566
|
};
|
|
1123
1567
|
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. */
|
|
1568
|
+
subscribe_translate_commit(cb: () => void): () => void;
|
|
1127
1569
|
notify_translate_commit: () => void;
|
|
1128
1570
|
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
1571
|
set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
|
|
1132
1572
|
push_surface_hover(id: NodeId | null): void;
|
|
1133
|
-
set_computed_resolver(fn: DomComputedResolver | null): void;
|
|
1573
|
+
set_computed_resolver(fn: DomComputedResolver | null): void;
|
|
1134
1574
|
set_geometry(p: GeometryProvider | null): void;
|
|
1135
1575
|
};
|
|
1136
1576
|
keymap: Keymap;
|
|
1137
1577
|
};
|
|
1578
|
+
/**
|
|
1579
|
+
* Construct a headless SVG editor. The returned object is the public
|
|
1580
|
+
* editor surface — observation (`state`, `subscribe`), commands
|
|
1581
|
+
* (`commands.*`), lifecycle (`attach` / `dispose`), and the typed-read
|
|
1582
|
+
* caches (`node_paint`, `node_properties`). Surfaces (DOM, headless)
|
|
1583
|
+
* attach later via `editor.attach(surface)`.
|
|
1584
|
+
*/
|
|
1585
|
+
declare function createSvgEditor(opts: CreateSvgEditorOptions): SvgEditor;
|
|
1138
1586
|
//#endregion
|
|
1139
|
-
export {
|
|
1587
|
+
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 };
|