@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.
- package/CHANGELOG.md +937 -2202
- package/README.md +118 -75
- package/dist/{autoPoseDescriptor-DF1SnnSx.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +29 -26
- package/dist/{chunk-2VXGHUVL.js → chunk-BDWAA634.js} +4 -22
- package/dist/chunk-BDWAA634.js.map +1 -0
- package/dist/{chunk-R3AWPTLZ.js → chunk-MG7OXCAI.js} +2760 -4491
- package/dist/chunk-MG7OXCAI.js.map +1 -0
- package/dist/{chunk-PRGBGMH3.js → chunk-MQI4PIX3.js} +3 -3
- package/dist/chunk-MQI4PIX3.js.map +1 -0
- package/dist/{chunk-WPM42WJP.js → chunk-UCPV7JXC.js} +201 -256
- package/dist/chunk-UCPV7JXC.js.map +1 -0
- package/dist/clipboard.d.ts +2 -3
- package/dist/clone.d.ts +3 -2
- package/dist/depSchema-nMqj_qTM.d.ts +3490 -0
- package/dist/{grid-0Pbn5B2C.d.ts → grid-BrIa38gG.d.ts} +7 -10
- package/dist/index.d.ts +1996 -1229
- package/dist/index.js +4 -5
- package/dist/insert.d.ts +4 -4
- package/dist/insert.js +1 -1
- package/dist/move.d.ts +5 -6
- package/dist/move.js +3 -6
- package/dist/move.js.map +1 -1
- package/dist/{options-DbYLImvq.d.ts → options-BDyCnrp8.d.ts} +3 -2
- package/dist/poseDescriptor-CGOgIgf8.d.ts +134 -0
- package/dist/renderer.d.ts +10 -4
- package/dist/renderer.js +4 -5
- package/dist/resize.d.ts +10 -12
- package/dist/resize.js +2 -2
- package/dist/routing.d.ts +1 -142
- package/dist/routing.js +1 -1
- package/dist/routing.js.map +1 -1
- package/dist/{types-ei3UMl9R.d.ts → types-DMyo7dnM.d.ts} +12 -41
- package/dist/{types-DEALFt5F.d.ts → types-DtjCJA5r.d.ts} +9 -3
- package/package.json +13 -10
- package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
- package/dist/builtins-BXFBXegF.d.ts +0 -840
- package/dist/chunk-2VXGHUVL.js.map +0 -1
- package/dist/chunk-BL65SHCX.js +0 -573
- package/dist/chunk-BL65SHCX.js.map +0 -1
- package/dist/chunk-PRGBGMH3.js.map +0 -1
- package/dist/chunk-R3AWPTLZ.js.map +0 -1
- package/dist/chunk-WPM42WJP.js.map +0 -1
- package/dist/geometry-6fCNhAux.d.ts +0 -114
- package/dist/path-JEV2c5If.d.ts +0 -48
- package/dist/registry-BY-wI9gm.d.ts +0 -4003
- package/dist/types-BHK2dkMu.d.ts +0 -172
- package/dist/types-bcc7jcUy.d.ts +0 -594
- package/dist/view-DSQgxBJB.d.ts +0 -63
package/README.md
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
# weasel
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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>`
|
|
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,
|
|
85
|
-
'app.publish': {
|
|
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: {
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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](
|
|
142
|
-
- [Hooks](
|
|
143
|
-
- [Adapters](
|
|
144
|
-
- [Extending](
|
|
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 {
|
|
2
|
-
import { B as
|
|
3
|
-
import {
|
|
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:
|
|
76
|
-
* bounding box, and
|
|
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}`.
|
|
79
|
-
* `TPose
|
|
80
|
-
*
|
|
81
|
-
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
149
|
-
* `getRotation` surfaces a `pose.rotation` field on non-Path poses
|
|
150
|
-
* using rect-with-rotation shapes (e.g. `RotatedPose`) don't have to
|
|
151
|
-
* `
|
|
152
|
-
* and rotation-aware corner hit-tests. */
|
|
153
|
-
declare const AUTO_POSE_DESCRIPTOR:
|
|
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,
|
|
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-
|
|
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,
|
|
145
|
-
//# sourceMappingURL=chunk-
|
|
146
|
-
//# sourceMappingURL=chunk-
|
|
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"]}
|