@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
package/README.md
CHANGED
|
@@ -51,7 +51,15 @@ Internally, the editor wraps the parsed SVG in a **typed element IR**: a per-nod
|
|
|
51
51
|
|
|
52
52
|
The IR is a **typed view, not alternative storage**. The parsed AST remains the in-memory store; file bytes remain the source of truth; the parse-side source-position trivia store carries whitespace, attribute order, and unknown-namespace content. The IR is rebuilt from the AST on every load and discarded on `dispose`. P1 round-trip stands.
|
|
53
53
|
|
|
54
|
-
This is consistent with the "Not a private IR" anti-goal below — that anti-goal rejects alternative on-disk format and bytes-projected-from-IR storage, neither of which this is. Design: `docs/wg/feat-svg-editor/element-ir.md
|
|
54
|
+
This is consistent with the "Not a private IR" anti-goal below — that anti-goal rejects alternative on-disk format and bytes-projected-from-IR storage, neither of which this is. Design: [`docs/wg/feat-svg-editor/element-ir.md`](https://grida.co/docs/wg/feat-svg-editor/element-ir). The phased migration sketch lands with the implementation slice.
|
|
55
|
+
|
|
56
|
+
### Defined terms
|
|
57
|
+
|
|
58
|
+
The editor's design docs use capitalised terms with precise meanings. The one most often referenced in everyday review:
|
|
59
|
+
|
|
60
|
+
- **[Policy Class](https://grida.co/docs/wg/feat-svg-editor/glossary/policy-class)** — the minimal partition of editable elements such that every editing intent admits the same set of legal solutions within a class. `<circle>` and `<ellipse>` are different Policy Classes because their resize solution spaces fork differently, even though both are conics. The unit at which a host's policy decision (refuse / native / promote / via-transform) maps onto. When a design discussion asks "should X and Y be treated the same?" — apply the Policy Class fork test, not authoring intuition.
|
|
61
|
+
|
|
62
|
+
Full glossary: [`docs/wg/feat-svg-editor/glossary/`](https://github.com/gridaco/grida/tree/main/docs/wg/feat-svg-editor/glossary/).
|
|
55
63
|
|
|
56
64
|
## Principles
|
|
57
65
|
|
|
@@ -163,13 +171,34 @@ const editor = createSvgEditor({
|
|
|
163
171
|
});
|
|
164
172
|
```
|
|
165
173
|
|
|
166
|
-
`createSvgEditor` is the only constructor. The returned `SvgEditor` is the only
|
|
174
|
+
`createSvgEditor` is the only **editor** constructor. The returned `SvgEditor` is the only editor instance consumers ever hold. A small set of Layer-A geometry primitives (see [Geometry primitives](#geometry-primitives) below) is also exported for callers that need canonical SVG geometry without mounting an editor — those are not editor instances and have no lifecycle.
|
|
167
175
|
|
|
168
176
|
The editor core is **headless**. It parses the SVG, owns the document IR, accepts commands, and emits state — but it does not import, reference, or call into `window`, `document`, `HTMLElement`, or any DOM type. To render or take input, the host attaches a `Surface` (next section).
|
|
169
177
|
|
|
178
|
+
### Geometry primitives
|
|
179
|
+
|
|
180
|
+
A small set of Layer-A primitives is exported for callers that want canonical SVG geometry without mounting an editor. These are not part of the editor lifecycle, do not subscribe, and do not produce diffs against an `SvgDocument` — they are pure value classes over the bytes you hand them.
|
|
181
|
+
|
|
182
|
+
#### `PathModel`
|
|
183
|
+
|
|
184
|
+
Models a single SVG path's vector network for callers that want path geometry without an editor. Construct from a `d` string, observe vertex/segment shape, compute a bbox, serialize back to `d`. No editor, no document, no DOM.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { PathModel } from "@grida/svg-editor";
|
|
188
|
+
|
|
189
|
+
const m = PathModel.fromSvgPathD("M 10 10 L 100 10 L 100 100 Z");
|
|
190
|
+
m.vertexCount(); // 3
|
|
191
|
+
m.segmentCount(); // 3
|
|
192
|
+
m.snapshot(); // { vertices, segments } — POJO
|
|
193
|
+
m.bbox(); // { x, y, width, height }
|
|
194
|
+
m.toSvgPathD(); // canonical d
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`@experimental` — the externally-stable contract for v0 is construction (`fromSvgPathD`) plus `snapshot()` / `bbox()` / `vertexCount()` / `segmentCount()` / `toSvgPathD()`. Mutation methods on the class exist for the editor's internal use and are not part of the documented public surface.
|
|
198
|
+
|
|
170
199
|
### Surface
|
|
171
200
|
|
|
172
|
-
A `Surface` is the host-provided rendering and input boundary. The
|
|
201
|
+
A `Surface` is the host-provided rendering and input boundary. The shipped `domSurface` is the reference implementation used by the React layer; non-DOM hosts (React Native, worker-side renderer, headless test harness) would implement the same interface — though only one implementation exists today (P6: public only after dogfooding).
|
|
173
202
|
|
|
174
203
|
```ts
|
|
175
204
|
import { attach_dom_surface } from "@grida/svg-editor/dom";
|
|
@@ -179,29 +208,32 @@ const handle = attach_dom_surface(editor, { container });
|
|
|
179
208
|
handle.detach();
|
|
180
209
|
```
|
|
181
210
|
|
|
182
|
-
The
|
|
211
|
+
The v0 contract is pure lifecycle:
|
|
183
212
|
|
|
184
213
|
```ts
|
|
185
214
|
interface Surface {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// surface → editor: hit-test on screen pixel
|
|
190
|
-
hit_test(x: number, y: number): NodeId | null;
|
|
191
|
-
|
|
192
|
-
// surface → editor: subscribe to normalized input
|
|
193
|
-
on_input(listener: (event: SurfaceInputEvent) => void): Unsubscribe;
|
|
194
|
-
|
|
215
|
+
/** Teardown: detach listeners, drop retained refs. Called from
|
|
216
|
+
* `editor.detach()` and `editor.dispose()`. */
|
|
195
217
|
dispose(): void;
|
|
196
218
|
}
|
|
197
219
|
```
|
|
198
220
|
|
|
221
|
+
What's deliberately **not** part of the contract yet:
|
|
222
|
+
|
|
223
|
+
- **Paint push.** There is no `paint(snapshot)` channel. The surface re-serializes the document by subscribing to the editor and writing to its own rendering target.
|
|
224
|
+
- **Normalized input events.** Input routing is surface-private — the DOM surface attaches pointer/keyboard listeners on its own container and reaches the editor through the in-package `_internal` channel.
|
|
225
|
+
- **Hit-testing.** Picking is surface-private: the DOM surface owns its own pointer → node-id resolver against its rendered scene. World-space geometry queries (`bounds_of`, `node_at_point` for non-pointer callers) route through `editor.geometry` instead. A cross-surface `hit_test` contract is deferred until a second surface needs one — its shape (screen vs. world units, z-order tie-breaks, hit-record vs. id) isn't pinned.
|
|
226
|
+
|
|
227
|
+
Each will become a public seam when a second surface implementation arrives and pins its shape. Until then, exporting `paint(snapshot: unknown)` / `on_input(event: unknown)` / `hit_test(x, y)` would be contracts a foreign implementer cannot honestly satisfy (P6 — public only after dogfooding).
|
|
228
|
+
|
|
199
229
|
Geometry (world-space bboxes, screen ↔ local projection) is exposed via `editor.geometry`, not the `Surface` itself — the DOM surface registers a `MemoizedGeometryProvider` with the editor on attach so headless callers can query bounds without going through the surface.
|
|
200
230
|
|
|
201
231
|
`@grida/svg-editor/dom` exports `attach_dom_surface(editor, { container, ... })` as the default DOM implementation, plus the surface-scoped types (`Camera`, `Gestures`, `SnapOptions`, `MemoizedGeometryProvider`, `DomComputedResolver`) that callers writing alternative surfaces or advanced integrations may need. It mounts the SVG into the container, wires pointer / keyboard listeners scoped to that container, and uses native `getBBox` / `getScreenCTM` for geometry. It is the only place in this package that imports DOM types.
|
|
202
232
|
|
|
203
233
|
The container is **exclusively owned** by the surface. Render toolbars, layer lists, inspectors, and any other interactive chrome as **siblings** of the container, not children. Children of the container interfere with pointer routing (capture redirects, hit-test ordering) and produce silent click breakage. The shipped `SvgEditorCanvas` React component enforces this by creating its own internal div; hosts using `domSurface` / `keynote.attach` directly are responsible for the same discipline. In development, the surface emits a `console.warn` at attach time when the container is non-empty.
|
|
204
234
|
|
|
235
|
+
**Attention gate.** The DOM surface installs document- and window-level keydown listeners (so a user with focus on a side-panel button can still hit editor shortcuts while the pointer is on the canvas). Those listeners are gated by an internal attention predicate: a key is claimed (and `preventDefault()`-ed) only when **focus is inside the container's subtree OR the pointer is over the container**. Body-focus alone — the natural state when the surface is embedded as a block in a longer document — is not attended, so the editor stays out of the way of page-level shortcuts (Space / arrows to scroll, Cmd+= to zoom, etc.). Passive observation listeners (modifier mirrors, blur resets) are not gated — they don't call `preventDefault()` and need to stay live across focus boundaries.
|
|
236
|
+
|
|
205
237
|
### Lifecycle
|
|
206
238
|
|
|
207
239
|
```ts
|
|
@@ -217,9 +249,32 @@ editor.dispose(): void; // permanent teardown
|
|
|
217
249
|
```ts
|
|
218
250
|
editor.load(svg: string): void; // replace the document (e.g. file-on-disk changed)
|
|
219
251
|
editor.serialize(): string; // emit clean SVG — guaranteed round-trip per P1
|
|
252
|
+
editor.serialize_node(id: NodeId): string; // emit ONE element's subtree — a fragment, see below
|
|
220
253
|
editor.reset(): void; // back to last load() input, clears history
|
|
221
254
|
```
|
|
222
255
|
|
|
256
|
+
`serialize_node(id)` exports the markup of a single element — the bridge from
|
|
257
|
+
"what the user selected" (a `NodeId`) to "the SVG for that element," e.g. to
|
|
258
|
+
hand a downstream consumer (an AI agent) the selected subtree without
|
|
259
|
+
re-serializing the whole document. It reuses `serialize()`'s trivia-preserving
|
|
260
|
+
rules (attribute order, quotes, whitespace, comments — emitted as authored).
|
|
261
|
+
|
|
262
|
+
It is deliberately **weaker** than `serialize()`, and the two must not be
|
|
263
|
+
conflated: `serialize()` emits the whole document and carries the P1
|
|
264
|
+
round-trip guarantee; `serialize_node()` emits a **fragment** and does not.
|
|
265
|
+
Namespace declarations that live on an ancestor (`xmlns:xlink` and friends,
|
|
266
|
+
normally on the root `<svg>`) are **not** inlined into the fragment — a node
|
|
267
|
+
using `xlink:href` serializes without `xmlns:xlink`. The fragment is the
|
|
268
|
+
element's markup as authored, not a standalone parseable document. Throws on
|
|
269
|
+
an unknown id or a non-element node (selections are always elements).
|
|
270
|
+
|
|
271
|
+
> A stable reference to a node that survives a `load()` — and survives an
|
|
272
|
+
> external rewrite of the file — is a separate, unsolved problem (`NodeId`
|
|
273
|
+
> regenerates on each parse). Positional child-index paths address only the
|
|
274
|
+
> deterministic-re-parse case, not structural edits; durable node identity is
|
|
275
|
+
> under design — see
|
|
276
|
+
> [durable node identity](https://grida.co/docs/wg/feat-svg-editor/durable-node-identity).
|
|
277
|
+
|
|
223
278
|
### Observation — state
|
|
224
279
|
|
|
225
280
|
```ts
|
|
@@ -498,18 +553,23 @@ editor.tree(): {
|
|
|
498
553
|
|
|
499
554
|
Returns a shallow snapshot. Cheap to call after a `version` bump.
|
|
500
555
|
|
|
501
|
-
### Modes
|
|
556
|
+
### Modes and tools
|
|
557
|
+
|
|
558
|
+
"What does a click do" is governed by **two orthogonal axes**, both editor-internal — consumers observe them and flip them via commands, but cannot define new values for either.
|
|
502
559
|
|
|
503
|
-
|
|
560
|
+
- **`Mode`** — what the editor is _doing_. Two values: `select` (normal interaction — pick / marquee / drag) and `edit-content` (inline text edit, or vector content edit on a path).
|
|
561
|
+
- **`Tool`** — what pointer-down _means_ within the current mode. `cursor` (the default — select / marquee / drag), `insert` (a tag — pointer-down draws a new element of that tag, drag-to-size), `insert-text` (click-only — places a single-line `<text>` and enters content-edit immediately; `<text>` has no intrinsic size so it doesn't drag-to-size), and the content-edit-only `lasso` / `bend` (valid only while `mode === "edit-content"` on a path).
|
|
504
562
|
|
|
505
563
|
```ts
|
|
506
|
-
editor.modes: ReadonlyArray<Mode>; // discoverable, frozen after construction
|
|
507
|
-
|
|
564
|
+
editor.modes: ReadonlyArray<Mode>; // discoverable, frozen after construction — ["select", "edit-content"]
|
|
565
|
+
editor.state.mode: Mode;
|
|
566
|
+
editor.state.tool: Tool;
|
|
508
567
|
|
|
509
568
|
editor.commands.set_mode(mode: Mode): void;
|
|
569
|
+
editor.set_tool(tool: Tool): void; // also dispatchable as the `tool.set` command (keymap V/R/O/L/T)
|
|
510
570
|
```
|
|
511
571
|
|
|
512
|
-
When a
|
|
572
|
+
When a tool-driven gesture completes (a shape is drawn, a text element placed), the tool reverts to `cursor` automatically. Modifier keys can override this (e.g. hold to stay in the insert tool); that behavior is bundled, not customizable.
|
|
513
573
|
|
|
514
574
|
### Commands
|
|
515
575
|
|
|
@@ -558,9 +618,11 @@ editor.commands.{
|
|
|
558
618
|
group(): void; // wrap selection in a new <g>
|
|
559
619
|
remove(): void;
|
|
560
620
|
|
|
561
|
-
// insertion
|
|
562
|
-
|
|
563
|
-
|
|
621
|
+
// insertion — `tag` is an open string (so paste / RPC can create any element,
|
|
622
|
+
// e.g. "path"); only the closed `InsertableTag` set gets a pointer-driven
|
|
623
|
+
// draw gesture and default paint.
|
|
624
|
+
insert(tag: string, attrs?: Readonly<Record<string, string>>): NodeId;
|
|
625
|
+
insert_preview(tag: string, initial?: Readonly<Record<string, string>>): InsertPreviewSession;
|
|
564
626
|
|
|
565
627
|
// content
|
|
566
628
|
set_text(value: string): void;
|
|
@@ -654,25 +716,25 @@ These are not internals to be replaced — they're documented sugar over `useEdi
|
|
|
654
716
|
```tsx
|
|
655
717
|
import {
|
|
656
718
|
// state slices (one-line wrappers over useEditorState)
|
|
657
|
-
useSelection,
|
|
658
|
-
useTool,
|
|
659
|
-
useMode,
|
|
660
|
-
useCanUndo,
|
|
661
|
-
useCanRedo,
|
|
719
|
+
useSelection, // → readonly NodeId[]
|
|
720
|
+
useTool, // → Tool
|
|
721
|
+
useMode, // → Mode
|
|
722
|
+
useCanUndo, // → boolean
|
|
723
|
+
useCanRedo, // → boolean
|
|
662
724
|
|
|
663
725
|
// lifecycle-aware preview sessions — unmount = discard (never commit)
|
|
664
|
-
usePaintPreview,
|
|
665
|
-
usePropertyPreview,
|
|
726
|
+
usePaintPreview, // (channel) → PaintPreviewSession
|
|
727
|
+
usePropertyPreview, // (name) → PreviewSession
|
|
666
728
|
|
|
667
729
|
// bound imperative actions, stable identity across renders
|
|
668
|
-
useEditorLoad,
|
|
669
|
-
useEditorSerialize,
|
|
730
|
+
useEditorLoad, // → (svg: string) => void
|
|
731
|
+
useEditorSerialize, // → () => string
|
|
670
732
|
|
|
671
733
|
// RAII hover override (clears on unmount if this hook set the override)
|
|
672
|
-
useHoverOverride,
|
|
734
|
+
useHoverOverride, // → (id: NodeId | null) => void
|
|
673
735
|
|
|
674
736
|
// camera bridge (subscribe to a slice of handle.camera without bumping state.version)
|
|
675
|
-
useCameraSnapshot,
|
|
737
|
+
useCameraSnapshot, // (handle, selector, fallback) → T
|
|
676
738
|
} from "@grida/svg-editor/react";
|
|
677
739
|
```
|
|
678
740
|
|
|
@@ -701,25 +763,27 @@ Everything else is consumer-built against the editor's API. The two patterns:
|
|
|
701
763
|
|
|
702
764
|
```tsx
|
|
703
765
|
function Toolbar() {
|
|
704
|
-
|
|
705
|
-
|
|
766
|
+
// Insertion is the `Tool` axis, not `Mode` — `Mode` is only
|
|
767
|
+
// "select" / "edit-content". Flip tools via `editor.set_tool(...)`.
|
|
768
|
+
const tool = useEditorState((s) => s.tool);
|
|
769
|
+
const editor = useSvgEditor();
|
|
706
770
|
return (
|
|
707
771
|
<>
|
|
708
772
|
<ToolButton
|
|
709
|
-
active={
|
|
710
|
-
onClick={() =>
|
|
773
|
+
active={tool.type === "cursor"}
|
|
774
|
+
onClick={() => editor.set_tool({ type: "cursor" })}
|
|
711
775
|
>
|
|
712
776
|
↖
|
|
713
777
|
</ToolButton>
|
|
714
778
|
<ToolButton
|
|
715
|
-
active={
|
|
716
|
-
onClick={() =>
|
|
779
|
+
active={tool.type === "insert" && tool.tag === "rect"}
|
|
780
|
+
onClick={() => editor.set_tool({ type: "insert", tag: "rect" })}
|
|
717
781
|
>
|
|
718
782
|
▭
|
|
719
783
|
</ToolButton>
|
|
720
784
|
<ToolButton
|
|
721
|
-
active={
|
|
722
|
-
onClick={() =>
|
|
785
|
+
active={tool.type === "insert-text"}
|
|
786
|
+
onClick={() => editor.set_tool({ type: "insert-text" })}
|
|
723
787
|
>
|
|
724
788
|
T
|
|
725
789
|
</ToolButton>
|
|
@@ -821,9 +885,14 @@ If a consumer needs any of the above, the right answer is "this is the wrong too
|
|
|
821
885
|
|
|
822
886
|
## Status
|
|
823
887
|
|
|
824
|
-
- `v0.
|
|
888
|
+
- `v0.x` — selection, transform, insert (rect / ellipse / line), inline text
|
|
889
|
+
edit, and the click-to-place text tool. Experimental.
|
|
890
|
+
|
|
891
|
+
The shape of the API, the mental model, the file-format guarantees, and the scope are all unsettled. Nothing here is stable — public types still in flux include the `Tool` union (a planned axis split, see `TODO.md` F2). Do not depend on it from production code.
|
|
892
|
+
|
|
893
|
+
## Contributing
|
|
825
894
|
|
|
826
|
-
|
|
895
|
+
- [`TODO.md`](./TODO.md) — open questions and deferred work, grouped by area.
|
|
827
896
|
|
|
828
897
|
## License
|
|
829
898
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-Dl7c0q5A.mjs";
|
|
2
|
+
import cmath from "@grida/cmath";
|
|
3
|
+
import { guide } from "@grida/cmath/_snap";
|
|
4
|
+
|
|
5
|
+
//#region src/core/snap/options.d.ts
|
|
6
|
+
type SnapOptions = {
|
|
7
|
+
/** When false, snap behavior and snap-guide rendering are both off. */enabled: boolean;
|
|
8
|
+
/** Snap threshold in HUD canvas pixels (container CSS px in svg-editor;
|
|
9
|
+
* whatever space the consumer feeds in). Constant across zoom because
|
|
10
|
+
* the input rects are already in screen-equivalent units. */
|
|
11
|
+
threshold_px: number;
|
|
12
|
+
};
|
|
13
|
+
declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/dom.d.ts
|
|
16
|
+
type DomSurfaceOptions = {
|
|
17
|
+
/** Mount the SVG inside this container. */container: HTMLElement;
|
|
18
|
+
/**
|
|
19
|
+
* Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
|
|
20
|
+
* keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
|
|
21
|
+
* carte via `handle.gestures.bind(...)`.
|
|
22
|
+
*/
|
|
23
|
+
gestures?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Auto-fit the document into the viewport on initial attach. Default
|
|
26
|
+
* `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
|
|
27
|
+
* Subsequent `editor.load()` calls do NOT re-fit — call
|
|
28
|
+
* `handle.camera.fit("<root>")` yourself if you want that behavior.
|
|
29
|
+
*/
|
|
30
|
+
fit?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Initial camera transform. Default `cmath.transform.identity`. Ignored
|
|
33
|
+
* when `fit: true`.
|
|
34
|
+
*/
|
|
35
|
+
initial_camera?: cmath.Transform;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Surface handle for the DOM surface. Extends the editor's core
|
|
39
|
+
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
|
|
40
|
+
* and pointer/wheel/keyboard gesture bindings (`gestures`).
|
|
41
|
+
*
|
|
42
|
+
* Camera + gestures are **surface-scoped**: detaching the surface drops
|
|
43
|
+
* both. They never appear on the headless `SvgEditor`.
|
|
44
|
+
*/
|
|
45
|
+
type DomSurfaceHandle = SurfaceHandle & {
|
|
46
|
+
camera: Camera;
|
|
47
|
+
gestures: Gestures;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
|
|
51
|
+
* whose `detach()` is the inverse — DOM cleared, listeners removed,
|
|
52
|
+
* gestures uninstalled.
|
|
53
|
+
*
|
|
54
|
+
* Usage is one-shot per container: the surface owns the container's children
|
|
55
|
+
* for its lifetime, and `detach()` restores it to empty.
|
|
56
|
+
*/
|
|
57
|
+
declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
|
|
58
|
+
/**
|
|
59
|
+
* Affine projection of a point through a 2×3 CTM, then offset by a
|
|
60
|
+
* container origin (in page CSS-px). Mirrors how `line_endpoints_in_container`
|
|
61
|
+
* and `shape_of` (transformed branch) bridge from local SVG coords to the
|
|
62
|
+
* HUD's container-CSS-px space (HUD keeps its own transform at identity;
|
|
63
|
+
* the SVG carries the camera as a CSS transform, which getScreenCTM
|
|
64
|
+
* folds in).
|
|
65
|
+
*
|
|
66
|
+
* Exported for headless test coverage — pure function, no DOM types.
|
|
67
|
+
*/
|
|
68
|
+
declare function project_point_through_ctm(px: number, py: number, ctm: {
|
|
69
|
+
a: number;
|
|
70
|
+
b: number;
|
|
71
|
+
c: number;
|
|
72
|
+
d: number;
|
|
73
|
+
e: number;
|
|
74
|
+
f: number;
|
|
75
|
+
}, container_offset: readonly [number, number]): [number, number];
|
|
76
|
+
/**
|
|
77
|
+
* Inverse of the CTM's linear part applied to a delta vector. Drops
|
|
78
|
+
* translation. Used to convert a HUD-reported container-space drag delta
|
|
79
|
+
* back to the path's local coord space for `PathModel.translateVertices`.
|
|
80
|
+
*
|
|
81
|
+
* Throws on a degenerate (det = 0) matrix — the caller is expected to
|
|
82
|
+
* have a non-singular CTM for any visible element.
|
|
83
|
+
*/
|
|
84
|
+
declare function project_delta_inverse_ctm(dx: number, dy: number, ctm: {
|
|
85
|
+
a: number;
|
|
86
|
+
b: number;
|
|
87
|
+
c: number;
|
|
88
|
+
d: number;
|
|
89
|
+
}): [number, number];
|
|
90
|
+
/**
|
|
91
|
+
* Inverse-project a doc-space rect through a CTM + container offset back
|
|
92
|
+
* into the element's local frame. The output is the AABB of the four
|
|
93
|
+
* inverse-projected corners — when the CTM has a rotation component the
|
|
94
|
+
* AABB is an approximation, but it matches what the user visually
|
|
95
|
+
* expects from a screen-aligned marquee drag.
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` when the CTM's linear part is singular (degenerate
|
|
98
|
+
* camera) — the caller should skip any test that needs the local rect.
|
|
99
|
+
*/
|
|
100
|
+
declare function inverse_project_rect(rect: {
|
|
101
|
+
x: number;
|
|
102
|
+
y: number;
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
}, ctm: {
|
|
106
|
+
a: number;
|
|
107
|
+
b: number;
|
|
108
|
+
c: number;
|
|
109
|
+
d: number;
|
|
110
|
+
e: number;
|
|
111
|
+
f: number;
|
|
112
|
+
}, offset: readonly [number, number]): cmath.Rectangle | null;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { project_delta_inverse_ctm as a, SnapOptions as c, inverse_project_rect as i, DomSurfaceOptions as n, project_point_through_ctm as o, attach_dom_surface as r, DEFAULT_SNAP_OPTIONS as s, DomSurfaceHandle as t };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-BKoo9SPL.js";
|
|
2
|
+
import cmath from "@grida/cmath";
|
|
3
|
+
//#region src/core/snap/options.d.ts
|
|
4
|
+
type SnapOptions = {
|
|
5
|
+
/** When false, snap behavior and snap-guide rendering are both off. */enabled: boolean;
|
|
6
|
+
/** Snap threshold in HUD canvas pixels (container CSS px in svg-editor;
|
|
7
|
+
* whatever space the consumer feeds in). Constant across zoom because
|
|
8
|
+
* the input rects are already in screen-equivalent units. */
|
|
9
|
+
threshold_px: number;
|
|
10
|
+
};
|
|
11
|
+
declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/dom.d.ts
|
|
14
|
+
type DomSurfaceOptions = {
|
|
15
|
+
/** Mount the SVG inside this container. */container: HTMLElement;
|
|
16
|
+
/**
|
|
17
|
+
* Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
|
|
18
|
+
* keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
|
|
19
|
+
* carte via `handle.gestures.bind(...)`.
|
|
20
|
+
*/
|
|
21
|
+
gestures?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Auto-fit the document into the viewport on initial attach. Default
|
|
24
|
+
* `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
|
|
25
|
+
* Subsequent `editor.load()` calls do NOT re-fit — call
|
|
26
|
+
* `handle.camera.fit("<root>")` yourself if you want that behavior.
|
|
27
|
+
*/
|
|
28
|
+
fit?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Initial camera transform. Default `cmath.transform.identity`. Ignored
|
|
31
|
+
* when `fit: true`.
|
|
32
|
+
*/
|
|
33
|
+
initial_camera?: cmath.Transform;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Surface handle for the DOM surface. Extends the editor's core
|
|
37
|
+
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
|
|
38
|
+
* and pointer/wheel/keyboard gesture bindings (`gestures`).
|
|
39
|
+
*
|
|
40
|
+
* Camera + gestures are **surface-scoped**: detaching the surface drops
|
|
41
|
+
* both. They never appear on the headless `SvgEditor`.
|
|
42
|
+
*/
|
|
43
|
+
type DomSurfaceHandle = SurfaceHandle & {
|
|
44
|
+
camera: Camera;
|
|
45
|
+
gestures: Gestures;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
|
|
49
|
+
* whose `detach()` is the inverse — DOM cleared, listeners removed,
|
|
50
|
+
* gestures uninstalled.
|
|
51
|
+
*
|
|
52
|
+
* Usage is one-shot per container: the surface owns the container's children
|
|
53
|
+
* for its lifetime, and `detach()` restores it to empty.
|
|
54
|
+
*/
|
|
55
|
+
declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
|
|
56
|
+
/**
|
|
57
|
+
* Affine projection of a point through a 2×3 CTM, then offset by a
|
|
58
|
+
* container origin (in page CSS-px). Mirrors how `line_endpoints_in_container`
|
|
59
|
+
* and `shape_of` (transformed branch) bridge from local SVG coords to the
|
|
60
|
+
* HUD's container-CSS-px space (HUD keeps its own transform at identity;
|
|
61
|
+
* the SVG carries the camera as a CSS transform, which getScreenCTM
|
|
62
|
+
* folds in).
|
|
63
|
+
*
|
|
64
|
+
* Exported for headless test coverage — pure function, no DOM types.
|
|
65
|
+
*/
|
|
66
|
+
declare function project_point_through_ctm(px: number, py: number, ctm: {
|
|
67
|
+
a: number;
|
|
68
|
+
b: number;
|
|
69
|
+
c: number;
|
|
70
|
+
d: number;
|
|
71
|
+
e: number;
|
|
72
|
+
f: number;
|
|
73
|
+
}, container_offset: readonly [number, number]): [number, number];
|
|
74
|
+
/**
|
|
75
|
+
* Inverse of the CTM's linear part applied to a delta vector. Drops
|
|
76
|
+
* translation. Used to convert a HUD-reported container-space drag delta
|
|
77
|
+
* back to the path's local coord space for `PathModel.translateVertices`.
|
|
78
|
+
*
|
|
79
|
+
* Throws on a degenerate (det = 0) matrix — the caller is expected to
|
|
80
|
+
* have a non-singular CTM for any visible element.
|
|
81
|
+
*/
|
|
82
|
+
declare function project_delta_inverse_ctm(dx: number, dy: number, ctm: {
|
|
83
|
+
a: number;
|
|
84
|
+
b: number;
|
|
85
|
+
c: number;
|
|
86
|
+
d: number;
|
|
87
|
+
}): [number, number];
|
|
88
|
+
/**
|
|
89
|
+
* Inverse-project a doc-space rect through a CTM + container offset back
|
|
90
|
+
* into the element's local frame. The output is the AABB of the four
|
|
91
|
+
* inverse-projected corners — when the CTM has a rotation component the
|
|
92
|
+
* AABB is an approximation, but it matches what the user visually
|
|
93
|
+
* expects from a screen-aligned marquee drag.
|
|
94
|
+
*
|
|
95
|
+
* Returns `null` when the CTM's linear part is singular (degenerate
|
|
96
|
+
* camera) — the caller should skip any test that needs the local rect.
|
|
97
|
+
*/
|
|
98
|
+
declare function inverse_project_rect(rect: {
|
|
99
|
+
x: number;
|
|
100
|
+
y: number;
|
|
101
|
+
width: number;
|
|
102
|
+
height: number;
|
|
103
|
+
}, ctm: {
|
|
104
|
+
a: number;
|
|
105
|
+
b: number;
|
|
106
|
+
c: number;
|
|
107
|
+
d: number;
|
|
108
|
+
e: number;
|
|
109
|
+
f: number;
|
|
110
|
+
}, offset: readonly [number, number]): cmath.Rectangle | null;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { project_delta_inverse_ctm as a, SnapOptions as c, inverse_project_rect as i, DomSurfaceOptions as n, project_point_through_ctm as o, attach_dom_surface as r, DEFAULT_SNAP_OPTIONS as s, DomSurfaceHandle as t };
|