@weasel-js/core 0.8.0 → 1.0.1
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 +748 -0
- package/dist/{DrawCommand-BKJ5JPkC.d.ts → DrawCommand-DCmiNkBj.d.ts} +14 -169
- package/dist/{chunk-GLZLSPGJ.js → chunk-6SHZHXXR.js} +46 -9
- package/dist/chunk-6SHZHXXR.js.map +1 -0
- package/dist/chunk-DCIU3SRK.js +505 -0
- package/dist/chunk-DCIU3SRK.js.map +1 -0
- package/dist/{chunk-DLV5F4NY.js → chunk-FVNWK7U3.js} +1525 -763
- package/dist/chunk-FVNWK7U3.js.map +1 -0
- package/dist/chunk-IO5Z75X4.js +167 -0
- package/dist/chunk-IO5Z75X4.js.map +1 -0
- package/dist/{chunk-BPRHGTLQ.js → chunk-UB6A6L77.js} +3 -3
- package/dist/{chunk-BPRHGTLQ.js.map → chunk-UB6A6L77.js.map} +1 -1
- package/dist/{chunk-CMBE45MT.js → chunk-Z3KNFTTS.js} +6 -6
- package/dist/chunk-Z3KNFTTS.js.map +1 -0
- package/dist/clipboard.d.ts +1 -1
- package/dist/{index-Clf_koyR.d.ts → index-DPI8DV0A.d.ts} +104 -91
- package/dist/index.d.ts +352 -43
- package/dist/index.js +6 -6
- package/dist/insert.js +5 -5
- package/dist/insert.js.map +1 -1
- package/dist/move.js +2 -2
- package/dist/paint-types-CnLIzqq1.d.ts +261 -0
- package/dist/patterns-builtin.d.ts +57 -2
- package/dist/patterns-builtin.js +1 -97
- package/dist/patterns-builtin.js.map +1 -1
- package/dist/renderer.d.ts +76 -15
- package/dist/renderer.js +6 -6
- package/dist/resize.js +2 -2
- package/dist/routing.d.ts +5 -5
- package/dist/routing.js +1 -1
- package/dist/{types-DUpI7Apc.d.ts → types-Kh_osAq9.d.ts} +8 -1
- package/dist/{viewToMat3-DFMdZvVl.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
- package/package.json +6 -6
- package/dist/chunk-6DAUIMTO.js +0 -233
- package/dist/chunk-6DAUIMTO.js.map +0 -1
- package/dist/chunk-CMBE45MT.js.map +0 -1
- package/dist/chunk-DLV5F4NY.js.map +0 -1
- package/dist/chunk-GLZLSPGJ.js.map +0 -1
- package/dist/chunk-Y52N27PF.js +0 -39
- package/dist/chunk-Y52N27PF.js.map +0 -1
- package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
package/dist/renderer.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { I as ImageMinification, G as GLImageCache } from './viewToMat3-
|
|
2
|
-
export { V as ViewLike, v as viewToMat3 } from './viewToMat3-
|
|
3
|
-
import { G as GradStop,
|
|
4
|
-
export {
|
|
1
|
+
import { I as ImageMinification, G as GLImageCache } from './viewToMat3-BvaWCN99.js';
|
|
2
|
+
export { V as ViewLike, v as viewToMat3 } from './viewToMat3-BvaWCN99.js';
|
|
3
|
+
import { G as GradStop, S as Stroke } from './paint-types-CnLIzqq1.js';
|
|
4
|
+
export { a as TextureHandle, r as registerTexture } from './paint-types-CnLIzqq1.js';
|
|
5
|
+
import { M as Mat3, S as ShaderProgramHandle, D as DrawCommand } from './DrawCommand-DCmiNkBj.js';
|
|
6
|
+
export { G as GroupDrawCommand, I as ImageDrawCommand, P as PathDrawCommand, a as ShaderDrawCommand, b as ShaderUniform, c as SolidPaint, T as TextDrawCommand, m as mat3, r as registerProgram } from './DrawCommand-DCmiNkBj.js';
|
|
5
7
|
import { P as Path } from './path-B6MMiodD.js';
|
|
6
8
|
export { canQueryLocalFonts, enableLocalFontOutlines, hasFontOutlines, isCanvasFont, listFontOutlines, outlineStatus, registerCanvasFont, registerFont, registerFontOutlines, subscribeGlyphReady, unregisterCanvasFont, unregisterFontOutlines } from '@weasel-js/font';
|
|
7
|
-
export { T as TextureHandle, r as registerTexture } from './registerTexture-BzHTLhD9.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Minimal compile/link/lookup wrapper for a GL program. Throws
|
|
@@ -153,12 +154,18 @@ declare class GLMeshCache {
|
|
|
153
154
|
* linear filtering, and mipmap resampling corrupts the multi-channel SDF signal.
|
|
154
155
|
*/
|
|
155
156
|
type TexSource = HTMLImageElement | ImageBitmap | ImageData | HTMLCanvasElement;
|
|
157
|
+
/** How a texture samples outside `0..1`. Pattern tiles need `'repeat'`;
|
|
158
|
+
* everything else clamps. WebGL2 allows `REPEAT` on NPOT textures, so a
|
|
159
|
+
* tile of any size tiles correctly. */
|
|
160
|
+
type TextureWrap = 'clamp' | 'repeat';
|
|
156
161
|
declare class GLTextureCache {
|
|
157
162
|
private readonly gl;
|
|
158
163
|
private readonly map;
|
|
159
164
|
constructor(gl: WebGL2RenderingContext);
|
|
160
165
|
has(id: string): boolean;
|
|
161
|
-
|
|
166
|
+
/** Uploads once per id; `wrap` therefore applies on first upload only.
|
|
167
|
+
* An id is either a pattern tile or a shader texture, never both. */
|
|
168
|
+
upload(id: string, source: TexSource, wrap?: TextureWrap): string;
|
|
162
169
|
/** Create a single-channel R8 texture from raw bytes (full upload).
|
|
163
170
|
* No-op if `id` already exists. Same LINEAR/CLAMP params as `upload`;
|
|
164
171
|
* UNPACK_ALIGNMENT dropped to 1 for non-4-aligned row widths. */
|
|
@@ -226,6 +233,56 @@ declare class GroupState {
|
|
|
226
233
|
pop(): void;
|
|
227
234
|
}
|
|
228
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Growable vertex staging for consecutive solid-fill geometry.
|
|
238
|
+
*
|
|
239
|
+
* Geometry only: `draw.ts` owns when a run starts, what breaks it, and the
|
|
240
|
+
* uniforms the flush draws under. Colors ride the vertices (the batch program
|
|
241
|
+
* is `pathFillVColor` with `u_color` left at white) because shapes in a run
|
|
242
|
+
* differ in color and a merged draw has one set of uniforms — and so does the
|
|
243
|
+
* model transform, applied here rather than uploaded, so that shapes under
|
|
244
|
+
* different transforms still share a draw.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
declare class SolidBatch {
|
|
248
|
+
private readonly gl;
|
|
249
|
+
private readonly vao;
|
|
250
|
+
private readonly vbo;
|
|
251
|
+
private readonly ibo;
|
|
252
|
+
private verts;
|
|
253
|
+
private idx;
|
|
254
|
+
private nVerts;
|
|
255
|
+
private nIdx;
|
|
256
|
+
/** What the GPU buffers are currently sized for. */
|
|
257
|
+
private vboVerts;
|
|
258
|
+
private iboIdx;
|
|
259
|
+
constructor(gl: WebGL2RenderingContext, prog: ShaderProgram);
|
|
260
|
+
get length(): number;
|
|
261
|
+
/** Whether staging `vertices` more would put the run past the per-flush cap. */
|
|
262
|
+
wouldOverflow(vertices: number): boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Append one rect's four corners through `m`, all carrying `rgba` (straight
|
|
265
|
+
* alpha). An affine maps a rect to a parallelogram, so two triangles still
|
|
266
|
+
* cover it and the batch draws at `u_model` identity.
|
|
267
|
+
*/
|
|
268
|
+
pushRect(x: number, y: number, w: number, h: number, m: Mat3, r: number, g: number, b: number, a: number): void;
|
|
269
|
+
/**
|
|
270
|
+
* Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
|
|
271
|
+
* mesh's own indices are rebased onto the staged vertices, which is why the
|
|
272
|
+
* index buffer is uploaded per flush rather than written once.
|
|
273
|
+
*/
|
|
274
|
+
pushMesh(mesh: Mesh, m: Mat3, r: number, g: number, b: number, a: number): void;
|
|
275
|
+
/** Upload the staged geometry and bind the batch VAO. Returns the index
|
|
276
|
+
* count for the caller's `drawElements`. */
|
|
277
|
+
uploadAndBind(): number;
|
|
278
|
+
reset(): void;
|
|
279
|
+
dispose(): void;
|
|
280
|
+
/** Grow the CPU arrays so `vertices` / `indices` more fit. */
|
|
281
|
+
private reserve;
|
|
282
|
+
/** Size both GPU buffers for at least what is staged, doubling to amortize. */
|
|
283
|
+
private growGpu;
|
|
284
|
+
}
|
|
285
|
+
|
|
229
286
|
interface WeaselRendererOptions {
|
|
230
287
|
gl?: WebGL2RenderingContext;
|
|
231
288
|
canvas?: HTMLCanvasElement;
|
|
@@ -266,6 +323,7 @@ declare class WeaselRenderer {
|
|
|
266
323
|
private textSdfR8;
|
|
267
324
|
private imageFill;
|
|
268
325
|
private gradFill;
|
|
326
|
+
private patternFill;
|
|
269
327
|
private meshCache;
|
|
270
328
|
private textureCache;
|
|
271
329
|
private imageCache;
|
|
@@ -273,10 +331,7 @@ declare class WeaselRenderer {
|
|
|
273
331
|
private programRegistry;
|
|
274
332
|
private quadVbo;
|
|
275
333
|
private quadIbo;
|
|
276
|
-
|
|
277
|
-
private rectVao;
|
|
278
|
-
private rectVbo;
|
|
279
|
-
private rectIbo;
|
|
334
|
+
private solidBatch;
|
|
280
335
|
private readonly groupState;
|
|
281
336
|
private widthCss;
|
|
282
337
|
private heightCss;
|
|
@@ -293,10 +348,6 @@ declare class WeaselRenderer {
|
|
|
293
348
|
* calls and `registerProgram()` calls. */
|
|
294
349
|
private disposed;
|
|
295
350
|
constructor(opts: WeaselRendererOptions);
|
|
296
|
-
/** Allocate the shared rect VAO + dynamic VBO (4 verts × 2 floats) + static
|
|
297
|
-
* IBO (6 indices). drawRectFast bufferSubData's the 4 corner coords each
|
|
298
|
-
* draw, avoiding per-rect buffer allocation in animated demos. */
|
|
299
|
-
private uploadRectGeometry;
|
|
300
351
|
private uploadQuadGeometry;
|
|
301
352
|
/**
|
|
302
353
|
* Compile a consumer-registered shader program against this renderer's GL context.
|
|
@@ -338,7 +389,15 @@ declare class WeaselRenderer {
|
|
|
338
389
|
* A disposed renderer ignores further `render()` and `registerProgram()`
|
|
339
390
|
* calls. */
|
|
340
391
|
dispose(): void;
|
|
341
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Draw one frame.
|
|
394
|
+
*
|
|
395
|
+
* `viewMatrix` is the frame's world→screen transform. It is only consulted
|
|
396
|
+
* by `units: 'world'` gradients, which fall back to screen space without
|
|
397
|
+
* it — every other command carries its own transform in the stream, so
|
|
398
|
+
* callers with no view concept can keep calling `render(commands)`.
|
|
399
|
+
*/
|
|
400
|
+
render(commands: DrawCommand[], viewMatrix?: Mat3): void;
|
|
342
401
|
resize(dims: {
|
|
343
402
|
width: number;
|
|
344
403
|
height: number;
|
|
@@ -347,10 +406,12 @@ declare class WeaselRenderer {
|
|
|
347
406
|
/** @internal */ _gl(): WebGL2RenderingContext;
|
|
348
407
|
/** @internal */ _pathFill(): ShaderProgram;
|
|
349
408
|
/** @internal */ _pathFillVColor(): ShaderProgram;
|
|
409
|
+
/** @internal */ _solidBatch(): SolidBatch;
|
|
350
410
|
/** @internal */ _textSdf(): ShaderProgram;
|
|
351
411
|
/** @internal */ _textSdfR8(): ShaderProgram;
|
|
352
412
|
/** @internal */ _imageFill(): ShaderProgram;
|
|
353
413
|
/** @internal */ _gradFill(): ShaderProgram;
|
|
414
|
+
/** @internal */ _patternFill(): ShaderProgram;
|
|
354
415
|
/** @internal */ _meshCache(): GLMeshCache;
|
|
355
416
|
/** @internal */ _textureCache(): GLTextureCache;
|
|
356
417
|
/** @internal */ _imageCache(): GLImageCache;
|
package/dist/renderer.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { IDENTITY_COLOR_MATRIX, OUTLINE_MIN_SCREEN_PX, ShaderCompileError, WeaselRenderer, buildGradientRamp, canQueryLocalFonts, enableLocalFontOutlines, hasFontOutlines, isCanvasFont, listFontOutlines, mat3, outlineStatus, registerCanvasFont, registerFont, registerFontOutlines, registerProgram, subscribeGlyphReady, tessellate, tessellateStroke, unregisterCanvasFont, unregisterFontOutlines, viewToMat3 } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-
|
|
1
|
+
export { IDENTITY_COLOR_MATRIX, OUTLINE_MIN_SCREEN_PX, ShaderCompileError, WeaselRenderer, buildGradientRamp, canQueryLocalFonts, enableLocalFontOutlines, hasFontOutlines, isCanvasFont, listFontOutlines, mat3, outlineStatus, registerCanvasFont, registerFont, registerFontOutlines, registerProgram, subscribeGlyphReady, tessellate, tessellateStroke, unregisterCanvasFont, unregisterFontOutlines, viewToMat3 } from './chunk-FVNWK7U3.js';
|
|
2
|
+
import './chunk-DCIU3SRK.js';
|
|
3
|
+
import './chunk-UB6A6L77.js';
|
|
4
|
+
import './chunk-Z3KNFTTS.js';
|
|
5
|
+
import './chunk-6SHZHXXR.js';
|
|
6
6
|
import './chunk-775XXAHR.js';
|
|
7
7
|
import './chunk-UGFFCMQP.js';
|
|
8
8
|
import './chunk-BHVYVFGV.js';
|
|
9
9
|
import './chunk-GVCNT7UH.js';
|
|
10
|
-
export { registerTexture } from './chunk-
|
|
10
|
+
export { registerTexture } from './chunk-IO5Z75X4.js';
|
|
11
11
|
import './chunk-PZ5AY32C.js';
|
|
12
12
|
//# sourceMappingURL=renderer.js.map
|
|
13
13
|
//# sourceMappingURL=renderer.js.map
|
package/dist/resize.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { CORNER_ANCHORS, DEFAULT_RESIZE_BEHAVIORS, clampMinSize, cornerPoint, cornerResizeHandles, fixedCornerOf, hitCornerHandle, lockAspectWithModifier, pointSnapToGrid, snapToGrid, snapToGuides } from './chunk-
|
|
2
|
-
export { RECT_POSE_DESCRIPTOR, ROTATED_POSE_DESCRIPTOR } from './chunk-
|
|
1
|
+
export { CORNER_ANCHORS, DEFAULT_RESIZE_BEHAVIORS, clampMinSize, cornerPoint, cornerResizeHandles, fixedCornerOf, hitCornerHandle, lockAspectWithModifier, pointSnapToGrid, snapToGrid, snapToGuides } from './chunk-Z3KNFTTS.js';
|
|
2
|
+
export { RECT_POSE_DESCRIPTOR, ROTATED_POSE_DESCRIPTOR } from './chunk-6SHZHXXR.js';
|
|
3
3
|
import './chunk-PZ5AY32C.js';
|
|
4
4
|
//# sourceMappingURL=resize.js.map
|
|
5
5
|
//# sourceMappingURL=resize.js.map
|
package/dist/routing.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { bd as Conflict, be as PREDICATE_TARGET, bf as RegistryEntry, bg as ToolDef, m as ToolKeybinding, bh as ToolScopes, bi as ViewportToolDef, bj as buildRouteRegistry, bk as defineTool, bl as defineViewportTool, bm as findConflicts, bn as findScopedConflicts, bo as formatConflict, bp as reportRouteConflicts } from './index-DPI8DV0A.js';
|
|
2
2
|
export { ChannelRef, DescribeRouteOptions, GESTURE_DESCRIPTORS, GestureArgSpec, GestureDescriptor, GestureName, ModRequirement, ModifierKey, ParsedModifiers, ParsedRoute, PhaseAtom, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, RouteDescriptionPart, RouteFieldName, RouteTermLabel, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
|
|
3
|
-
import './types-
|
|
3
|
+
import './types-Kh_osAq9.js';
|
|
4
4
|
import '@weasel-js/history';
|
|
5
5
|
import './path-B6MMiodD.js';
|
|
6
6
|
import './types-BhJJ2OCM.js';
|
|
@@ -8,9 +8,9 @@ import './types-B_-khFM0.js';
|
|
|
8
8
|
import './view-DSQgxBJB.js';
|
|
9
9
|
import '@weasel-js/modes';
|
|
10
10
|
import 'react';
|
|
11
|
-
import './DrawCommand-BKJ5JPkC.js';
|
|
12
|
-
import './registerTexture-BzHTLhD9.js';
|
|
13
|
-
import 'react/jsx-runtime';
|
|
14
11
|
import './geometry-DVeZ2i-c.js';
|
|
12
|
+
import './paint-types-CnLIzqq1.js';
|
|
13
|
+
import './DrawCommand-DCmiNkBj.js';
|
|
14
|
+
import 'react/jsx-runtime';
|
|
15
15
|
import '@weasel-js/geom';
|
|
16
16
|
import './types-BJ8_cyT7.js';
|
package/dist/routing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { GESTURE_DESCRIPTORS, PREDICATE_TARGET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, buildRouteRegistry, canonicalModifiers, collapseShiftPairs, defineTool, defineViewportTool, describeRoute, describeRouteParts, findConflicts, findScopedConflicts, formatConflict, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute, reportRouteConflicts } from './chunk-
|
|
1
|
+
export { GESTURE_DESCRIPTORS, PREDICATE_TARGET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, buildRouteRegistry, canonicalModifiers, collapseShiftPairs, defineTool, defineViewportTool, describeRoute, describeRouteParts, findConflicts, findScopedConflicts, formatConflict, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute, reportRouteConflicts } from './chunk-DCIU3SRK.js';
|
|
2
2
|
import './chunk-PZ5AY32C.js';
|
|
3
3
|
//# sourceMappingURL=routing.js.map
|
|
4
4
|
//# sourceMappingURL=routing.js.map
|
|
@@ -7,7 +7,7 @@ import { P as Path } from './path-B6MMiodD.js';
|
|
|
7
7
|
* shape used by `composeRectPose` and the `unionBounds` helper. Rotation is
|
|
8
8
|
* in radians, pivoted on the unrotated AABB center; absent === 0. Kit-side
|
|
9
9
|
* code that consumes rotation already reads `pose.rotation ?? 0`
|
|
10
|
-
* (`
|
|
10
|
+
* (`wrapNodeOutput`, `rotate/handle.ts`, `pathInWorld.ts`), so
|
|
11
11
|
* the slot exists on every default scene whether or not the consumer
|
|
12
12
|
* populates it.
|
|
13
13
|
*/
|
|
@@ -212,6 +212,13 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
|
|
|
212
212
|
childrenOf(id: NodeId): readonly NodeId[];
|
|
213
213
|
ancestorsOf(id: NodeId): readonly NodeId[];
|
|
214
214
|
renderOrder(): Iterable<NodeId>;
|
|
215
|
+
/** The same layer-major sequence as {@link Scene.renderOrder}, as the nodes
|
|
216
|
+
* themselves. Prefer this wherever the ids are only going to be resolved
|
|
217
|
+
* back to nodes: the traversal already holds them, and re-looking each one
|
|
218
|
+
* up was ~40% of the area hit-test's per-node cost. Cached until a
|
|
219
|
+
* structural edit, so repeat calls hand back the same array — a snapshot,
|
|
220
|
+
* not a live view, and not yours to mutate. */
|
|
221
|
+
renderOrderNodes(): readonly Node<TData, TLayer, TPose>[];
|
|
215
222
|
add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;
|
|
216
223
|
/** Delete `id` **and its entire subtree** — every descendant is removed in
|
|
217
224
|
* the same operation. Recorded as one undoable step; `undo()` restores the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Domain-agnostic 2D scene graph primitives for React: viewport math, drag/resize/insert/clone interactions, layered canvas rendering.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"react": ">=18"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@weasel-js/font": "0.
|
|
78
|
-
"@weasel-js/geom": "0.
|
|
79
|
-
"@weasel-js/gestures": "0.
|
|
80
|
-
"@weasel-js/history": "0.
|
|
81
|
-
"@weasel-js/modes": "0.
|
|
77
|
+
"@weasel-js/font": "1.0.1",
|
|
78
|
+
"@weasel-js/geom": "1.0.1",
|
|
79
|
+
"@weasel-js/gestures": "1.0.1",
|
|
80
|
+
"@weasel-js/history": "1.0.1",
|
|
81
|
+
"@weasel-js/modes": "1.0.1",
|
|
82
82
|
"earcut": "2.2.4",
|
|
83
83
|
"polygon-clipping": "^0.15.7"
|
|
84
84
|
},
|
package/dist/chunk-6DAUIMTO.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { __export } from './chunk-PZ5AY32C.js';
|
|
2
|
-
import { parseRoute, isKnownGestureName, getGestureDescriptor, formatRoute, formatPhaseAtom, describeRouteParts, describeRoute, collapseShiftPairs, canonicalModifiers, ROUTE_TERMS, ROUTE_FIELD_DEFINITIONS, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, GESTURE_DESCRIPTORS } from '@weasel-js/gestures';
|
|
3
|
-
export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
|
|
4
|
-
|
|
5
|
-
// src/tools/routing/index.ts
|
|
6
|
-
var routing_exports = {};
|
|
7
|
-
__export(routing_exports, {
|
|
8
|
-
GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
|
|
9
|
-
PREDICATE_TARGET: () => PREDICATE_TARGET,
|
|
10
|
-
RESERVED_ID_NAMES: () => RESERVED_ID_NAMES,
|
|
11
|
-
RESERVED_ID_PREFIXES: () => RESERVED_ID_PREFIXES,
|
|
12
|
-
ROUTE_FIELD_DEFINITIONS: () => ROUTE_FIELD_DEFINITIONS,
|
|
13
|
-
ROUTE_TERMS: () => ROUTE_TERMS,
|
|
14
|
-
buildRouteRegistry: () => buildRouteRegistry,
|
|
15
|
-
canonicalModifiers: () => canonicalModifiers,
|
|
16
|
-
collapseShiftPairs: () => collapseShiftPairs,
|
|
17
|
-
defineTool: () => defineTool,
|
|
18
|
-
defineViewportTool: () => defineViewportTool,
|
|
19
|
-
describeRoute: () => describeRoute,
|
|
20
|
-
describeRouteParts: () => describeRouteParts,
|
|
21
|
-
findConflicts: () => findConflicts,
|
|
22
|
-
findScopedConflicts: () => findScopedConflicts,
|
|
23
|
-
formatConflict: () => formatConflict,
|
|
24
|
-
formatPhaseAtom: () => formatPhaseAtom,
|
|
25
|
-
formatRoute: () => formatRoute,
|
|
26
|
-
getGestureDescriptor: () => getGestureDescriptor,
|
|
27
|
-
isKnownGestureName: () => isKnownGestureName,
|
|
28
|
-
parseRoute: () => parseRoute,
|
|
29
|
-
reportRouteConflicts: () => reportRouteConflicts
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// src/tools/routing/defineTool.ts
|
|
33
|
-
function validateId(id, kind) {
|
|
34
|
-
if (id.length === 0) {
|
|
35
|
-
throw new Error(`weasel: ${kind} id may not be empty`);
|
|
36
|
-
}
|
|
37
|
-
if (RESERVED_ID_PREFIXES.has(id[0])) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`weasel: ${kind} id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (RESERVED_ID_NAMES.has(id)) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`weasel: ${kind} id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function defineTool(def) {
|
|
49
|
-
validateId(def.id, "tool");
|
|
50
|
-
const resolveCursor = (ctx) => {
|
|
51
|
-
if (def.cursor == null) return "";
|
|
52
|
-
return typeof def.cursor === "function" ? def.cursor(ctx) : def.cursor;
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
id: def.id,
|
|
56
|
-
capabilities: def.capabilities,
|
|
57
|
-
actions: def.actions,
|
|
58
|
-
def,
|
|
59
|
-
presentation: def.presentation,
|
|
60
|
-
keybinding: def.keybinding,
|
|
61
|
-
onActivate: def.onActivate,
|
|
62
|
-
onDeactivate: def.onDeactivate,
|
|
63
|
-
initScratch: def.initScratch ?? (() => null),
|
|
64
|
-
cursor: resolveCursor,
|
|
65
|
-
bindings: def.bindings,
|
|
66
|
-
overlay: def.overlay
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// src/tools/routing/defineViewportTool.ts
|
|
71
|
-
function defineViewportTool(def) {
|
|
72
|
-
return defineTool(def);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/tools/routing/reflection/registry.ts
|
|
76
|
-
var PREDICATE_TARGET = "predicate";
|
|
77
|
-
var SPEC_KIND_TO_GESTURE = {
|
|
78
|
-
key: "keyDown",
|
|
79
|
-
"key-held": "keyHeld",
|
|
80
|
-
wheel: "wheel",
|
|
81
|
-
click: "click",
|
|
82
|
-
doubleClick: "dblTap",
|
|
83
|
-
contextMenu: "contextMenu",
|
|
84
|
-
longPress: "longPress",
|
|
85
|
-
drag: "drag",
|
|
86
|
-
pointerDown: "pointerDown",
|
|
87
|
-
multiTouch: void 0,
|
|
88
|
-
multiTouchTap: "multiTouchTap",
|
|
89
|
-
drop: void 0,
|
|
90
|
-
paste: void 0
|
|
91
|
-
};
|
|
92
|
-
function buildRouteRegistry(tools) {
|
|
93
|
-
const out = [];
|
|
94
|
-
for (const tool of tools) {
|
|
95
|
-
for (const binding of tool.bindings ?? []) {
|
|
96
|
-
const entry = entryFor(tool.id, binding.spec, binding.actionId);
|
|
97
|
-
if (entry) out.push(entry);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
|
-
function entryFor(toolId, spec, actionId) {
|
|
103
|
-
const gesture = SPEC_KIND_TO_GESTURE[spec.kind];
|
|
104
|
-
if (!gesture || !isKnownGestureName(gesture)) return null;
|
|
105
|
-
const descriptor = getGestureDescriptor(gesture);
|
|
106
|
-
return {
|
|
107
|
-
toolId,
|
|
108
|
-
actionId,
|
|
109
|
-
gesture,
|
|
110
|
-
phase: phaseOf("phase" in spec ? spec.phase : void 0),
|
|
111
|
-
modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
|
|
112
|
-
arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
|
|
113
|
-
target: descriptor.hasTarget ? targetOf("target" in spec ? spec.target : void 0) : void 0,
|
|
114
|
-
spec
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function phaseOf(phase) {
|
|
118
|
-
if (phase === void 0 || phase === "*") return "any";
|
|
119
|
-
if (phase === "initial" || phase === "engaged") return phase;
|
|
120
|
-
const distinct = new Set(phase.map((atom) => atom.phase));
|
|
121
|
-
if (distinct.size !== 1) return "any";
|
|
122
|
-
const only = [...distinct][0];
|
|
123
|
-
return only === "initial" || only === "engaged" ? only : "any";
|
|
124
|
-
}
|
|
125
|
-
function parseModSpec(mods) {
|
|
126
|
-
if (!mods) return {};
|
|
127
|
-
const out = {};
|
|
128
|
-
for (const [name, req] of Object.entries(mods)) {
|
|
129
|
-
if (req === true) out[name] = "required";
|
|
130
|
-
else if (req === "optional") out[name] = "optional";
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
function targetOf(target) {
|
|
135
|
-
if (target === void 0) return void 0;
|
|
136
|
-
return typeof target === "string" ? target : PREDICATE_TARGET;
|
|
137
|
-
}
|
|
138
|
-
function argOf(spec, fallback) {
|
|
139
|
-
if ("key" in spec) {
|
|
140
|
-
return Array.isArray(spec.key) ? spec.key.join("|") : spec.key;
|
|
141
|
-
}
|
|
142
|
-
if ("fingers" in spec) return String(spec.fingers);
|
|
143
|
-
if ("direction" in spec) return spec.direction ?? fallback;
|
|
144
|
-
return fallback;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/tools/routing/reflection/conflicts.ts
|
|
148
|
-
function findConflicts(tools) {
|
|
149
|
-
const entries = buildRouteRegistry(tools);
|
|
150
|
-
const groups = /* @__PURE__ */ new Map();
|
|
151
|
-
for (const entry of entries) {
|
|
152
|
-
const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ""}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
|
|
153
|
-
const bucket = groups.get(key);
|
|
154
|
-
if (bucket) bucket.push(entry);
|
|
155
|
-
else groups.set(key, [entry]);
|
|
156
|
-
}
|
|
157
|
-
const conflicts = [];
|
|
158
|
-
for (const bucket of groups.values()) {
|
|
159
|
-
if (bucket.length < 2) continue;
|
|
160
|
-
const first = bucket[0];
|
|
161
|
-
conflicts.push({
|
|
162
|
-
phase: first.phase,
|
|
163
|
-
gesture: first.gesture,
|
|
164
|
-
arg: first.arg,
|
|
165
|
-
target: first.target,
|
|
166
|
-
modifiers: first.modifiers,
|
|
167
|
-
toolIds: bucket.map((e) => e.toolId)
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return conflicts;
|
|
171
|
-
}
|
|
172
|
-
function targetKey(entry) {
|
|
173
|
-
if (entry.target !== PREDICATE_TARGET) return entry.target ?? "";
|
|
174
|
-
const spec = entry.spec;
|
|
175
|
-
const pred = spec.target;
|
|
176
|
-
if (typeof pred !== "object" || pred === null) return PREDICATE_TARGET;
|
|
177
|
-
return `${PREDICATE_TARGET}#${predicateId(pred)}`;
|
|
178
|
-
}
|
|
179
|
-
var PREDICATE_IDS = /* @__PURE__ */ new WeakMap();
|
|
180
|
-
var nextPredicateId = 0;
|
|
181
|
-
function predicateId(pred) {
|
|
182
|
-
let id = PREDICATE_IDS.get(pred);
|
|
183
|
-
if (id === void 0) {
|
|
184
|
-
id = nextPredicateId++;
|
|
185
|
-
PREDICATE_IDS.set(pred, id);
|
|
186
|
-
}
|
|
187
|
-
return id;
|
|
188
|
-
}
|
|
189
|
-
function findScopedConflicts(scopes) {
|
|
190
|
-
const registry = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
|
|
191
|
-
const ambient = scopes.ambient ?? [];
|
|
192
|
-
const out = [];
|
|
193
|
-
const seen = /* @__PURE__ */ new Set();
|
|
194
|
-
const add = (conflicts) => {
|
|
195
|
-
for (const c of conflicts) {
|
|
196
|
-
const key = `${c.phase}|${c.gesture}|${c.arg ?? ""}|${c.target ?? ""}|${canonicalModifiers(c.modifiers)}|${c.toolIds.join(",")}`;
|
|
197
|
-
if (seen.has(key)) continue;
|
|
198
|
-
seen.add(key);
|
|
199
|
-
out.push(c);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
for (const tool of [...registry, ...ambient]) add(findConflicts([tool]));
|
|
203
|
-
if (ambient.length > 1) add(findConflicts(ambient));
|
|
204
|
-
const hotkeyTools = registry.filter(
|
|
205
|
-
(t) => t.def?.hotkey !== void 0
|
|
206
|
-
);
|
|
207
|
-
if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));
|
|
208
|
-
return out;
|
|
209
|
-
}
|
|
210
|
-
function formatConflict(conflict) {
|
|
211
|
-
const route = formatRoute({
|
|
212
|
-
phases: [{ channel: "&", phase: conflict.phase === "any" ? "*" : conflict.phase }],
|
|
213
|
-
gesture: conflict.gesture,
|
|
214
|
-
arg: conflict.arg,
|
|
215
|
-
target: conflict.target,
|
|
216
|
-
modifiers: conflict.modifiers
|
|
217
|
-
});
|
|
218
|
-
return `${route} \u2014 declared by ${conflict.toolIds.join(", ")}`;
|
|
219
|
-
}
|
|
220
|
-
function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
|
|
221
|
-
const conflicts = findScopedConflicts(scopes);
|
|
222
|
-
for (const c of conflicts) {
|
|
223
|
-
warn(
|
|
224
|
-
`[weasel] route conflict: two bindings declare the same (phase, gesture, arg, target, modifiers) tuple in the same scope, so declaration order alone decides which one fires.
|
|
225
|
-
${formatConflict(c)}`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return conflicts;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export { PREDICATE_TARGET, buildRouteRegistry, defineTool, defineViewportTool, findConflicts, findScopedConflicts, formatConflict, reportRouteConflicts, routing_exports };
|
|
232
|
-
//# sourceMappingURL=chunk-6DAUIMTO.js.map
|
|
233
|
-
//# sourceMappingURL=chunk-6DAUIMTO.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/routing/index.ts","../src/tools/routing/defineTool.ts","../src/tools/routing/defineViewportTool.ts","../src/tools/routing/reflection/registry.ts","../src/tools/routing/reflection/conflicts.ts"],"names":[],"mappings":"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","file":"chunk-6DAUIMTO.js","sourcesContent":["export type { ToolDef, ViewportToolDef, ToolKeybinding } from './types';\nexport { parseRoute, formatRoute, formatPhaseAtom, collapseShiftPairs, describeRoute, describeRouteParts, canonicalModifiers, ROUTE_TERMS, ROUTE_FIELD_DEFINITIONS, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './routeGrammar';\nexport type { ParsedRoute, ParsedModifiers, ModifierKey, ModRequirement, PhaseAtom, ChannelRef, DescribeRouteOptions, RouteDescriptionPart, RouteTermLabel, RouteFieldName } from './routeGrammar';\nexport { getGestureDescriptor, isKnownGestureName, GESTURE_DESCRIPTORS } from './gestures';\nexport type { GestureName, GestureDescriptor, GestureArgSpec } from './gestures';\nexport { defineTool } from './defineTool';\nexport { defineViewportTool } from './defineViewportTool';\n\n// Reflection consumers — registry / conflict checker / debug overlay.\nexport * from './reflection';\n","// src/tools/routing/defineTool.ts\nimport type { Tool, ToolCtx } from '../types';\nimport type { ToolDef } from './types';\nimport { RESERVED_ID_NAMES, RESERVED_ID_PREFIXES } from './routeGrammar';\n\n/** Validate that `id` is usable as a tool / channel id in the route\n * grammar. Rejects ids that start with a reserved sigil (would shadow\n * future grammar extensions) and ids that collide with phase keywords\n * (would parse as the bare-phase shorthand). */\nfunction validateId(id: string, kind: 'tool' | 'action'): void {\n if (id.length === 0) {\n throw new Error(`weasel: ${kind} id may not be empty`);\n }\n if (RESERVED_ID_PREFIXES.has(id[0]!)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" starts with reserved sigil \"${id[0]}\" ` +\n `(reserved set: ${[...RESERVED_ID_PREFIXES].join(' ')})`,\n );\n }\n if (RESERVED_ID_NAMES.has(id)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" collides with a reserved phase keyword ` +\n `(reserved: ${[...RESERVED_ID_NAMES].join(', ')})`,\n );\n }\n}\n\n/**\n * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.\n *\n * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase\n * tables of hit-keyed route handlers, and `defineTool` compiled them into the\n * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the\n * tool-routing dispatcher called. Both ends of that are gone — tools declare\n * `bindings` and the gesture dispatcher routes them — so what remains is\n * identity plumbing plus the id check and the cursor resolver.\n *\n * It stays a function rather than becoming a spread because the id validation\n * and the `initScratch` / `cursor` defaults are worth applying uniformly, and\n * because `Tool.def` gives reflection a handle on the authored form.\n */\nexport function defineTool<TScratch = void>(\n def: ToolDef<TScratch>,\n): Tool<TScratch> {\n validateId(def.id, 'tool');\n\n // Normalize `string | ((ctx) => string)` to the function form so callers\n // have one shape to deal with. Returns '' (not undefined) so the signature\n // satisfies `Tool.cursor: (ctx) => string`.\n const resolveCursor = (ctx: ToolCtx<TScratch>): string => {\n if (def.cursor == null) return '';\n return typeof def.cursor === 'function' ? def.cursor(ctx) : def.cursor;\n };\n\n return {\n id: def.id,\n capabilities: def.capabilities,\n actions: def.actions,\n def,\n presentation: def.presentation,\n keybinding: def.keybinding,\n onActivate: def.onActivate,\n onDeactivate: def.onDeactivate,\n initScratch: def.initScratch ?? (() => null as unknown as TScratch),\n cursor: resolveCursor,\n bindings: def.bindings,\n overlay: def.overlay,\n };\n}\n","import type { Tool } from '../types';\nimport type { ViewportToolDef } from './types';\nimport { defineTool } from './defineTool';\n\n/**\n * Define a tool that acts on the viewport rather than the scene.\n *\n * This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`\n * (no click routes, drag restricted to the function form), and the factory\n * lifted it back to the permissive shape before handing it to `defineTool`.\n * With phase tables gone there is no shape left to narrow — a viewport tool\n * declares `bindings` like any other, pointing at `viewport.*` actions.\n *\n * It survives as an authoring signal. `defineViewportTool` at the top of a\n * hook says \"this tool moves the camera, not the drawing\", which is worth\n * more than the type gymnastics it replaced.\n */\nexport function defineViewportTool<TScratch = void>(\n def: ViewportToolDef<TScratch>,\n): Tool<TScratch> {\n return defineTool<TScratch>(def);\n}\n","import type { Tool } from '../../types';\nimport type { GestureSpec, ModSpec, TargetSpec, PhaseSpec } from '@weasel-js/gestures';\nimport type { ParsedModifiers, ModifierKey } from '../routeGrammar';\nimport { getGestureDescriptor, isKnownGestureName, type GestureName } from '../gestures';\n\n/**\n * One row in the route registry — a single `GestureBinding` on one tool,\n * flattened into the route grammar's vocabulary so the inspector, the\n * conflict checker, and `describeRoute` can all read the same shape.\n *\n * Multiple rows can share (gesture, arg, target, modifiers) if different tools\n * declare them; consumers walk the list and group client-side.\n */\nexport interface RegistryEntry {\n toolId: string;\n /** Which phase the binding's `phase` spec restricts it to. `'any'` when it\n * declares none, declares `'*'`, or declares an atom list whose atoms\n * don't agree on one phase — such a binding fires in either phase, and\n * reporting it as `'initial'` made it collide with genuinely-initial\n * bindings in `findConflicts`' bucket key. */\n phase: 'initial' | 'engaged' | 'any';\n /** Structured v3 modifier requirements. Empty object = \"no modifiers\n * held\" (the strict default). */\n modifiers: ParsedModifiers;\n gesture: GestureName;\n /** Resolved arg value for arg-bearing gestures (wheel direction,\n * key name, multiTouchTap fingers). Undefined for gestures whose\n * descriptor has no `arg`. */\n arg: string | undefined;\n /** Target class for hit-testing gestures. `undefined` when the descriptor\n * has `hasTarget: false` or the spec declares no target;\n * {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which\n * the route grammar has no notation for. */\n target: string | undefined;\n /** The action this binding fires. */\n actionId: string;\n /** The `GestureSpec` this row was flattened from, by reference. Reflection\n * consumers that need something the grammar doesn't capture — the\n * specificity tuple, a `kindOf` predicate identity — read it here rather\n * than re-walking `Tool.bindings`. */\n spec: GestureSpec;\n}\n\n/**\n * Stand-in target for a `{ kindOf }` predicate spec.\n *\n * The route grammar can name target *classes* (`empty`, `selected-body`,\n * `kind:rect`) but not arbitrary predicates, so three distinct select-tool\n * bindings all render as this one token. Narrowing it would mean giving the\n * grammar a way to describe a function, which is a real design question and\n * not one this reflection layer should answer by inventing syntax.\n */\nexport const PREDICATE_TARGET = 'predicate';\n\n/** `GestureSpec.kind` → route-grammar gesture name. `multiTouch` has no\n * route-grammar gesture (only its tap synthesis does), and `drop` / `paste`\n * shipped without grammar names, so specs of those kinds are skipped. */\nconst SPEC_KIND_TO_GESTURE: Record<GestureSpec['kind'], GestureName | undefined> = {\n key: 'keyDown',\n 'key-held': 'keyHeld',\n wheel: 'wheel',\n click: 'click',\n doubleClick: 'dblTap',\n contextMenu: 'contextMenu',\n longPress: 'longPress',\n drag: 'drag',\n pointerDown: 'pointerDown',\n multiTouch: undefined,\n multiTouchTap: 'multiTouchTap',\n drop: undefined,\n paste: undefined,\n};\n\n/**\n * Flatten every tool's `bindings` into route-registry rows.\n *\n * This was `buildActionRegistry`, and it walked `ToolDef.initial` /\n * `.engaged` phase tables — the grammar that no longer exists. It was also\n * blind to `Tool.bindings` the entire time the two lived side by side, which\n * is why the inspector under-reported select by more than half. The rename\n * fixes a second thing: it never had anything to do with the Actions\n * Registry, and reading `buildActionRegistry` next to `ActionsRegistry`\n * suggested otherwise.\n */\nexport function buildRouteRegistry(\n tools: readonly Tool<unknown>[],\n): RegistryEntry[] {\n const out: RegistryEntry[] = [];\n for (const tool of tools) {\n for (const binding of tool.bindings ?? []) {\n const entry = entryFor(tool.id, binding.spec, binding.actionId);\n if (entry) out.push(entry);\n }\n }\n return out;\n}\n\nfunction entryFor(\n toolId: string,\n spec: GestureSpec,\n actionId: string,\n): RegistryEntry | null {\n const gesture = SPEC_KIND_TO_GESTURE[spec.kind];\n if (!gesture || !isKnownGestureName(gesture)) return null;\n const descriptor = getGestureDescriptor(gesture);\n return {\n toolId,\n actionId,\n gesture,\n phase: phaseOf('phase' in spec ? spec.phase : undefined),\n modifiers: parseModSpec('mods' in spec ? spec.mods : undefined),\n arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : undefined,\n target: descriptor.hasTarget\n ? targetOf('target' in spec ? spec.target : undefined)\n : undefined,\n spec,\n };\n}\n\n/** Collapse a `PhaseSpec` to the single phase a binding is restricted to, or\n * `'any'` when it isn't restricted to one.\n *\n * `PhaseAtom.channel` says which channel's phase state the binding reads,\n * not which phase it fires in, so it plays no part in this collapse — only\n * the set of distinct `atom.phase` values matters. */\nfunction phaseOf(phase: PhaseSpec | undefined): 'initial' | 'engaged' | 'any' {\n if (phase === undefined || phase === '*') return 'any';\n if (phase === 'initial' || phase === 'engaged') return phase;\n const distinct = new Set(phase.map((atom) => atom.phase));\n if (distinct.size !== 1) return 'any';\n const only = [...distinct][0];\n return only === 'initial' || only === 'engaged' ? only : 'any';\n}\n\n/** `ModSpec` (per-key tri-state) → `ParsedModifiers`. `false` and absent both\n * mean \"must not be held\", which the parsed form spells as an absent key. */\nfunction parseModSpec(mods: ModSpec | undefined): ParsedModifiers {\n if (!mods) return {};\n const out: ParsedModifiers = {};\n for (const [name, req] of Object.entries(mods)) {\n if (req === true) out[name as ModifierKey] = 'required';\n else if (req === 'optional') out[name as ModifierKey] = 'optional';\n }\n return out;\n}\n\nfunction targetOf(target: TargetSpec | undefined): string | undefined {\n if (target === undefined) return undefined;\n return typeof target === 'string' ? target : PREDICATE_TARGET;\n}\n\nfunction argOf(spec: GestureSpec, fallback: string | undefined): string | undefined {\n if ('key' in spec) {\n return Array.isArray(spec.key) ? spec.key.join('|') : spec.key;\n }\n if ('fingers' in spec) return String(spec.fingers);\n if ('direction' in spec) return spec.direction ?? fallback;\n return fallback;\n}\n\n// Re-export for downstream consumers.\nexport { getGestureDescriptor };\nexport type { GestureName };\n","import type { Tool } from '../../types';\nimport type { ParsedModifiers } from '../routeGrammar';\nimport { canonicalModifiers, formatRoute } from '../routeGrammar';\nimport { buildRouteRegistry, PREDICATE_TARGET, type RegistryEntry, type GestureName } from './registry';\n\n/** Two or more tools declare the same exact (phase, gesture, arg, target,\n * modifiers) tuple — the dispatcher's slot precedence picks one\n * arbitrarily (well, deterministically by slot order, but the author\n * probably didn't intend the duplication). */\nexport interface Conflict {\n phase: 'initial' | 'engaged' | 'any';\n gesture: GestureName;\n arg: string | undefined;\n target: string | undefined;\n modifiers: ParsedModifiers;\n /** All tool ids that registered the same tuple. At least 2 by\n * construction. Order matches the input tools[] order. */\n toolIds: string[];\n}\n\n/** Detect exact-tuple overlaps across a tool registration set.\n *\n * Intentionally NOT flagged:\n * - Broad vs. narrow targets (e.g. an untargeted `click` alongside\n * `click` on `empty`) — the dispatcher's specificity ordering resolves\n * those cleanly, and the broad one is usually the intended fallback.\n * - Different modifier requirements on the same target — they fire on\n * different inputs.\n * - A binding whose action declines via `enabled()` so a lower-priority\n * one can take the gesture. Detecting that intent would mean evaluating\n * the action; consumers can suppress known-intentional compositions in\n * their UI layer.\n *\n * - Two `{ kindOf }` predicate targets. The route grammar renders every\n * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on\n * the rendered target alone reported select's `resize` / `rotate` / `move`\n * drags — three genuinely different predicates — as a three-way conflict.\n * Predicate entries bucket by function identity instead, which means two\n * *separately written but equivalent* predicates go unflagged. That's the\n * right way to be wrong here: this check has to be silent when nothing is\n * wrong or nobody will keep it on.\n *\n * Note that two bindings sharing a tuple on the SAME tool are now possible\n * (bindings are an array, where phase tables were objects with unique\n * keys) — so a conflict may name one tool twice.\n *\n * This is the raw same-tuple detector. Feeding it a whole tool *registry*\n * over-reports, because registry tools take turns in the active slot and\n * can't collide with each other — see {@link findScopedConflicts}.\n */\nexport function findConflicts(\n tools: readonly Tool<unknown>[],\n): Conflict[] {\n const entries = buildRouteRegistry(tools);\n const groups = new Map<string, RegistryEntry[]>();\n for (const entry of entries) {\n const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ''}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n const conflicts: Conflict[] = [];\n for (const bucket of groups.values()) {\n if (bucket.length < 2) continue;\n const first = bucket[0];\n conflicts.push({\n phase: first.phase,\n gesture: first.gesture,\n arg: first.arg,\n target: first.target,\n modifiers: first.modifiers,\n toolIds: bucket.map((e) => e.toolId),\n });\n }\n return conflicts;\n}\n\n/** Bucket key for an entry's target slot. Predicate targets all render as\n * the same grammar token, so they're keyed by the predicate's identity —\n * two different functions are two different targets. */\nfunction targetKey(entry: RegistryEntry): string {\n if (entry.target !== PREDICATE_TARGET) return entry.target ?? '';\n const spec = entry.spec as { target?: unknown };\n const pred = spec.target;\n if (typeof pred !== 'object' || pred === null) return PREDICATE_TARGET;\n return `${PREDICATE_TARGET}#${predicateId(pred)}`;\n}\n\nconst PREDICATE_IDS = new WeakMap<object, number>();\nlet nextPredicateId = 0;\n\nfunction predicateId(pred: object): number {\n let id = PREDICATE_IDS.get(pred);\n if (id === undefined) {\n id = nextPredicateId++;\n PREDICATE_IDS.set(pred, id);\n }\n return id;\n}\n\n/**\n * The tool scopes as the dispatcher sees them — which is what decides whether\n * two same-tuple bindings can actually collide.\n */\nexport interface ToolScopes {\n /** Tools eligible for the active slot, keyed by id or as a flat list. */\n registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;\n /** Always-on tools. Every one of these is live at once. */\n ambient?: readonly Tool<unknown>[];\n}\n\n/**\n * Detect the same-tuple overlaps that are *reachable* — the ones where the\n * dispatcher really does fall back on declaration order.\n *\n * `matchSorted` walks scopes in strict priority (hotkey > active > ambient)\n * and only sorts by specificity *within* a scope. So a cross-scope tie isn't\n * a tie at all: an ambient tool losing a tuple to the active tool is the\n * documented design, not an accident. Likewise two registry tools sharing a\n * tuple — `rect` and `ellipse` both binding a bare `drag` — can never both be\n * in the active slot, so they never compete.\n *\n * What's left, and what this reports:\n * - a single tool colliding with **itself** (possible since bindings became\n * an array), which is ambiguous in whichever slot it occupies;\n * - two **ambient** tools, all of which are live simultaneously and are\n * ordered only by registration;\n * - two **hotkey-capable** tools, which can stack.\n *\n * Not covered: the actions registry's `defaultBinding`s, which the dispatcher\n * also folds into ambient/hotkey scope. They're assembled somewhere else\n * entirely (`ActionsRegistry`, not `useTools`), so catching tool-vs-action\n * collisions means giving this function a second input it doesn't have yet.\n */\nexport function findScopedConflicts(scopes: ToolScopes): Conflict[] {\n const registry = Array.isArray(scopes.registry)\n ? (scopes.registry as readonly Tool<unknown>[])\n : Object.values(scopes.registry as Readonly<Record<string, Tool<unknown>>>);\n const ambient = scopes.ambient ?? [];\n\n const out: Conflict[] = [];\n const seen = new Set<string>();\n const add = (conflicts: readonly Conflict[]): void => {\n for (const c of conflicts) {\n const key = `${c.phase}|${c.gesture}|${c.arg ?? ''}|${c.target ?? ''}`\n + `|${canonicalModifiers(c.modifiers)}|${c.toolIds.join(',')}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(c);\n }\n };\n\n // Self-collisions: every tool, whichever slot it lands in.\n for (const tool of [...registry, ...ambient]) add(findConflicts([tool]));\n // Ambient tools are all live together.\n if (ambient.length > 1) add(findConflicts(ambient));\n // Hotkey-capable tools can stack on each other. `hotkey` is declared on the\n // authored `ToolDef`, not carried onto the runtime `Tool` — `Tool.def` is\n // the reflection handle for exactly this kind of read, and it's typed\n // `unknown` so consumers cast at the use site.\n const hotkeyTools = registry.filter(\n (t) => (t.def as { hotkey?: unknown } | undefined)?.hotkey !== undefined,\n );\n if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));\n\n return out;\n}\n\n/**\n * Render a conflict as one line of human-readable text.\n *\n * The tuple is printed through {@link formatRoute}, so the message names the\n * collision in the same grammar the author wrote the binding in — modulo the\n * phase slot, which prints as a bare `'&'`-channel atom because the bucket key\n * collapses channel-bearing phase specs (`sel:engaged` and `&:engaged` collide\n * with each other, and the collapse is what made them collide).\n */\nexport function formatConflict(conflict: Conflict): string {\n const route = formatRoute({\n phases: [{ channel: '&', phase: conflict.phase === 'any' ? '*' : conflict.phase }],\n gesture: conflict.gesture,\n arg: conflict.arg,\n target: conflict.target,\n modifiers: conflict.modifiers,\n });\n return `${route} — declared by ${conflict.toolIds.join(', ')}`;\n}\n\n/**\n * Detect reachable route conflicts in an assembled tool set and report each one.\n *\n * This is the wiring `findConflicts` spent its first life without: the kit\n * could detect the one class of genuine routing ambiguity it has and never\n * looked. Call it once wherever a tool set is assembled (`useTools` does),\n * behind a `process.env.NODE_ENV !== 'production'` guard.\n *\n * **Warn, never throw.** A conflict between a consumer's tool and a kit tool\n * is a design question — sometimes deliberate, since the loser can still take\n * the gesture by declining through `enabled()` — and exploding in a running\n * app is the wrong way to raise it. A conflict between two *kit* tools is\n * always a bug, but the place to fail on that is the kit's own test suite\n * (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.\n *\n * @returns The conflicts found, so callers can dedupe repeat reports.\n */\nexport function reportRouteConflicts(\n scopes: ToolScopes,\n warn: (message: string) => void = (m) => console.warn(m),\n): Conflict[] {\n const conflicts = findScopedConflicts(scopes);\n for (const c of conflicts) {\n warn(\n `[weasel] route conflict: two bindings declare the same (phase, gesture, arg, target, modifiers) tuple `\n + `in the same scope, so declaration order alone decides which one fires.\\n ${formatConflict(c)}`,\n );\n }\n return conflicts;\n}\n"]}
|