@weasel-js/core 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +937 -2202
  2. package/README.md +118 -75
  3. package/dist/{autoPoseDescriptor-DF1SnnSx.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +29 -26
  4. package/dist/{chunk-2VXGHUVL.js → chunk-BDWAA634.js} +4 -22
  5. package/dist/chunk-BDWAA634.js.map +1 -0
  6. package/dist/{chunk-R3AWPTLZ.js → chunk-MG7OXCAI.js} +2760 -4491
  7. package/dist/chunk-MG7OXCAI.js.map +1 -0
  8. package/dist/{chunk-PRGBGMH3.js → chunk-MQI4PIX3.js} +3 -3
  9. package/dist/chunk-MQI4PIX3.js.map +1 -0
  10. package/dist/{chunk-WPM42WJP.js → chunk-UCPV7JXC.js} +201 -256
  11. package/dist/chunk-UCPV7JXC.js.map +1 -0
  12. package/dist/clipboard.d.ts +2 -3
  13. package/dist/clone.d.ts +3 -2
  14. package/dist/depSchema-nMqj_qTM.d.ts +3490 -0
  15. package/dist/{grid-0Pbn5B2C.d.ts → grid-BrIa38gG.d.ts} +7 -10
  16. package/dist/index.d.ts +1996 -1229
  17. package/dist/index.js +4 -5
  18. package/dist/insert.d.ts +4 -4
  19. package/dist/insert.js +1 -1
  20. package/dist/move.d.ts +5 -6
  21. package/dist/move.js +3 -6
  22. package/dist/move.js.map +1 -1
  23. package/dist/{options-DbYLImvq.d.ts → options-BDyCnrp8.d.ts} +3 -2
  24. package/dist/poseDescriptor-CGOgIgf8.d.ts +134 -0
  25. package/dist/renderer.d.ts +10 -4
  26. package/dist/renderer.js +4 -5
  27. package/dist/resize.d.ts +10 -12
  28. package/dist/resize.js +2 -2
  29. package/dist/routing.d.ts +1 -142
  30. package/dist/routing.js +1 -1
  31. package/dist/routing.js.map +1 -1
  32. package/dist/{types-ei3UMl9R.d.ts → types-DMyo7dnM.d.ts} +12 -41
  33. package/dist/{types-DEALFt5F.d.ts → types-DtjCJA5r.d.ts} +9 -3
  34. package/package.json +13 -10
  35. package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
  36. package/dist/builtins-BXFBXegF.d.ts +0 -840
  37. package/dist/chunk-2VXGHUVL.js.map +0 -1
  38. package/dist/chunk-BL65SHCX.js +0 -573
  39. package/dist/chunk-BL65SHCX.js.map +0 -1
  40. package/dist/chunk-PRGBGMH3.js.map +0 -1
  41. package/dist/chunk-R3AWPTLZ.js.map +0 -1
  42. package/dist/chunk-WPM42WJP.js.map +0 -1
  43. package/dist/geometry-6fCNhAux.d.ts +0 -114
  44. package/dist/path-JEV2c5If.d.ts +0 -48
  45. package/dist/registry-BY-wI9gm.d.ts +0 -4003
  46. package/dist/types-BHK2dkMu.d.ts +0 -172
  47. package/dist/types-bcc7jcUy.d.ts +0 -594
  48. package/dist/view-DSQgxBJB.d.ts +0 -63
package/README.md CHANGED
@@ -1,19 +1,16 @@
1
1
  # weasel
2
2
 
3
- Domain-agnostic 2D scene-graph hooks for React, rendered on WebGL2. Bring your own object type and pose shape; weasel handles the viewport math, pointer gestures (move / resize / insert / clone / area-select / text edit), layered scene rendering, an op-based undo/redo model, and a stack of selection-driven action hooks (delete, duplicate, nudge, group, clipboard, undo/redo, …) wired to keyboard shortcuts when you ask.
3
+ A 2D scene-graph engine for React, rendered on WebGL2. Bring your own node data and pose shape; weasel owns the scene tree, the viewport math, pointer and keyboard input, layered rendering, and op-based undo/redo — plus a standard set of editing actions (select, move, resize, rotate, delete, duplicate, nudge, group, align, clipboard, …) bound to the keys and gestures an editor user expects.
4
4
 
5
5
  Built for diagram editors, sketch tools, schematic editors, scene composers — anything where "objects on a canvas the user can grab, move, and arrange" is the substrate.
6
6
 
7
- > Pre-1.0: the API surface (paths, nesting, per-subobject units) is still settling. Expect breaking changes between minor versions until 1.0.
8
-
9
7
  ## Features
10
8
 
11
- - Pointer gestures: move, resize, rotate, insert, clone, area-select, text edit
9
+ - Pointer interactions: move, resize, rotate, insert, clone, area-select, lasso, path-anchor editing, text edit
12
10
  - Op-based scene mutation with undo/redo and coalescing
13
- - Selection-driven action hooks (delete, duplicate, nudge, reorder, clipboard, …)
14
- - Centralized actions registry with default keybindings (`@experimental`)
11
+ - A registry of actions with default key and gesture bindings, each one overridable by id (`@experimental`)
15
12
  - Layered canvas rendering with debug overlays
16
- - Path poses, rect poses, rotated poses; first-class compound paths
13
+ - Rect, rotated and path poses; first-class compound paths; nested containers
17
14
  - Viewport with zoom/pan tools, momentum, and boundary clamping
18
15
  - Detached read-only scene views and navigation minimap (`<SceneViewCanvas>`, `<MinimapCanvas>`)
19
16
  - WebGL2 renderer with MSDF text, gradients, patterns, and per-vertex colors
@@ -29,65 +26,70 @@ npm install @weasel-js/core react
29
26
 
30
27
  ## How it fits together
31
28
 
32
- Every interaction takes a small, narrow **adapter** — a few methods that read the current scene and apply ops back. The kit doesn't own your scene; it asks. That keeps it agnostic to whether your scene lives in React state, Zustand, Redux, or a CRDT.
33
-
34
- ```tsx
35
- import { useMove, useDelete, createHistory, snap, gridSnapStrategy } from '@weasel-js/core';
36
-
37
- const history = createHistory(adapter);
29
+ `useScene` holds the scene: a tree of leaf and container nodes, each carrying your data and a pose. Every change to it is an **op**, which is what makes it undoable. `<SceneCanvas>` renders a scene and routes input to it.
38
30
 
39
- // Drag-to-move with snapping, history, and parent reparenting:
40
- const move = useMove(adapter, {
41
- behaviors: [snap(gridSnapStrategy(20))],
42
- });
31
+ Interactions are **actions** — descriptors registered into an actions registry — reached by **gesture bindings**. A tool is a list of `{ spec, actionId }` pairs and nothing more; the kit's own select, shape and viewport tools are built that way.
43
32
 
44
- // Selection-aware delete with Backspace/Delete bound:
45
- useDelete(adapter, { bindKeyboard: true });
33
+ ```tsx
34
+ import { SceneCanvas, useScene, useSelection, gridSnapStrategy, type RectPose } from '@weasel-js/core';
35
+
36
+ const W = 800, H = 600;
37
+
38
+ export function Editor() {
39
+ const scene = useScene({ systemLayers: [{ id: 'default' }], initial: [] });
40
+ const selection = useSelection({ mode: 'multi' });
41
+
42
+ // Click to select, drag to move with grid snapping, handles to resize and
43
+ // rotate, the shape tools, and the standard keyboard actions (Cmd+A, Cmd+Z,
44
+ // Delete, …):
45
+ return (
46
+ <SceneCanvas
47
+ width={W}
48
+ height={H}
49
+ scene={scene}
50
+ selection={selection}
51
+ selectionMode="multi"
52
+ toolBundle="exhaustive"
53
+ selectTool={{ snap: gridSnapStrategy<RectPose>(20) }}
54
+ layers={{
55
+ grid: { spacing: 20, bounds: () => ({ x: 0, y: 0, width: W, height: H }) },
56
+ }}
57
+ />
58
+ );
59
+ }
46
60
  ```
47
61
 
48
- Most apps don't call the gesture hooks directly — `<SceneCanvas>` owns useMove /
49
- useResize / useInsert / useAreaSelect / useSelection internally. Drop in a
50
- `useScene()` tree and a `layers` map and you get click-to-select, drag-to-move,
51
- corner-handle-resize, and an `tool="insert"` mode for free.
62
+ To change a behavior you override the descriptor by id, bind a different gesture to it, or register your own — you don't call a hook per interaction. [docs/hooks.md](https://github.com/orochi235/weasel/blob/main/docs/hooks.md) has the full action table.
52
63
 
53
- See the live demo for a full working example: <https://orochi235.github.io/weasel/>
64
+ Lower-level surfaces take a narrow **adapter** instead of a scene — a few methods that read your state and apply ops back — so they work over state weasel doesn't own. See [docs/adapters.md](https://github.com/orochi235/weasel/blob/main/docs/adapters.md).
54
65
 
55
66
  ## Demo
56
67
 
57
- Live demo: <https://orochi235.github.io/weasel/>
58
-
59
- ## Text rendering
60
-
61
- Text is rendered via MSDF atlases. Register fonts before the first paint:
62
-
63
- ```tsx
64
- import { registerFont } from '@weasel-js/core';
65
-
66
- await registerFont('Inter', { weight: 400 }, '/fonts/Inter-400.json', '/fonts/Inter-400.png');
67
- ```
68
-
69
- Core doesn't ship a prebuilt atlas — bake one with `npm run gen:font -- <font.ttf> --name Inter-400 --out public/fonts` (see `scripts/gen-font.ts`) and serve the resulting `.json`/`.png` pair. The `hud` package ships its own bundled Inter atlas for consumers who don't need custom fonts.
68
+ <https://orochi235.github.io/weasel/> — every kit feature as a small runnable demo, plus the release history for every published version.
70
69
 
71
70
  ## Actions registry
72
71
 
73
- An `Action` is a named operation — `delete`, `duplicate`, `group`, `insert`, `viewport.dragPan` — paired with the input that triggers it. `<ActionsProvider>` holds the registered descriptors, and the gesture dispatcher matches live input against each one's `defaultBinding`. Keystrokes and pointer gestures take the same path, so a keyboard shortcut and a drag are two bindings on one action rather than two mechanisms.
72
+ An `Action` is a named operation — `delete`, `duplicate`, `group`, `insert`, `viewport.dragPan` — paired with the input that triggers it. `<ActionsProvider>` holds the registered descriptors, and the gesture dispatcher matches live input against each one's `defaultBinding` and the active tool's bindings. Keystrokes and pointer gestures take the same path, so a keyboard shortcut and a drag are two bindings on one action rather than two mechanisms.
74
73
 
75
- `<SceneCanvas>` auto-mounts a provider when none is above it and registers the kit-standard descriptors, derived from the scene, selection, view and history it already owns.
74
+ `<SceneCanvas>` mounts a provider when none is above it and registers the kit-standard actions: escape, select-all, delete, duplicate, group and ungroup, undo and redo, flip, nudge, reorder, align, distribute, the pathfinder booleans, path-anchor editing, fill and stroke, clipboard, and the pointer-driven move, resize, rotate, insert, clone, area-select and lasso.
76
75
 
77
76
  ```tsx
78
- import { SceneCanvas } from '@weasel-js/core';
79
-
80
77
  <SceneCanvas
78
+ width={W}
79
+ height={H}
81
80
  scene={scene}
82
81
  selection={selection}
83
82
  actions={{
84
- duplicate: null, // drop the default
85
- 'app.publish': { // add your own
83
+ duplicate: null, // drop the default
84
+ 'app.publish': { // add your own
86
85
  id: 'app.publish',
87
86
  label: 'Publish',
88
87
  defaultBinding: { kind: 'key', key: 'p', mods: { mod: true } },
89
88
  requires: ['selection'],
90
- invoker: { timing: 'immediate', run: ({ selection }) => publish(selection.get()) },
89
+ invoker: {
90
+ timing: 'immediate',
91
+ run: (deps) => publish((deps.selection as SelectionApi).get()),
92
+ },
91
93
  },
92
94
  }}
93
95
  />
@@ -95,53 +97,94 @@ import { SceneCanvas } from '@weasel-js/core';
95
97
 
96
98
  The `actions` prop takes `null` to unregister every default, or a record keyed by action id. Each value is `null` to drop that one id, a partial `Action` to merge onto the default of the same id, or a complete `Action` to register a new one.
97
99
 
98
- An action does its work through `invoker`, not a bare callback. `{ timing: 'immediate' }` runs once; `{ timing: 'ongoing' }` returns a handle so a drag can preview while it moves and commit at the end. The deps an invoker reads (`selection`, `scene`, `applyOps`, …) are declared in `requires` and resolved at invocation time, which is what lets a consumer swap one — see `useDepSource`.
100
+ An action does its work through `invoker`, not a bare callback. `{ timing: 'immediate' }` runs once; `{ timing: 'ongoing' }` returns a handle so a drag can preview while it moves and commit at the end. The deps an invoker reads (`selection`, `scene`, `applyOps`, …) are declared in `requires` and resolved at invocation time, which is what lets a consumer swap one — see `useDepSource`. To fire an action yourself, wrap the canvas in your own `<ActionsProvider>` and call `trigger(id, params)` on the registry `useActionsRegistry()` returns.
101
+
102
+ ## Text rendering
103
+
104
+ Text is rendered via MSDF atlases. Register fonts before the first paint:
105
+
106
+ ```tsx
107
+ import { registerFont } from '@weasel-js/core';
108
+
109
+ await registerFont('Inter', { weight: 400 }, '/fonts/Inter-400.json', '/fonts/Inter-400.png');
110
+ ```
111
+
112
+ Core doesn't ship a prebuilt atlas — bake one from a weasel checkout with `npm run gen:font -- <font.ttf> --name Inter-400 --out public/fonts` (see [`packages/font/scripts/gen-font.ts`](https://github.com/orochi235/weasel/blob/main/packages/font/scripts/gen-font.ts)) and serve the resulting `.json`/`.png` pair. `@weasel-js/hud` bundles its own Inter atlas for its widgets.
113
+
114
+ The glyph tier lives in `@weasel-js/font`; `registerFont` is re-exported from core, so the import above keeps working. An unregistered family renders in the default family with a one-time warning — see that package's README for `setFontFallbackPolicy`.
99
115
 
100
116
  ## Custom shaders (`@experimental`)
101
117
 
102
- The renderer supports `kind: 'shader'` `DrawCommand`s for layers that want a custom fragment shader. Register the program once, then emit a draw command with uniforms and bounds:
118
+ A render layer can draw with its own fragment shader. Register the program once, then return a `kind: 'shader'` draw command with its uniforms and bounds:
103
119
 
104
120
  ```tsx
105
- import { registerProgram, registerTexture } from '@weasel-js/core';
106
-
107
- const voronoi = registerProgram(
108
- 'voronoi',
109
- /* vert */ null, // null → use the default quad prelude
110
- /* frag */ `
111
- precision highp float;
112
- varying vec2 v_uv;
113
- uniform float u_time;
114
- void main() { /* … */ }
115
- `,
116
- );
117
-
118
- // Inside a RenderLayer.draw, return a tree of DrawCommands:
119
- return {
121
+ import { registerProgram } from '@weasel-js/core/renderer';
122
+
123
+ const stripes = registerProgram('stripes', '', `#version 300 es
124
+ precision highp float;
125
+ in vec2 v_uv;
126
+ uniform float u_time;
127
+ out vec4 outColor;
128
+ void main() {
129
+ float v = 0.5 + 0.5 * sin(v_uv.x * 40.0 + u_time);
130
+ outColor = vec4(vec3(v), 1.0);
131
+ }`);
132
+
133
+ // Inside a RenderLayer's draw:
134
+ return [{
120
135
  kind: 'shader',
121
- program: voronoi,
136
+ program: stripes,
122
137
  uniforms: { u_time: performance.now() / 1000 },
123
138
  bounds: { x: 0, y: 0, w: 256, h: 256 },
124
- };
139
+ }];
125
140
  ```
126
141
 
127
- Uniforms support `number`, `vec2..4`, `mat3`, `mat4`, and `TextureHandle` (from `registerTexture`). The vertex prelude exposes `v_uv`, `v_screen`, and `v_world` varyings. API may change before v2.
142
+ An empty vertex source selects the kit's vertex shader, which provides the `v_uv`, `v_screen` and `v_world` varyings and sets `u_bounds`, `u_view` and `u_proj` itself. Uniforms take a number, a 2–4 element tuple, a `Float32Array`, or a `TextureHandle` from `registerTexture`. Output **premultiplied** alpha — `vec4(rgb * a, a)` — or translucent pixels come out too bright. `bounds` is in CSS pixels.
128
143
 
129
144
  ## Subpath imports
130
145
 
131
- For tree-shaking and clarity, hook-specific helpers are scoped:
132
-
133
- ```ts
134
- import { snapToGrid } from '@weasel-js/core/move';
135
- import { snapToGrid, clampMinSize } from '@weasel-js/core/resize';
136
- import { snapToGrid } from '@weasel-js/core/insert';
137
- ```
146
+ Core's main entry carries the everyday surface. A few narrower ones have their own:
147
+
148
+ | Import | Holds |
149
+ |---|---|
150
+ | `@weasel-js/core/renderer` | the renderer, `registerProgram`, `registerTexture`, draw commands |
151
+ | `@weasel-js/core/move`, `/resize`, `/insert`, `/clone`, `/clipboard` | helpers for building on those actions, e.g. `snapToGrid`, `clampMinSize` |
152
+ | `@weasel-js/core/patterns-builtin` | the built-in fill patterns |
153
+ | `@weasel-js/core/routing` | route grammar and introspection: parsing, the route registry, conflict checks |
154
+
155
+ ## Packages
156
+
157
+ Every package is published under `@weasel-js` and released together at one version.
158
+
159
+ | Package | What it is |
160
+ |---|---|
161
+ | `core` | the scene graph, `<SceneCanvas>`, actions, tools, and the WebGL2 renderer |
162
+ | `geom` | pure 2D geometry — affine, box, curve, polyline; polygon booleans under `./booleans` |
163
+ | `gestures` | the gesture taxonomy, route grammars and matchers; no React, no DOM |
164
+ | `history` | undo/redo with scoped sub-histories; no React, no DOM |
165
+ | `paint` | fills, strokes, gradients and dashes as plain data |
166
+ | `text` | styled runs, kerned layout, wrapping and measurement |
167
+ | `bidi` | the Unicode Bidirectional Algorithm (UAX #9) |
168
+ | `font` | MSDF atlases, glyph metrics and runtime glyph rasterization |
169
+ | `svg` | SVG import and export |
170
+ | `modes` | app-level modality: capability tags, mode definitions, a mode registry |
171
+ | `diagram` | node-link diagrams: ports on any node's perimeter, flowchart-style bodies |
172
+ | `hud` | WebGL-rendered widgets composited into a canvas |
173
+ | `cursor` | tool cursors as authored glyphs, baked to CSS or painted when too large |
174
+ | `loupe` | a magnifier model: where it's aimed, how far it magnifies, what's under it |
175
+ | `audio` | a Web Audio engine: voices, buses, lookahead scheduling, spatialization; no weasel dependencies |
176
+ | `d3` | d3 data-join and transitions over `useScene` |
177
+ | `theme` | design tokens as CSS variables and a parallel TypeScript export |
178
+ | `ui` | React chrome components for weasel apps |
179
+ | `labkit` | React widgets for self-contained interactive lab pages |
138
180
 
139
181
  ## Documentation
140
182
 
141
- - [Concepts](./docs/concepts.md)
142
- - [Hooks](./docs/hooks.md)
143
- - [Adapters](./docs/adapters.md)
144
- - [Extending](./docs/extending.md)
183
+ - [Concepts](https://github.com/orochi235/weasel/blob/main/docs/concepts.md)
184
+ - [Hooks](https://github.com/orochi235/weasel/blob/main/docs/hooks.md)
185
+ - [Adapters](https://github.com/orochi235/weasel/blob/main/docs/adapters.md)
186
+ - [Extending](https://github.com/orochi235/weasel/blob/main/docs/extending.md)
187
+ - [Scene serialization](https://github.com/orochi235/weasel/blob/main/docs/scene-serialization.md)
145
188
 
146
189
  ## License
147
190
 
@@ -1,7 +1,6 @@
1
- import { c as ResizeAnchor, R as ResizePose, B as BoundsConstraint, P as PointSnapBehavior, d as PointSnapFrame, M as ModifierState } from './types-ei3UMl9R.js';
2
- import { B as Bounds, P as PoseProjection } from './geometry-6fCNhAux.js';
3
- import { D as DebugSink } from './types-BHK2dkMu.js';
4
- import { P as Path } from './path-JEV2c5If.js';
1
+ import { ResizeAnchor, Bounds, DebugSink, ModifierState } from '@weasel-js/routing';
2
+ import { B as BoundsConstraint, P as PointSnapBehavior, a as PointSnapFrame } from './types-DMyo7dnM.js';
3
+ import { P as PoseDescriptor, a as Path } from './poseDescriptor-CGOgIgf8.js';
5
4
 
6
5
  /** Corner resize-handle: world-space center plus the anchor that pins the opposite corner during resize. */
7
6
  interface CornerHandle {
@@ -72,14 +71,13 @@ declare function fixedCornerOf(bounds: Bounds, anchor: ResizeAnchor): {
72
71
  * Consumers should import from here directly; `resize.ts` re-exports the
73
72
  * same symbol for back-compat. */
74
73
 
75
- /** Options for the `resize` action: how the pose projects to and from a
76
- * bounding box, and the constraints applied to that box during the drag. */
74
+ /** Options for the `resize` action: the constraints applied to the dragged
75
+ * bounding box, and how a gesture expands a group into its leaves. */
77
76
  interface UseResizeOptions<TPose> {
78
- /** Behaviors are rect-typed: they read/write `{x,y,width,height}`. When
79
- * `TPose` is non-rect, pass `geometry` to project pose↔bounds; behaviors
80
- * in that case are typed `never` because none in the kit's library would
81
- * understand the pose shape. */
82
- behaviors?: TPose extends ResizePose ? BoundsConstraint<TPose>[] : never;
77
+ /** Behaviors are rect-typed: they read/write `{x,y,width,height}`. For a
78
+ * non-rect `TPose`, constraints are typed `never`; the pose descriptor
79
+ * comes from `<SceneCanvas poseDescriptor>`. */
80
+ behaviors?: TPose extends Bounds ? BoundsConstraint<TPose>[] : never;
83
81
  resizeLabel?: string;
84
82
  /** Reserved; resize is never transient in practice. Ignored. */
85
83
  transient?: boolean;
@@ -99,16 +97,12 @@ interface UseResizeOptions<TPose> {
99
97
  *
100
98
  * Called once at `start()`. Returning `[]` aborts the gesture cleanly. */
101
99
  expandIds?: (ids: string[]) => string[];
102
- /** Projection from `TPose` to bounds and back. Defaults to rect identity
103
- * when `TPose extends ResizePose`. Required for non-rect TPose (Path,
104
- * polygon, etc.). */
105
- geometry?: PoseProjection<TPose>;
106
100
  /** Behaviors that operate on world-space anchor points. Fire after
107
101
  * `behaviors[]` (bounds-frame). Each behavior receives a `PointSnapContext`
108
102
  * with world-space frame points and returns at most one `PointSnapResult`;
109
103
  * the hook back-solves the local pose so the chosen frame's world point
110
104
  * lands on the snap target. First non-null result wins. */
111
- pointSnapBehaviors?: TPose extends ResizePose ? PointSnapBehavior<TPose>[] : never;
105
+ pointSnapBehaviors?: TPose extends Bounds ? PointSnapBehavior<TPose>[] : never;
112
106
  /** Optional debug sink. When supplied, records corner-handle positions +
113
107
  * circular hitboxes when the gesture starts (covers the on-screen
114
108
  * handles for the resized target). Tree-shakes via optional-chain
@@ -132,7 +126,7 @@ interface UseResizeOptions<TPose> {
132
126
  * corner, the fixed corner, or the center, per `frame` — to a grid, moving
133
127
  * the whole pose by the same delta. Unlike the bounds-level `snapToGrid`,
134
128
  * this preserves the node's dimensions. */
135
- declare function pointSnapToGrid<TPose extends ResizePose>(args: {
129
+ declare function pointSnapToGrid<TPose extends Bounds>(args: {
136
130
  spacing: number;
137
131
  frame?: PointSnapFrame;
138
132
  bypassKey?: keyof ModifierState;
@@ -140,16 +134,25 @@ declare function pointSnapToGrid<TPose extends ResizePose>(args: {
140
134
 
141
135
  /** True for Path-shaped poses (`{kind: 'polygon' | 'rect'}`). Useful for
142
136
  * callers that need to fork between `pathPoseDescriptor` and
143
- * `RECT_POSE_DESCRIPTOR` without forcing the consumer to wire `geometry`
144
- * explicitly. */
137
+ * `RECT_POSE_DESCRIPTOR` without forcing the consumer to wire
138
+ * `poseDescriptor` explicitly. */
145
139
  declare function isPathLike(p: unknown): p is Path;
140
+ /** True for a pose with numeric top-level `x`/`y`/`width`/`height` — the only
141
+ * shape the rect descriptor and the kit's built-in painters can read. */
142
+ declare function isRectPose(p: unknown): p is {
143
+ x: number;
144
+ y: number;
145
+ width: number;
146
+ height: number;
147
+ rotation?: number;
148
+ };
146
149
  /** Per-call dispatch: if the pose looks like a Path, route to
147
150
  * `pathPoseDescriptor`; otherwise treat as a plain rect pose. Avoids forcing
148
- * demos with Path TPose to wire `geometry={pathPoseDescriptor}` explicitly.
149
- * `getRotation` surfaces a `pose.rotation` field on non-Path poses so demos
150
- * using rect-with-rotation shapes (e.g. `RotatedPose`) don't have to wire
151
- * `geometry={ROTATED_POSE_DESCRIPTOR}` just to get rotated selection chrome
152
- * and rotation-aware corner hit-tests. */
153
- declare const AUTO_POSE_DESCRIPTOR: PoseProjection<unknown>;
151
+ * demos with Path TPose to wire `poseDescriptor={pathPoseDescriptor}`
152
+ * explicitly. `getRotation` surfaces a `pose.rotation` field on non-Path poses
153
+ * so demos using rect-with-rotation shapes (e.g. `RotatedPose`) don't have to
154
+ * wire `poseDescriptor={ROTATED_POSE_DESCRIPTOR}` just to get rotated
155
+ * selection chrome and rotation-aware corner hit-tests. */
156
+ declare const AUTO_POSE_DESCRIPTOR: PoseDescriptor<unknown>;
154
157
 
155
- export { AUTO_POSE_DESCRIPTOR as A, CORNER_ANCHORS as C, type UseResizeOptions as U, type CornerAnchor as a, type CornerHandle as b, cornerPoint as c, cornerResizeHandles as d, type CornerEdge as e, fixedCornerOf as f, hitCornerHandle as h, isPathLike as i, pointSnapToGrid as p };
158
+ export { AUTO_POSE_DESCRIPTOR as A, CORNER_ANCHORS as C, type UseResizeOptions as U, type CornerAnchor as a, type CornerHandle as b, cornerPoint as c, cornerResizeHandles as d, isRectPose as e, fixedCornerOf as f, type CornerEdge as g, hitCornerHandle as h, isPathLike as i, pointSnapToGrid as p };
@@ -1,4 +1,4 @@
1
- import { RECT_ORIGIN_PROJECTION } from './chunk-WPM42WJP.js';
1
+ import { RECT_ORIGIN_PROJECTION } from './chunk-UCPV7JXC.js';
2
2
  import { registerOpFactory, captureSlot, slotFromIndex, resolveSlot } from './chunk-4RJP2N2L.js';
3
3
 
4
4
  // src/core/ops/transform.ts
@@ -99,24 +99,6 @@ function createReparentOp(args) {
99
99
  }
100
100
  registerOpFactory("reparent", (args) => createReparentOp(args));
101
101
 
102
- // src/interactions/scratchKey.ts
103
- function scratchKey(name) {
104
- return { name };
105
- }
106
- function getScratch(store, key) {
107
- return store[key.name];
108
- }
109
- function setScratch(store, key, value) {
110
- store[key.name] = value;
111
- }
112
- function deleteScratch(store, key) {
113
- if (Object.prototype.hasOwnProperty.call(store, key.name)) {
114
- delete store[key.name];
115
- return true;
116
- }
117
- return false;
118
- }
119
-
120
102
  // src/interactions/gestures/shared/snap.ts
121
103
  function snap(strategy, opts = {}) {
122
104
  const { bypassKey } = opts;
@@ -141,6 +123,6 @@ function snap(strategy, opts = {}) {
141
123
  };
142
124
  }
143
125
 
144
- export { createReparentOp, createTransformOp, deleteScratch, getScratch, scratchKey, setScratch, snap };
145
- //# sourceMappingURL=chunk-2VXGHUVL.js.map
146
- //# sourceMappingURL=chunk-2VXGHUVL.js.map
126
+ export { createReparentOp, createTransformOp, snap };
127
+ //# sourceMappingURL=chunk-BDWAA634.js.map
128
+ //# sourceMappingURL=chunk-BDWAA634.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/ops/transform.ts","../src/core/ops/reparent.ts","../src/interactions/gestures/shared/snap.ts"],"names":[],"mappings":"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","file":"chunk-BDWAA634.js","sourcesContent":["import type { Op } from './types';\nimport { registerOpFactory } from './registry';\n\ninterface TransformAdapter<TPose> {\n setPose(id: string, pose: TPose): void;\n}\n\n/** @internal */\ninterface TransformArgs<TPose> {\n id: string;\n from: TPose;\n to: TPose;\n label?: string;\n coalesceKey?: string;\n}\n\n/** Op: set an object's pose, inverting back to `from`. `coalesceKey` defaults to\n * `transform:${id}` so successive pose writes to the same node (drag-move,\n * resize, repeated nudges) merge into one undo entry within the history's\n * coalesce window. Pass an explicit key (or a unique one) to opt out. */\nexport function createTransformOp<TPose>(args: TransformArgs<TPose>): Op {\n const { id, from, to, label, coalesceKey = `transform:${id}` } = args;\n return {\n name: 'transform',\n args: { id, from, to, label, coalesceKey },\n label,\n coalesceKey,\n apply(adapter) {\n // Always call setPose so consumers that inspect op behavior (tests,\n // overlays) see a consistent \"this op writes (id, to)\" signal. When\n // from and to are structurally identical, additionally report a\n // no-op to history so the entry can be skipped from the undo stack.\n // setPose(id, to) is idempotent in this branch — adapters write the\n // same pose they already had.\n (adapter as TransformAdapter<TPose>).setPose(id, to);\n if (poseShallowEqual(from, to)) return false;\n return undefined;\n },\n invert() {\n return createTransformOp<TPose>({ id, from: to, to: from, label, coalesceKey });\n },\n };\n}\n\nregisterOpFactory<TransformArgs<unknown>>('transform', (args) => createTransformOp(args));\n\nfunction poseShallowEqual<TPose>(a: TPose, b: TPose): boolean {\n if (a === b) return true;\n if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;\n const ka = Object.keys(a as object);\n const kb = Object.keys(b as object);\n if (ka.length !== kb.length) return false;\n for (const k of ka) {\n if ((a as Record<string, unknown>)[k] !== (b as Record<string, unknown>)[k]) return false;\n }\n return true;\n}\n","import type { Op } from './types';\nimport { registerOpFactory } from './registry';\nimport { captureSlot, resolveSlot, slotFromIndex, type OrderedReader, type Slot } from './slot';\n\ninterface ReparentAdapter extends OrderedReader {\n setParent(id: string, parentId: string | null): void;\n /** Sibling-order write, paired with `getChildren`. Optional: an adapter\n * without both silently ignores slots and appends. */\n setChildOrder?(parentId: string | null, ids: string[]): void;\n}\n\n/** @internal */\ninterface ReparentArgs {\n id: string;\n fromParentId: string | null;\n toParentId: string | null;\n /** Sibling ordinal under the old parent. Sugar for `fromSlot: { index }`,\n * and only a seed: `apply` re-observes the node's full slot through the\n * adapter and that observation supersedes this. */\n fromIndex?: number;\n /** Sibling ordinal under the new parent. Sugar for `toSlot: { index }`.\n * Omit to append. */\n toIndex?: number;\n /** Slot under the old parent, anchor included. `invert()` forwards it as\n * the destination slot; without it undo re-parents with no position and\n * the node lands last, quietly changing paint order. */\n fromSlot?: Slot;\n /** Slot under the new parent, anchor included. Omit to append. */\n toSlot?: Slot;\n label?: string;\n coalesceKey?: string;\n}\n\n/** Move `id` to `slot` among `parentId`'s children. No-op when the adapter\n * has no ordering seam, when `id` isn't there, or when it's already in\n * place. */\nfunction placeAt(\n a: ReparentAdapter,\n parentId: string | null,\n id: string,\n slot: Slot,\n): void {\n if (!a.getChildren || !a.setChildOrder) return;\n const current = a.getChildren(parentId);\n const from = current.indexOf(id);\n if (from < 0) return;\n const next = current.slice();\n next.splice(from, 1);\n const resolved = resolveSlot(next, slot);\n const to = resolved === undefined ? next.length : Math.max(0, Math.min(resolved, next.length));\n if (from === to) return;\n next.splice(to, 0, id);\n a.setChildOrder(parentId, next);\n}\n\n/** Present when the caller named a destination at all; `undefined` leaves\n * placement to the adapter. */\nfunction optionalSlot(slot: Slot | undefined, index: number | undefined): Slot | undefined {\n if (slot) return slot;\n return index === undefined ? undefined : slotFromIndex(index);\n}\n\n/**\n * Op: change `id`'s parent and sibling slot, inverting back to the prior\n * parent and slot.\n *\n * `coalesceKey` defaults to `reparent:${id}` so successive reparents of the\n * same id batch-merge cleanly.\n */\nexport function createReparentOp(args: ReparentArgs): Op {\n const {\n id, fromParentId, toParentId,\n label = 'Reparent', coalesceKey = `reparent:${id}`,\n } = args;\n let fromSlot = optionalSlot(args.fromSlot, args.fromIndex);\n const toSlot = optionalSlot(args.toSlot, args.toIndex);\n const argsForSerial: ReparentArgs = {\n id, fromParentId, toParentId, fromSlot, toSlot, label, coalesceKey,\n };\n return {\n name: 'reparent',\n args: argsForSerial,\n label,\n coalesceKey,\n apply(adapter) {\n const a = adapter as ReparentAdapter;\n const observed = captureSlot(a, fromParentId, id);\n if (observed) {\n fromSlot = observed;\n argsForSerial.fromSlot = observed;\n }\n a.setParent(id, toParentId);\n if (toSlot) placeAt(a, toParentId, id, toSlot);\n },\n invert() {\n return createReparentOp({\n id,\n fromParentId: toParentId,\n toParentId: fromParentId,\n fromSlot: toSlot,\n toSlot: fromSlot,\n label,\n coalesceKey,\n });\n },\n };\n}\n\nregisterOpFactory<ReparentArgs>('reparent', (args) => createReparentOp(args));\n","import type { MoveBehavior, ModifierState, SnapStrategy } from '../types';\nimport {\n RECT_ORIGIN_PROJECTION,\n type OriginProjection,\n} from './strategies/grid';\n\ntype ModKey = keyof ModifierState;\n\n/**\n * Generic snap behavior factory: wraps a `SnapStrategy` and returns a\n * `MoveBehavior` whose `onMove` returns a uniform `GroupTransform`.\n *\n * Implementation:\n * 1. Compute the primary's proposed pose by applying the gesture's\n * `transform` (translate) to `origin.get(primaryId)`.\n * 2. Call `strategy.snap(proposed)`. If null, no-op.\n * 3. Derive the snapped delta via the `OriginProjection`:\n * dx = origin(snapped).x - origin(originPose).x\n * ...so the gesture applies the same delta to every dragged id.\n *\n * The pose-shape-aware delta extraction lives here — gestures stay pose-shape\n * agnostic. Default projection is `{x, y}`-bearing (rect / path / polygon).\n * Pass `origin` for exotic poses.\n */\nexport function snap<TPose>(\n strategy: SnapStrategy<TPose>,\n opts: { bypassKey?: ModKey; origin?: OriginProjection<TPose> } = {},\n): MoveBehavior<TPose> {\n const { bypassKey } = opts;\n const proj: OriginProjection<TPose> = opts.origin\n ?? (RECT_ORIGIN_PROJECTION as unknown as OriginProjection<TPose>);\n return {\n onMove(ctx, transform) {\n if (bypassKey && ctx.modifiers[bypassKey]) return;\n // The new contract: behaviors see a GroupTransform, not a pose. But\n // strategies still operate on poses. Reconstruct the proposed pose\n // by applying the (translate) transform to the primary's origin.\n if (transform.kind !== 'translate') return;\n const primaryId = ctx.draggedIds[0];\n if (primaryId === undefined) return;\n const originPose = ctx.origin.get(primaryId);\n if (originPose === undefined) return;\n const proposed = proj.translate(originPose, transform.dx, transform.dy);\n const snapped = strategy.snap(proposed, ctx);\n if (snapped === null) return;\n const o0 = proj.getOrigin(originPose);\n const o1 = proj.getOrigin(snapped);\n return {\n transform: { kind: 'translate', dx: o1.x - o0.x, dy: o1.y - o0.y },\n };\n },\n };\n}\n"]}