@weasel-js/core 0.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 +381 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/DrawCommand-Dl0bXNfS.d.ts +500 -0
- package/dist/chunk-775XXAHR.js +216 -0
- package/dist/chunk-775XXAHR.js.map +1 -0
- package/dist/chunk-7V6JEOXE.js +27754 -0
- package/dist/chunk-7V6JEOXE.js.map +1 -0
- package/dist/chunk-AM6ARSPN.js +517 -0
- package/dist/chunk-AM6ARSPN.js.map +1 -0
- package/dist/chunk-BGGZ4CVF.js +248 -0
- package/dist/chunk-BGGZ4CVF.js.map +1 -0
- package/dist/chunk-BHVYVFGV.js +29 -0
- package/dist/chunk-BHVYVFGV.js.map +1 -0
- package/dist/chunk-CQNKCG34.js +831 -0
- package/dist/chunk-CQNKCG34.js.map +1 -0
- package/dist/chunk-GVCNT7UH.js +47 -0
- package/dist/chunk-GVCNT7UH.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-UGFFCMQP.js +28 -0
- package/dist/chunk-UGFFCMQP.js.map +1 -0
- package/dist/chunk-VOVKONXA.js +103 -0
- package/dist/chunk-VOVKONXA.js.map +1 -0
- package/dist/chunk-Y52N27PF.js +39 -0
- package/dist/chunk-Y52N27PF.js.map +1 -0
- package/dist/clipboard.d.ts +91 -0
- package/dist/clipboard.js +6 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/clone.d.ts +8 -0
- package/dist/clone.js +6 -0
- package/dist/clone.js.map +1 -0
- package/dist/fitViewToBounds-evGsnR8Q.d.ts +62 -0
- package/dist/grid-Cf87knjU.d.ts +153 -0
- package/dist/index-DZBYMsHI.d.ts +1555 -0
- package/dist/index.css +121 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +10200 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/insert.d.ts +33 -0
- package/dist/insert.js +81 -0
- package/dist/insert.js.map +1 -0
- package/dist/move.d.ts +69 -0
- package/dist/move.js +145 -0
- package/dist/move.js.map +1 -0
- package/dist/options-BPPBWMa7.d.ts +64 -0
- package/dist/patterns-builtin.d.ts +55 -0
- package/dist/patterns-builtin.js +100 -0
- package/dist/patterns-builtin.js.map +1 -0
- package/dist/pointSnapToGrid-D7s7QmOF.d.ts +185 -0
- package/dist/registerFont-CP-wCsrz.d.ts +109 -0
- package/dist/registerTexture-BzHTLhD9.d.ts +25 -0
- package/dist/renderer.css +121 -0
- package/dist/renderer.css.map +1 -0
- package/dist/renderer.d.ts +376 -0
- package/dist/renderer.js +13 -0
- package/dist/renderer.js.map +1 -0
- package/dist/resize.d.ts +78 -0
- package/dist/resize.js +5 -0
- package/dist/resize.js.map +1 -0
- package/dist/routing.css +35 -0
- package/dist/routing.css.map +1 -0
- package/dist/routing.d.ts +14 -0
- package/dist/routing.js +4 -0
- package/dist/routing.js.map +1 -0
- package/dist/types-B6MMiodD.d.ts +59 -0
- package/dist/types-BJ8_cyT7.d.ts +130 -0
- package/dist/types-B_-khFM0.d.ts +331 -0
- package/dist/types-BjUi2vA-.d.ts +355 -0
- package/dist/types-Cpb4hii1.d.ts +445 -0
- package/dist/types-D2tTKEU0.d.ts +18 -0
- package/dist/view-DSQgxBJB.d.ts +63 -0
- package/package.json +96 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import * as _weasel_js_history from '@weasel-js/history';
|
|
2
|
+
import { Op, SerializedHistory } from '@weasel-js/history';
|
|
3
|
+
import { P as Path } from './types-B6MMiodD.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pose composition for hierarchical scene graphs.
|
|
7
|
+
*
|
|
8
|
+
* As of the nesting change, `getPose(id)` on adapters returns the
|
|
9
|
+
* **local** pose — relative to the object's direct parent. Anything in the
|
|
10
|
+
* kit that needs to draw, hit-test, snap, or otherwise reason about world
|
|
11
|
+
* coordinates routes through `composeWorldPose`, which walks the parent
|
|
12
|
+
* chain and folds local poses together via a consumer-supplied `compose`.
|
|
13
|
+
*
|
|
14
|
+
* Pose shape is generic, so the compose strategy is too. For the common
|
|
15
|
+
* `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
|
|
16
|
+
* translation only, child dimensions preserved. Custom pose shapes (paths,
|
|
17
|
+
* matrix transforms) supply their own.
|
|
18
|
+
*
|
|
19
|
+
* The inverse — `rebaseLocalPose` — converts a world-space pose into a
|
|
20
|
+
* local pose under a target parent. Used when reparenting so the visual
|
|
21
|
+
* world position of a child is preserved across the parent change.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Axis-aligned rectangle pose with optional rotation. The canonical pose
|
|
25
|
+
* shape used by `composeRectPose` and the `unionBounds` helper. Rotation is
|
|
26
|
+
* in radians, pivoted on the unrotated AABB center; absent === 0. Kit-side
|
|
27
|
+
* code that consumes rotation already reads `pose.rotation ?? 0`
|
|
28
|
+
* (`SceneCanvas.defaultDrawOne`, `rotate/handle.ts`, `pathInWorld.ts`), so
|
|
29
|
+
* the slot exists on every default scene whether or not the consumer
|
|
30
|
+
* populates it. Defined here in core/transforms so the compose helpers
|
|
31
|
+
* don't depend on features.
|
|
32
|
+
*/
|
|
33
|
+
interface RectPose {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
/** Rotation in radians around the unrotated AABB center. Absent === 0. */
|
|
39
|
+
rotation?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Minimal adapter needed by `composeWorldPose` and friends — pose lookup plus parent walk. */
|
|
42
|
+
interface PoseAdapter<TPose> {
|
|
43
|
+
getPose(id: string): TPose;
|
|
44
|
+
getParent(id: string): string | null;
|
|
45
|
+
}
|
|
46
|
+
/** Consumer's pose-composition strategy for hierarchical scenes. `compose`
|
|
47
|
+
* folds a child's pose (in parent's frame) up to the next frame; `decompose`
|
|
48
|
+
* is its inverse. Default is IDENTITY — an absolute-pose scene where every
|
|
49
|
+
* node already stores world coords (parent is grouping-only, no transform). */
|
|
50
|
+
interface PoseComposition<TPose> {
|
|
51
|
+
compose: (parent: TPose, child: TPose) => TPose;
|
|
52
|
+
decompose: (parent: TPose, world: TPose) => TPose;
|
|
53
|
+
}
|
|
54
|
+
/** Default pose-composition strategy: IDENTITY. Both `compose` and
|
|
55
|
+
* `decompose` return the child/world pose unchanged, modeling an
|
|
56
|
+
* absolute-pose scene where every node stores world coords and parents are
|
|
57
|
+
* grouping-only (no transform). With this strategy `composeWorldPose`
|
|
58
|
+
* returns a node's own raw pose and `rebaseLocalPose` is a no-op. */
|
|
59
|
+
declare const IDENTITY_POSE_COMPOSITION: PoseComposition<unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Walk `id`'s parent chain (root first to id last) and fold local poses into
|
|
62
|
+
* a world pose via `compose`. Returns the world pose for `id`. Cycle-safe:
|
|
63
|
+
* a visited-set guard breaks if the chain ever loops back to itself.
|
|
64
|
+
*
|
|
65
|
+
* `compose(parent, child)` interprets `child` as expressed *in `parent`'s
|
|
66
|
+
* local frame* and returns the equivalent pose in the next frame up. For a
|
|
67
|
+
* standard translation-only rect: `world = { x: p.x + c.x, y: p.y + c.y,
|
|
68
|
+
* width: c.width, height: c.height }`.
|
|
69
|
+
*/
|
|
70
|
+
declare function composeWorldPose<TPose>(adapter: PoseAdapter<TPose>, id: string, compose: (parent: TPose, child: TPose) => TPose): TPose;
|
|
71
|
+
/**
|
|
72
|
+
* Default `compose` for axis-aligned rectangles. Adds translation; preserves
|
|
73
|
+
* child width/height. Treat as the canonical compose for any
|
|
74
|
+
* `{x, y, width, height}` pose under a translation-only hierarchy.
|
|
75
|
+
*
|
|
76
|
+
* Generic over the concrete pose type so callers with a wider pose
|
|
77
|
+
* (e.g. `RectPose & { rotation }`) can pass it through; the extra fields
|
|
78
|
+
* are taken from the child unchanged.
|
|
79
|
+
*/
|
|
80
|
+
declare function composeRectPose<TPose extends RectPose>(parent: TPose, child: TPose): TPose;
|
|
81
|
+
/**
|
|
82
|
+
* Translate a `RectPose`-shaped pose by `(dx, dy)`. Suitable as the default
|
|
83
|
+
* `translatePose` for `useMove` when poses carry top-level `x`/`y`. Other
|
|
84
|
+
* fields (width/height, plus any extra props on `TPose`) are preserved.
|
|
85
|
+
*/
|
|
86
|
+
declare function translateRectPose<TPose extends RectPose>(pose: TPose, dx: number, dy: number): TPose;
|
|
87
|
+
/**
|
|
88
|
+
* Convert `worldPose` into a local pose expressed under `newParentId`'s
|
|
89
|
+
* frame. Used when reparenting so the child's visual world position is
|
|
90
|
+
* preserved despite the change of frame. Inverse of one `compose` step.
|
|
91
|
+
*
|
|
92
|
+
* `decompose(parent, world)` returns the local pose `child` such that
|
|
93
|
+
* `compose(parent, child) === world`. For axis-aligned rects:
|
|
94
|
+
* `child = { ...world, x: world.x - parent.x, y: world.y - parent.y }`.
|
|
95
|
+
*
|
|
96
|
+
* Pass `newParentId === null` for the root frame; the function returns
|
|
97
|
+
* `worldPose` unchanged.
|
|
98
|
+
*/
|
|
99
|
+
declare function rebaseLocalPose<TPose>(adapter: PoseAdapter<TPose>, worldPose: TPose, newParentId: string | null, compose: (parent: TPose, child: TPose) => TPose, decompose: (parent: TPose, world: TPose) => TPose): TPose;
|
|
100
|
+
/** Inverse of `composeRectPose` — subtracts parent translation. */
|
|
101
|
+
declare function decomposeRectPose<TPose extends RectPose>(parent: TPose, world: TPose): TPose;
|
|
102
|
+
/**
|
|
103
|
+
* Build a `(id) => world pose | null` callback over a `PoseAdapter`.
|
|
104
|
+
* Convenience for RenderLayers that take a `getPose` callback (selection
|
|
105
|
+
* overlays, debug layers, etc.) so consumers don't hand-write a
|
|
106
|
+
* `composeWorldPose` call per layer.
|
|
107
|
+
*
|
|
108
|
+
* Returns `null` when `adapter.getPose` or `adapter.getParent` throws — the
|
|
109
|
+
* common case is an id removed mid-render between selection state and the
|
|
110
|
+
* next paint. Layers should treat `null` as "skip this id."
|
|
111
|
+
*/
|
|
112
|
+
declare function worldPoseLookup<TPose>(adapter: PoseAdapter<TPose>, compose: (parent: TPose, child: TPose) => TPose): (id: string) => TPose | null;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* # SceneNode — the thing in the scene
|
|
116
|
+
*
|
|
117
|
+
* A `SceneNode` is the single canonical unit of a weasel scene. Everything
|
|
118
|
+
* the user sees on the canvas — a shape, a group, an annotation, a tile —
|
|
119
|
+
* is one of these. Containers and leaves are both nodes; the kit has no
|
|
120
|
+
* other concept of "scene element."
|
|
121
|
+
*
|
|
122
|
+
* ## Three orthogonal slots
|
|
123
|
+
*
|
|
124
|
+
* Every node carries three independent slots, plus its tree position:
|
|
125
|
+
*
|
|
126
|
+
* - **`data: TData`** — app-defined payload. The kit never inspects it.
|
|
127
|
+
* Color, label, kind, glyph, sample-rate, whatever the app's domain
|
|
128
|
+
* calls for. Mutated via `Scene.update(id, { data })`.
|
|
129
|
+
*
|
|
130
|
+
* - **`pose: TPose`** — local transform, relative to the node's direct
|
|
131
|
+
* parent (or world, for root nodes). Default `RectPose` is
|
|
132
|
+
* `{ x, y, width, height }`, but `TPose` is fully generic so apps can
|
|
133
|
+
* use rotated rects, paths, ellipses, etc. The kit composes world
|
|
134
|
+
* poses via `composeWorldPose` when rendering / hit-testing / snapping.
|
|
135
|
+
*
|
|
136
|
+
* - **`layer: TLayer`** — a string tag associating the node with a
|
|
137
|
+
* visual `RenderLayer` at draw time. Separate from `LayerRecord` (the
|
|
138
|
+
* per-layer visible/locked metadata held by the `Scene`).
|
|
139
|
+
*
|
|
140
|
+
* Tree position lives on the node itself: every node has a `parent` (or
|
|
141
|
+
* `null` for roots), and `ContainerNode` adds an ordered `children: NodeId[]`.
|
|
142
|
+
*
|
|
143
|
+
* ## Identity is by `NodeId`, not by reference
|
|
144
|
+
*
|
|
145
|
+
* Nodes are addressed by `NodeId` everywhere outside the scene tree:
|
|
146
|
+
* selection is `NodeId[]`, ops reference `NodeId`s, adapter methods accept
|
|
147
|
+
* `string` ids and look up the node on demand. The `Node` object itself is
|
|
148
|
+
* a snapshot of current state — don't hold references to it across scene
|
|
149
|
+
* updates; look up by id when you need the latest.
|
|
150
|
+
*
|
|
151
|
+
* Picking helpers (`pickBest`, `pickEvery`) likewise return ids, not nodes —
|
|
152
|
+
* they're hit-testing primitives that stay ignorant of node payload shape.
|
|
153
|
+
*
|
|
154
|
+
* ## Vocabulary
|
|
155
|
+
*
|
|
156
|
+
* The kit-internal name is `Node`; the public re-export is `SceneNode`
|
|
157
|
+
* (avoids collision with DOM `Node` at call sites). Adapter methods speak
|
|
158
|
+
* the same vocabulary: `getNode`, `getNodes`, `insertNode`, `removeNode`,
|
|
159
|
+
* `cloneNode`, `addNode`. Older code, demos, and comments may still say
|
|
160
|
+
* "object" or "item" — those are historical aliases for the same concept.
|
|
161
|
+
*/
|
|
162
|
+
/** Opaque branded id. Treat as opaque outside the kit. */
|
|
163
|
+
type NodeId = string & {
|
|
164
|
+
readonly __brand: 'NodeId';
|
|
165
|
+
};
|
|
166
|
+
/** Brand a string as a NodeId. */
|
|
167
|
+
declare const asNodeId: (s: string) => NodeId;
|
|
168
|
+
interface NodeBase<TData, TLayer extends string, TPose> {
|
|
169
|
+
id: NodeId;
|
|
170
|
+
layer: TLayer;
|
|
171
|
+
pose: TPose;
|
|
172
|
+
data: TData;
|
|
173
|
+
parent: NodeId | null;
|
|
174
|
+
}
|
|
175
|
+
interface LeafNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
176
|
+
kind: 'leaf';
|
|
177
|
+
}
|
|
178
|
+
interface ContainerNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
179
|
+
kind: 'container';
|
|
180
|
+
children: NodeId[];
|
|
181
|
+
/** Optional clip-path source. Re-evaluated each render. Returning `null`
|
|
182
|
+
* means "no clip for this container right now"; an empty / zero-area path
|
|
183
|
+
* means "clip everything out" (children render nowhere). When set, the
|
|
184
|
+
* renderer rasterizes the returned path into the stencil buffer and
|
|
185
|
+
* paints descendants only where it covers. */
|
|
186
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
187
|
+
}
|
|
188
|
+
type Node<TData, TLayer extends string, TPose = RectPose> = LeafNode<TData, TLayer, TPose> | ContainerNode<TData, TLayer, TPose>;
|
|
189
|
+
interface LayerRecordBase<TLayer extends string> {
|
|
190
|
+
id: TLayer;
|
|
191
|
+
visible: boolean;
|
|
192
|
+
locked: boolean;
|
|
193
|
+
}
|
|
194
|
+
interface SystemLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
195
|
+
kind: 'system';
|
|
196
|
+
}
|
|
197
|
+
interface UserLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
198
|
+
kind: 'user';
|
|
199
|
+
name: string;
|
|
200
|
+
}
|
|
201
|
+
type LayerRecord<TLayer extends string> = SystemLayerRecord<TLayer> | UserLayerRecord<TLayer>;
|
|
202
|
+
interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {
|
|
203
|
+
kind: 'leaf' | 'container';
|
|
204
|
+
layer: TLayer;
|
|
205
|
+
pose: TPose;
|
|
206
|
+
data: TData;
|
|
207
|
+
parent?: NodeId | null;
|
|
208
|
+
index?: number;
|
|
209
|
+
/** Explicit id wins over the Scene's `generateId` and the kit default. */
|
|
210
|
+
id?: NodeId;
|
|
211
|
+
/** Only meaningful when `kind === 'container'`. Attach a clip-path function
|
|
212
|
+
* to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */
|
|
213
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
214
|
+
}
|
|
215
|
+
interface RegisteredOp<P> {
|
|
216
|
+
apply: (payload: P) => void;
|
|
217
|
+
revert: (payload: P) => void;
|
|
218
|
+
}
|
|
219
|
+
interface SystemLayerSpec<TLayer extends string> {
|
|
220
|
+
id: TLayer;
|
|
221
|
+
visible?: boolean;
|
|
222
|
+
locked?: boolean;
|
|
223
|
+
}
|
|
224
|
+
/** Argument to `Scene.addLayer`. Always produces a `UserLayerRecord`
|
|
225
|
+
* (`kind: 'user'`). */
|
|
226
|
+
interface AddLayerSpec<TLayer extends string> {
|
|
227
|
+
id: TLayer;
|
|
228
|
+
name: string;
|
|
229
|
+
/** Default `true`. */
|
|
230
|
+
visible?: boolean;
|
|
231
|
+
/** Default `false`. */
|
|
232
|
+
locked?: boolean;
|
|
233
|
+
/** Render-stack position. Default: top of stack (highest render index). */
|
|
234
|
+
index?: number;
|
|
235
|
+
}
|
|
236
|
+
/** JSON-serializable shape of a Scene's current state. Produced by
|
|
237
|
+
* `scene.toJSON()`; consumed by `sceneFromJSON()`. Function fields
|
|
238
|
+
* (e.g., `clipFromPose`) appear as string keys (`clipFromPoseKey`) and
|
|
239
|
+
* are resolved through `SceneRegistry` at load time. */
|
|
240
|
+
interface SerializedScene<TData, TLayer extends string, TPose> {
|
|
241
|
+
version: 1;
|
|
242
|
+
systemLayers: readonly SystemLayerSpec<TLayer>[];
|
|
243
|
+
nodes: readonly SerializedNode<TData, TLayer, TPose>[];
|
|
244
|
+
}
|
|
245
|
+
/** JSON-serializable shape of a single node. Mirrors `AddNodeSpec` but
|
|
246
|
+
* with function fields replaced by registry keys. */
|
|
247
|
+
interface SerializedNode<TData, TLayer extends string, TPose> {
|
|
248
|
+
id: string;
|
|
249
|
+
kind: 'leaf' | 'container';
|
|
250
|
+
layer: TLayer;
|
|
251
|
+
pose: TPose;
|
|
252
|
+
data: TData;
|
|
253
|
+
/** Parent id; omitted for roots. */
|
|
254
|
+
parent?: string;
|
|
255
|
+
/** Registry key for the container's clip-path factory.
|
|
256
|
+
* Containers only; omitted when the container has no clip. */
|
|
257
|
+
clipFromPoseKey?: string;
|
|
258
|
+
}
|
|
259
|
+
/** Per-scene registry mapping string keys to live function references.
|
|
260
|
+
* Passed to `createScene({ ..., registry })` and `sceneFromJSON(json, { registry })`.
|
|
261
|
+
* Each function-field type has its own keyed map. */
|
|
262
|
+
interface SceneRegistry<TPose> {
|
|
263
|
+
/** Maps registry keys to `clipFromPose` factory functions for container nodes. */
|
|
264
|
+
clipFromPose?: Readonly<Record<string, (pose: TPose) => Path | null>>;
|
|
265
|
+
}
|
|
266
|
+
interface UseSceneOptions<TData, TLayer extends string, TPose = RectPose> {
|
|
267
|
+
systemLayers: readonly SystemLayerSpec<TLayer>[];
|
|
268
|
+
initial?: readonly AddNodeSpec<TData, TLayer, TPose>[];
|
|
269
|
+
ops?: Readonly<Record<string, RegisteredOp<unknown>>>;
|
|
270
|
+
historyLimit?: number;
|
|
271
|
+
/** Window (ms) within which consecutive same-shaped mutations merge into
|
|
272
|
+
* the previous undo entry (matching per-op coalesce keys — e.g. repeated
|
|
273
|
+
* `setPose` on the same node, or repeated `applyBatch` calls whose ops
|
|
274
|
+
* carry matching `coalesceKey` multisets). `0` (default) disables
|
|
275
|
+
* coalescing: every mutation is a discrete undo entry. Undo of a
|
|
276
|
+
* coalesced entry returns to the state before the first merged mutation;
|
|
277
|
+
* redo restores the latest. `scene.batch` entries never coalesce. */
|
|
278
|
+
coalesceWindowMs?: number;
|
|
279
|
+
generateId?: () => NodeId;
|
|
280
|
+
/** Per-scene registry for non-serializable function fields (clipFromPose, etc.).
|
|
281
|
+
* Required only when serializing/deserializing scenes that use function fields. */
|
|
282
|
+
registry?: SceneRegistry<TPose>;
|
|
283
|
+
/** When supplied, `scene.applyOps(ops, label)` consults this on every call.
|
|
284
|
+
* If it returns a non-null `Journal`, ops are routed to the journal's
|
|
285
|
+
* `applyBatch` instead of recording a new parent-history entry. The journal
|
|
286
|
+
* drives adapter mutation internally (via `op.apply(adapter)`), so the
|
|
287
|
+
* scene state changes as normal; only the history tracking differs.
|
|
288
|
+
*
|
|
289
|
+
* The accessor is called on every `applyOps` invocation so the caller can
|
|
290
|
+
* swap the active journal in and out by updating the closure's reference
|
|
291
|
+
* (e.g., an app's mode machine holds `let activeJournal: Journal | null`
|
|
292
|
+
* and the accessor reads that variable).
|
|
293
|
+
*
|
|
294
|
+
* When the accessor isn't known at scene-construction time (typical for
|
|
295
|
+
* mode machines that depend on `scene.history`), pass nothing here and
|
|
296
|
+
* wire it after construction via `scene.setActiveJournalAccessor(fn)`. */
|
|
297
|
+
getActiveJournal?: () => _weasel_js_history.Journal | null;
|
|
298
|
+
}
|
|
299
|
+
interface Scene<TData, TLayer extends string, TPose = RectPose> {
|
|
300
|
+
readonly nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;
|
|
301
|
+
readonly roots: readonly NodeId[];
|
|
302
|
+
readonly layers: readonly LayerRecord<TLayer>[];
|
|
303
|
+
get(id: NodeId): Node<TData, TLayer, TPose> | undefined;
|
|
304
|
+
childrenOf(id: NodeId): readonly NodeId[];
|
|
305
|
+
ancestorsOf(id: NodeId): readonly NodeId[];
|
|
306
|
+
renderOrder(): Iterable<NodeId>;
|
|
307
|
+
add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;
|
|
308
|
+
/** Delete `id` **and its entire subtree** — every descendant is removed in
|
|
309
|
+
* the same operation. Recorded as one undoable step; `undo()` restores the
|
|
310
|
+
* whole subtree (root + descendants, child order intact). */
|
|
311
|
+
remove(id: NodeId): void;
|
|
312
|
+
update(id: NodeId, patch: {
|
|
313
|
+
data: TData;
|
|
314
|
+
}): void;
|
|
315
|
+
setPose(id: NodeId, pose: TPose): void;
|
|
316
|
+
/** Retag `id` to `layer`. On a **container this cascades**: every descendant
|
|
317
|
+
* is moved to the same layer, recorded as a **single** undo step.
|
|
318
|
+
*
|
|
319
|
+
* Invariants:
|
|
320
|
+
* - **Layer floor** — a child may not render below its parent, so retagging
|
|
321
|
+
* to a layer *below* the node's parent throws. Retagging to the parent's
|
|
322
|
+
* layer or any higher one is allowed; a node with no parent is
|
|
323
|
+
* unconstrained.
|
|
324
|
+
* - **No-op elision** — setting the layer a node already has does nothing
|
|
325
|
+
* and pushes **no** history entry. */
|
|
326
|
+
setLayer(id: NodeId, layer: TLayer): void;
|
|
327
|
+
/** Reparent `id` under `parent` (or to a root when `parent` is `null`) at
|
|
328
|
+
* `index` within the new sibling list, appending when `index` is omitted.
|
|
329
|
+
* Siblings are reindexed. Recorded as one undoable step.
|
|
330
|
+
*
|
|
331
|
+
* Rejected (throws) when:
|
|
332
|
+
* - `parent` exists but is a **leaf**, not a container;
|
|
333
|
+
* - the move would form a **cycle** — `parent` is `id` itself or one of
|
|
334
|
+
* `id`'s own descendants;
|
|
335
|
+
* - it would drop `id` **below its new parent's layer** (child may not
|
|
336
|
+
* render below its parent).
|
|
337
|
+
*
|
|
338
|
+
* `move(id, null)` — detaching to a root — is always allowed regardless of
|
|
339
|
+
* layer, since a root has no parent to render beneath. */
|
|
340
|
+
move(id: NodeId, parent: NodeId | null, index?: number): void;
|
|
341
|
+
/** Shift `id` to `index` within its **current** parent's child list. Unlike
|
|
342
|
+
* {@link move}, the parent never changes — only sibling order. */
|
|
343
|
+
reorder(id: NodeId, index: number): void;
|
|
344
|
+
setLayerVisible(layer: TLayer, visible: boolean): void;
|
|
345
|
+
setLayerLocked(layer: TLayer, locked: boolean): void;
|
|
346
|
+
addLayer(spec: AddLayerSpec<TLayer>): void;
|
|
347
|
+
removeLayer(layer: TLayer): void;
|
|
348
|
+
renameLayer(layer: TLayer, name: string): void;
|
|
349
|
+
moveLayer(layer: TLayer, index: number): void;
|
|
350
|
+
registerOp<P>(kind: string, handler: RegisteredOp<P>): void;
|
|
351
|
+
recordOp<P>(op: {
|
|
352
|
+
kind: string;
|
|
353
|
+
payload: P;
|
|
354
|
+
}): void;
|
|
355
|
+
/** Install (or clear) the active-journal accessor after scene construction.
|
|
356
|
+
* Useful when the journal source (typically a mode machine) is built
|
|
357
|
+
* with `scene.history` as a dependency — a chicken-and-egg situation
|
|
358
|
+
* where the accessor can't be passed in via `UseSceneOptions`.
|
|
359
|
+
*
|
|
360
|
+
* Pass `null` to detach. Overrides any `getActiveJournal` set in
|
|
361
|
+
* `UseSceneOptions`. */
|
|
362
|
+
setActiveJournalAccessor(fn: (() => _weasel_js_history.Journal | null) | null): void;
|
|
363
|
+
/** Apply a batch of ops with journal-aware routing.
|
|
364
|
+
*
|
|
365
|
+
* - **Without active journal** (or no `getActiveJournal` in options):
|
|
366
|
+
* the ops themselves are recorded as one undo entry on the scene's own
|
|
367
|
+
* history, rebound to `adapter` — undo replays each op's `invert()`
|
|
368
|
+
* against that same adapter. Consecutive `applyBatch` entries can
|
|
369
|
+
* coalesce via matching op `coalesceKey`s when the scene opts into
|
|
370
|
+
* `coalesceWindowMs`.
|
|
371
|
+
* - **With active journal**: routes ops to `journal.applyBatch(ops, label)`.
|
|
372
|
+
* The scene's history recording is suppressed for the duration so the
|
|
373
|
+
* journal's inner history — not the scene's undo stack — tracks the batch.
|
|
374
|
+
* Mutations still happen on `adapter` / scene state.
|
|
375
|
+
*
|
|
376
|
+
* `adapter` must be the same adapter the ops expect (typically a
|
|
377
|
+
* `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
|
|
378
|
+
* adapter. */
|
|
379
|
+
applyBatch(ops: Op[], label: string, adapter: unknown): void;
|
|
380
|
+
undo(): boolean;
|
|
381
|
+
redo(): boolean;
|
|
382
|
+
canUndo(): boolean;
|
|
383
|
+
canRedo(): boolean;
|
|
384
|
+
batch<T>(label: string, fn: () => T): T;
|
|
385
|
+
/** Read-only snapshot of every history entry currently reachable from
|
|
386
|
+
* the present state. Oldest applied first, then redoable entries in
|
|
387
|
+
* the order they'd be re-applied. Each entry id is stable. */
|
|
388
|
+
historyEntries(): readonly {
|
|
389
|
+
id: string;
|
|
390
|
+
label: string;
|
|
391
|
+
}[];
|
|
392
|
+
/** Index of the "current state". Equals the count of applied entries;
|
|
393
|
+
* `0` means "nothing applied" (initial). */
|
|
394
|
+
historyIndex(): number;
|
|
395
|
+
/** Jump to the given history index by calling undo/redo repeatedly.
|
|
396
|
+
* Clamps to [0, total]. Returns true if any movement occurred. */
|
|
397
|
+
jumpToHistoryIndex(index: number): boolean;
|
|
398
|
+
/** Snapshot the undo/redo history in a JSON-serializable form (the
|
|
399
|
+
* engine's `SerializedHistory`). Entries containing any nameless op are
|
|
400
|
+
* dropped (hand-rolled anonymous ops passed to `applyBatch`); kit and
|
|
401
|
+
* consumer-registered ops always carry names. Payload JSON-safety
|
|
402
|
+
* (e.g. typed arrays inside poses) is the caller's concern. Do not call
|
|
403
|
+
* mid-`batch` — the open batch's ops are not yet recorded. */
|
|
404
|
+
serializeHistory(): SerializedHistory;
|
|
405
|
+
/** Replace the undo/redo history from a `serializeHistory()` snapshot.
|
|
406
|
+
* Call on a scene whose node/layer state already matches the snapshot's
|
|
407
|
+
* head state (i.e. right after `loadState` from the paired scene
|
|
408
|
+
* snapshot); node state is NOT mutated. Ops re-registered via
|
|
409
|
+
* `registerOp` before this call round-trip; unknown kinds become no-op
|
|
410
|
+
* placeholders; external ops rebuild via the global op-factory registry
|
|
411
|
+
* and replay against the `setHistoryAdapter` accessor. Restored entries
|
|
412
|
+
* never coalesce with new ones. Notifies once. Do not call mid-`batch`:
|
|
413
|
+
* the stacks are replaced underneath the open batch, whose eventual
|
|
414
|
+
* flush would graft onto (and evict against) the restored stacks. */
|
|
415
|
+
restoreHistory(snapshot: SerializedHistory): void;
|
|
416
|
+
/** Install (or clear with `null`) the accessor for the adapter that
|
|
417
|
+
* RESTORED external ops (recorded via `applyBatch`, rebuilt from a
|
|
418
|
+
* `restoreHistory` snapshot) apply against on undo/redo. Resolved lazily
|
|
419
|
+
* at each apply, so wiring order relative to `restoreHistory` doesn't
|
|
420
|
+
* matter. Live `applyBatch` entries are unaffected (they bind their
|
|
421
|
+
* call-site adapter). If unset when a restored op applies, the op is a
|
|
422
|
+
* debug-warned no-op. */
|
|
423
|
+
setHistoryAdapter(fn: (() => unknown) | null): void;
|
|
424
|
+
/** Snapshot the current scene state to a JSON-serializable shape.
|
|
425
|
+
* History (undo/redo stacks) is NOT captured. Function fields like
|
|
426
|
+
* `ContainerNode.clipFromPose` are translated to string keys via the
|
|
427
|
+
* scene's registry; throws if any function field has no matching key. */
|
|
428
|
+
toJSON(): SerializedScene<TData, TLayer, TPose>;
|
|
429
|
+
/** Replace this scene's entire node + layer state in place from a snapshot
|
|
430
|
+
* produced by `toJSON()`. Unlike `sceneFromJSON`, the existing Scene
|
|
431
|
+
* instance is preserved — holders such as `<SceneCanvas>` keep their
|
|
432
|
+
* reference. History (undo/redo) is cleared, matching `sceneFromJSON`.
|
|
433
|
+
* Bumps `getVersion()` and notifies subscribers exactly once.
|
|
434
|
+
*
|
|
435
|
+
* Throws on an unsupported version or unknown registry/layer ids; on a
|
|
436
|
+
* malformed snapshot the scene is left empty or partially populated (callers should treat a
|
|
437
|
+
* `loadState` throw as fatal and reload). Snapshots from `toJSON()` are
|
|
438
|
+
* always well-formed. */
|
|
439
|
+
loadState(json: SerializedScene<TData, TLayer, TPose>): void;
|
|
440
|
+
subscribe(listener: () => void): () => void;
|
|
441
|
+
/** Monotonically increasing version. Snapshot for `useSyncExternalStore`. */
|
|
442
|
+
getVersion(): number;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export { type AddLayerSpec as A, type ContainerNode as C, IDENTITY_POSE_COMPOSITION as I, type LayerRecord as L, type NodeId as N, type PoseComposition as P, type RectPose as R, type Scene as S, type UseSceneOptions as U, type Node as a, type SerializedScene as b, type SceneRegistry as c, type RegisteredOp as d, type AddNodeSpec as e, type LeafNode as f, type PoseAdapter as g, type SerializedNode as h, type SystemLayerRecord as i, type SystemLayerSpec as j, type UserLayerRecord as k, asNodeId as l, composeRectPose as m, composeWorldPose as n, decomposeRectPose as o, rebaseLocalPose as r, translateRectPose as t, worldPoseLookup as w };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guide — a horizontal or vertical world-space line used as a snap target
|
|
3
|
+
* and an optional rendered overlay.
|
|
4
|
+
*
|
|
5
|
+
* `axis === 'x'` means a vertical line at world `x = offset` (snaps the X
|
|
6
|
+
* coordinate); `axis === 'y'` means a horizontal line at world `y = offset`
|
|
7
|
+
* (snaps the Y coordinate). This matches "the X axis snaps along X."
|
|
8
|
+
*/
|
|
9
|
+
interface Guide {
|
|
10
|
+
/** Stable, caller-supplied id. Used by removeGuide and as a render key. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Which world axis this guide constrains. */
|
|
13
|
+
axis: 'x' | 'y';
|
|
14
|
+
/** World-space offset along the constrained axis. */
|
|
15
|
+
offset: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { Guide as G };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Pan offset (in pixels) plus per-axis zoom (pixels per content unit). */
|
|
2
|
+
interface ViewTransform {
|
|
3
|
+
panX: number;
|
|
4
|
+
panY: number;
|
|
5
|
+
zoom: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Project a world-space point to screen-space pixels through a `ViewTransform`. */
|
|
11
|
+
declare function worldToScreen(worldX: number, worldY: number, view: ViewTransform): [number, number];
|
|
12
|
+
/** Inverse of `worldToScreen` — recover the world-space point under a screen-space pixel. */
|
|
13
|
+
declare function screenToWorld(screenX: number, screenY: number, view: ViewTransform): [number, number];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Viewport state. `(view.x, view.y)` is the **world point currently
|
|
17
|
+
* rendered at the canvas top-left**; `view.scale.x` / `view.scale.y` is
|
|
18
|
+
* pixels per world unit on each axis (default `{ x: 1, y: 1 }`). So:
|
|
19
|
+
*
|
|
20
|
+
* screenX = (worldX - view.x) * view.scale.x
|
|
21
|
+
* screenY = (worldY - view.y) * view.scale.y
|
|
22
|
+
* worldX = screenX / view.scale.x + view.x
|
|
23
|
+
* worldY = screenY / view.scale.y + view.y
|
|
24
|
+
*
|
|
25
|
+
* `scale` is always a 2-vector. Input convenience types
|
|
26
|
+
* {@link ZoomFactor} and {@link ZoomBound} let callers pass a scalar
|
|
27
|
+
* when they want both axes treated the same.
|
|
28
|
+
*/
|
|
29
|
+
interface View {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
scale: {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Input convenience for zoom primitives. A `number` is treated as a
|
|
39
|
+
* uniform factor applied to both axes; a `{x, y}` vector applies
|
|
40
|
+
* per-axis factors.
|
|
41
|
+
*/
|
|
42
|
+
type ZoomFactor = number | {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Input convenience for zoom-clamp ranges. A `number` is applied as the
|
|
48
|
+
* same bound on both axes; a `{x, y}` vector applies per-axis bounds.
|
|
49
|
+
*/
|
|
50
|
+
type ZoomBound = number | {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Bridge `View` into the legacy `ViewTransform` shape so chrome can keep
|
|
56
|
+
* calling `worldToScreen` / `screenToWorld`. `View` and `ViewTransform`
|
|
57
|
+
* use opposite sign conventions for the translation half (`view.x` is
|
|
58
|
+
* camera position; `panX` is canvas translation), so the adapter flips
|
|
59
|
+
* the sign and multiplies by per-axis scale.
|
|
60
|
+
*/
|
|
61
|
+
declare function viewToTransform(view: View): ViewTransform;
|
|
62
|
+
|
|
63
|
+
export { type View as V, type ZoomBound as Z, type ViewTransform as a, type ZoomFactor as b, screenToWorld as s, viewToTransform as v, worldToScreen as w };
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/core",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Domain-agnostic 2D scene graph primitives for React: viewport math, drag/resize/insert/clone interactions, layered canvas rendering.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "orochi235",
|
|
7
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./move": {
|
|
36
|
+
"import": "./dist/move.js",
|
|
37
|
+
"types": "./dist/move.d.ts"
|
|
38
|
+
},
|
|
39
|
+
"./resize": {
|
|
40
|
+
"import": "./dist/resize.js",
|
|
41
|
+
"types": "./dist/resize.d.ts"
|
|
42
|
+
},
|
|
43
|
+
"./insert": {
|
|
44
|
+
"import": "./dist/insert.js",
|
|
45
|
+
"types": "./dist/insert.d.ts"
|
|
46
|
+
},
|
|
47
|
+
"./clipboard": {
|
|
48
|
+
"import": "./dist/clipboard.js",
|
|
49
|
+
"types": "./dist/clipboard.d.ts"
|
|
50
|
+
},
|
|
51
|
+
"./clone": {
|
|
52
|
+
"import": "./dist/clone.js",
|
|
53
|
+
"types": "./dist/clone.d.ts"
|
|
54
|
+
},
|
|
55
|
+
"./patterns-builtin": {
|
|
56
|
+
"import": "./dist/patterns-builtin.js",
|
|
57
|
+
"types": "./dist/patterns-builtin.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"./renderer": {
|
|
60
|
+
"import": "./dist/renderer.js",
|
|
61
|
+
"types": "./dist/renderer.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./routing": {
|
|
64
|
+
"import": "./dist/routing.js",
|
|
65
|
+
"types": "./dist/routing.d.ts"
|
|
66
|
+
},
|
|
67
|
+
"./package.json": "./package.json"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": ">=18"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@weasel-js/geom": "0.5.0",
|
|
74
|
+
"@weasel-js/gestures": "0.5.0",
|
|
75
|
+
"@weasel-js/history": "0.5.0",
|
|
76
|
+
"@weasel-js/modes": "0.5.0",
|
|
77
|
+
"earcut": "2.2.4",
|
|
78
|
+
"polygon-clipping": "^0.15.7"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"keywords": [
|
|
84
|
+
"react",
|
|
85
|
+
"canvas",
|
|
86
|
+
"scene-graph",
|
|
87
|
+
"drag",
|
|
88
|
+
"resize",
|
|
89
|
+
"viewport",
|
|
90
|
+
"2d"
|
|
91
|
+
],
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "tsup",
|
|
94
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
95
|
+
}
|
|
96
|
+
}
|