@weasel-js/labkit 0.1.0 → 0.7.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/dist/_dts/{fitViewToBounds-rWrBI4a6.d.ts → DrawCommand-CEvS5AdV.d.ts} +124 -23
- package/dist/{chunk-B5ZUYRKN.js → chunk-GSSBVWH7.js} +5178 -5352
- package/dist/chunk-GSSBVWH7.js.map +1 -0
- package/dist/chunk-H4NC6XM3.js +6037 -0
- package/dist/chunk-H4NC6XM3.js.map +1 -0
- package/dist/{chunk-SD3HDXR7.js → chunk-LSO7MDGZ.js} +8 -8
- package/dist/chunk-LSO7MDGZ.js.map +1 -0
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/opentype-BYRB6NOX.js +15965 -0
- package/dist/opentype-BYRB6NOX.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +1 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +2220 -744
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/ui/layers/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-6FO7G5BB.js +0 -6095
- package/dist/chunk-6FO7G5BB.js.map +0 -1
- package/dist/chunk-B5ZUYRKN.js.map +0 -1
- package/dist/chunk-SD3HDXR7.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
|
-
import { ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, PointerEvent as PointerEvent$1, KeyboardEvent
|
|
4
|
-
import { b as NodeId,
|
|
3
|
+
import { ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, MutableRefObject, PointerEvent as PointerEvent$1, KeyboardEvent, RefCallback } from 'react';
|
|
4
|
+
import { O as Op, V as View, D as DrawCommand, b as NodeId, c as Path, S as Scene, H as History, d as PoseComposition, B as Bounds } from '../_dts/DrawCommand-CEvS5AdV.js';
|
|
5
5
|
import { TextFieldProps, ValidationResult, CheckboxProps as CheckboxProps$1, SwitchProps as SwitchProps$1, TabProps as TabProps$1, TabListProps as TabListProps$1, TabPanelProps as TabPanelProps$1, TabsProps as TabsProps$1, RadioProps as RadioProps$1, RadioGroupProps as RadioGroupProps$1, NumberFieldProps as NumberFieldProps$1, SelectProps as SelectProps$1, ListBoxItemProps, ComboBoxProps as ComboBoxProps$1, SliderProps as SliderProps$1, ModalOverlayProps, DialogProps as DialogProps$1 } from 'react-aria-components';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1031,6 +1031,23 @@ type ToggleBarProps<V extends string | number = string> = (CommonProps & {
|
|
|
1031
1031
|
mode: 'multiple';
|
|
1032
1032
|
items: readonly ToggleBarItem<V>[];
|
|
1033
1033
|
value: readonly V[];
|
|
1034
|
+
/**
|
|
1035
|
+
* Values that are neither on nor off — the sources this bar
|
|
1036
|
+
* aggregates disagree (a text range that is bold in part of it, a
|
|
1037
|
+
* multi-selection whose nodes differ). Rendered `aria-pressed="mixed"`,
|
|
1038
|
+
* the ARIA tri-state a toggle button actually has, rather than
|
|
1039
|
+
* `SelectionPanel`'s reduced-opacity-plus-`title` workaround for
|
|
1040
|
+
* `Switch`, which has no indeterminate state to render.
|
|
1041
|
+
*
|
|
1042
|
+
* `value` wins where the two lists overlap, so a caller that can't
|
|
1043
|
+
* cheaply keep them disjoint doesn't get an ambiguous segment.
|
|
1044
|
+
*
|
|
1045
|
+
* Clicking a mixed segment turns it fully **on**, matching the
|
|
1046
|
+
* everywhere-else convention (and `toggleFlagInRange`'s rule for a
|
|
1047
|
+
* partially-styled text range) that a mixed toggle resolves toward
|
|
1048
|
+
* the affirmative rather than clearing.
|
|
1049
|
+
*/
|
|
1050
|
+
mixedValues?: readonly V[];
|
|
1034
1051
|
onChange: (next: V[]) => void;
|
|
1035
1052
|
});
|
|
1036
1053
|
declare function ToggleBar<V extends string | number = string>(props: ToggleBarProps<V>): ReactElement;
|
|
@@ -1107,422 +1124,189 @@ interface PowerlineProps {
|
|
|
1107
1124
|
}
|
|
1108
1125
|
declare function Powerline({ segments, startCap, size, variant, depth, gap, className, ...rest }: PowerlineProps): react_jsx_runtime.JSX.Element;
|
|
1109
1126
|
|
|
1110
|
-
|
|
1111
|
-
interface SelectionApi {
|
|
1112
|
-
/** Current selection. Re-renders trigger when this reference changes. */
|
|
1113
|
-
current: readonly NodeId[];
|
|
1114
|
-
/** Imperative read for use inside event callbacks (avoids stale closures). */
|
|
1115
|
-
get(): NodeId[];
|
|
1116
|
-
/** Replace selection. */
|
|
1117
|
-
set(ids: NodeId[]): void;
|
|
1118
|
-
/** Add id (multi-mode appends; single-mode replaces). */
|
|
1119
|
-
add(id: NodeId): void;
|
|
1120
|
-
/** Remove id from selection. */
|
|
1121
|
-
remove(id: NodeId): void;
|
|
1122
|
-
/** Toggle id in/out of selection. */
|
|
1123
|
-
toggle(id: NodeId): void;
|
|
1124
|
-
/** Clear selection. */
|
|
1125
|
-
clear(): void;
|
|
1126
|
-
/** True if id is selected. */
|
|
1127
|
-
contains(id: NodeId): boolean;
|
|
1128
|
-
/**
|
|
1129
|
-
* Apply a click to the selection per the configured mode/extend key.
|
|
1130
|
-
* - `single`: replaces selection with `[id]`, regardless of modifiers.
|
|
1131
|
-
* - `multi`: with the extend key held, toggles `id` in/out of the selection;
|
|
1132
|
-
* otherwise replaces with `[id]`.
|
|
1133
|
-
*/
|
|
1134
|
-
applyClick(id: NodeId, modifiers: {
|
|
1135
|
-
shift: boolean;
|
|
1136
|
-
meta: boolean;
|
|
1137
|
-
ctrl: boolean;
|
|
1138
|
-
}): void;
|
|
1139
|
-
/** Pre-built methods for spreading into an adapter that needs them. */
|
|
1140
|
-
adapterMethods: {
|
|
1141
|
-
getSelection: () => NodeId[];
|
|
1142
|
-
setSelection: (ids: NodeId[]) => void;
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
type HandleKind = 'corner' | 'rotation' | 'anchor';
|
|
1147
|
-
type HitShape = {
|
|
1148
|
-
kind: 'rect';
|
|
1127
|
+
type ContainerBounds = {
|
|
1149
1128
|
x: number;
|
|
1150
1129
|
y: number;
|
|
1151
1130
|
width: number;
|
|
1152
1131
|
height: number;
|
|
1153
|
-
rotation?: number;
|
|
1154
|
-
} | {
|
|
1155
|
-
kind: 'circle';
|
|
1156
|
-
cx: number;
|
|
1157
|
-
cy: number;
|
|
1158
|
-
r: number;
|
|
1159
|
-
} | {
|
|
1160
|
-
kind: 'path';
|
|
1161
|
-
d: Path2D;
|
|
1162
1132
|
};
|
|
1163
|
-
interface
|
|
1164
|
-
|
|
1165
|
-
|
|
1133
|
+
interface LayoutChild<TPose> {
|
|
1134
|
+
id: string;
|
|
1135
|
+
pose: TPose;
|
|
1136
|
+
}
|
|
1137
|
+
interface DropTarget<TPose> {
|
|
1138
|
+
/** Where the dragged child lands if this target is picked. */
|
|
1139
|
+
pose: TPose;
|
|
1140
|
+
/** Reference point for distance metrics (snap algorithms). */
|
|
1141
|
+
origin: {
|
|
1166
1142
|
x: number;
|
|
1167
1143
|
y: number;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1144
|
+
};
|
|
1145
|
+
/** Optional axis-aligned region (world units) used by region-aware snaps
|
|
1146
|
+
* (e.g. `containedThenNearest`). When present, a pointer inside this rect
|
|
1147
|
+
* is treated as a containment hit on this target. Strategies that emit
|
|
1148
|
+
* region-shaped targets (gutters, drop-zones) should populate this.
|
|
1149
|
+
* Strategies whose targets are point-like (free-form, snap-point) can omit
|
|
1150
|
+
* it and rely on `origin`-distance snaps. */
|
|
1151
|
+
hitBounds?: {
|
|
1170
1152
|
x: number;
|
|
1171
1153
|
y: number;
|
|
1172
1154
|
width: number;
|
|
1173
1155
|
height: number;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1156
|
+
};
|
|
1157
|
+
/** Strategy-private metadata (e.g. cell coords for tile-grid). */
|
|
1158
|
+
meta?: unknown;
|
|
1159
|
+
}
|
|
1160
|
+
interface LayoutSnap<TPose> {
|
|
1161
|
+
pickTarget(targets: DropTarget<TPose>[], pointer: {
|
|
1176
1162
|
x: number;
|
|
1177
1163
|
y: number;
|
|
1178
|
-
}):
|
|
1179
|
-
|
|
1164
|
+
}): DropTarget<TPose> | null;
|
|
1165
|
+
}
|
|
1166
|
+
interface LayoutContainer {
|
|
1167
|
+
id: string;
|
|
1168
|
+
bounds: ContainerBounds;
|
|
1169
|
+
}
|
|
1170
|
+
interface LayoutDragged<TPose> {
|
|
1171
|
+
id: string;
|
|
1172
|
+
/** The pose the dragged child currently has (pre-drop). */
|
|
1173
|
+
originPose: TPose;
|
|
1174
|
+
/** The pose the gesture proposes (pointer-driven, pre-snap). */
|
|
1175
|
+
pose: TPose;
|
|
1176
|
+
sourceContainerId: string | null;
|
|
1177
|
+
}
|
|
1178
|
+
interface LayoutStrategy<TPose> {
|
|
1179
|
+
childPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>): Map<string, TPose>;
|
|
1180
|
+
getDropTargets(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>): DropTarget<TPose>[];
|
|
1181
|
+
reflowPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Map<string, TPose>;
|
|
1182
|
+
commitDrop(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Op[];
|
|
1183
|
+
snap: LayoutSnap<TPose>;
|
|
1184
|
+
/** Optional: predicate for whether a world-space point is inside this
|
|
1185
|
+
* container. When absent, callers fall back to an axis-aligned bounding-box
|
|
1186
|
+
* test on the container's pose. Strategies whose containers aren't
|
|
1187
|
+
* rectangular (circles, irregular zones) implement this to override the
|
|
1188
|
+
* AABB default. */
|
|
1189
|
+
contains?(containerPose: TPose, point: {
|
|
1180
1190
|
x: number;
|
|
1181
1191
|
y: number;
|
|
1182
|
-
}
|
|
1183
|
-
recordLayer(id: string, label: string, space: 'world' | 'screen', index: number): void;
|
|
1184
|
-
/** Clears every non-snap array. Called at the start of each Canvas render. */
|
|
1185
|
-
beginFrame(): void;
|
|
1186
|
-
/** Clears the snap array. Called at gesture end. */
|
|
1187
|
-
clearSnap(): void;
|
|
1192
|
+
}): boolean;
|
|
1188
1193
|
}
|
|
1189
1194
|
|
|
1190
1195
|
/**
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
* - which gestures carry an argument and what values are legal
|
|
1194
|
-
* - the default arg value (used when none is specified in a route)
|
|
1196
|
+
* Opaque clipboard payload. `items` is `unknown[]` so each app's clipboard
|
|
1197
|
+
* adapter stores whatever shape it wants; the kit never inspects entries.
|
|
1195
1198
|
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1199
|
+
* The adapter is responsible for both producing snapshots
|
|
1200
|
+
* (`snapshotSelection`) and consuming them (`commitPaste`). Type safety lives
|
|
1201
|
+
* at that boundary, not in the kit.
|
|
1198
1202
|
*/
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
* order: mod → shift → alt (matches formatShortcut). */
|
|
1203
|
-
type ModifierCombo = 'default' | 'mod' | 'shift' | 'alt' | 'mod+shift' | 'mod+alt' | 'shift+alt' | 'mod+shift+alt';
|
|
1204
|
-
|
|
1205
|
-
/** Phase of a gesture lifecycle. `initial` means the tool is idle
|
|
1206
|
-
* (scratch null); `engaged` means a gesture is in progress (scratch
|
|
1207
|
-
* populated). The route-grammar's `[phase]` slot draws from this set. */
|
|
1208
|
-
type RoutePhase$1 = 'initial' | 'engaged';
|
|
1209
|
-
|
|
1203
|
+
interface ClipboardSnapshot {
|
|
1204
|
+
items: unknown[];
|
|
1205
|
+
}
|
|
1210
1206
|
/**
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
* route = phaseSlot WS gesture WS argSlot? WS targetSlot? WS modSlot?
|
|
1214
|
-
* phaseSlot = '[' phaseList ']'
|
|
1215
|
-
* phaseList = phaseAtom (WS ',' WS phaseAtom)*
|
|
1216
|
-
* phaseAtom = (channel ':')? phaseValue -- bare phaseValue ≡ '&:phaseValue'
|
|
1217
|
-
* channel = '&' | '*' | toolId -- '&' = the binding's own tool
|
|
1218
|
-
* phaseValue = 'initial' | 'engaged' | '*'
|
|
1219
|
-
* argSlot = '(' argValue ')' -- whitespace inside parens is significant
|
|
1220
|
-
* targetSlot = '=>' WS targetValue -- omitted slot defaults to '*' for hasTarget
|
|
1221
|
-
* modSlot = modAtom (WS modAtom)*
|
|
1222
|
-
* modAtom = sigil modName
|
|
1223
|
-
* sigil = '+' | '?' -- ! @ # $ % ^ & * reserved as id-prefix
|
|
1224
|
-
* modName = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
|
|
1225
|
-
*
|
|
1226
|
-
* Shorthand: a bare phaseValue (no `:`) implies channel `&` ("this tool's
|
|
1227
|
-
* own phase"). `[engaged]` ≡ `[&:engaged]`; `[*]` ≡ `[&:*]`. The truly-loose
|
|
1228
|
-
* form (any channel, any phase) is `[*:*]`.
|
|
1207
|
+
* SnapTarget — where a dragged node would re-parent to if released.
|
|
1229
1208
|
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* [rect:engaged] wheel -- when rect tool is mid-gesture
|
|
1234
|
-
* [*:engaged] keyDown(Delete) -- when any tool is mid-gesture
|
|
1235
|
-
* [initial,engaged] contextMenu => empty -- either self phase
|
|
1236
|
-
* [*] click => empty -- self, any phase
|
|
1209
|
+
* `slotPose` is the pose (in world coordinates) the node should snap to
|
|
1210
|
+
* within the target. `metadata` is an opaque pass-through for app-specific
|
|
1211
|
+
* snap details (slot index, visual hint, etc.).
|
|
1237
1212
|
*/
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
/** One element of a phase list: a (channel, phase) pair. The default
|
|
1243
|
-
* channel (omitted in the shorthand) is `'&'`. `phase: '*'` means
|
|
1244
|
-
* "any phase of the given channel". */
|
|
1245
|
-
interface PhaseAtom {
|
|
1246
|
-
channel: ChannelRef;
|
|
1247
|
-
phase: RoutePhase$1 | '*';
|
|
1213
|
+
interface SnapTarget<TPose = unknown> {
|
|
1214
|
+
parentId: string;
|
|
1215
|
+
slotPose: TPose;
|
|
1216
|
+
metadata?: unknown;
|
|
1248
1217
|
}
|
|
1249
|
-
|
|
1250
1218
|
/**
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
* Used by `Action.defaultBinding` (the action's preferred gesture) and by
|
|
1254
|
-
* `GestureBinding.spec` (a tool's binding table entry). The dispatcher matches
|
|
1255
|
-
* incoming input events against registered specs to determine which action to
|
|
1256
|
-
* invoke.
|
|
1257
|
-
*
|
|
1258
|
-
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md` § "Types".
|
|
1219
|
+
* Narrow adapter for `useMove`. Includes optional snap-target
|
|
1220
|
+
* lookup; apps without container-snapping leave it out.
|
|
1259
1221
|
*/
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1222
|
+
interface MoveAdapter<TNode extends {
|
|
1223
|
+
id: string;
|
|
1224
|
+
}, TPose> {
|
|
1225
|
+
getNode(id: string): TNode | undefined;
|
|
1226
|
+
/** Enumerate all nodes. `<Canvas>` derives a default rect-pose `pickEvery`
|
|
1227
|
+
* and the scene-iteration loop from this. */
|
|
1228
|
+
getNodes(): TNode[];
|
|
1229
|
+
getPose(id: string): TPose;
|
|
1230
|
+
/** Optional. Required only by hierarchy-aware paths: layout-pass drop
|
|
1231
|
+
* targeting (`getLayout` present), nested-hit collapse
|
|
1232
|
+
* (`pickTopMostHit`), and group-pose composition. Flat scenes may omit. */
|
|
1233
|
+
getParent?(id: string): string | null;
|
|
1234
|
+
setPose(id: string, pose: TPose): void;
|
|
1235
|
+
/** Optional. Used only by reparent ops (e.g. drag-into-container drops via
|
|
1236
|
+
* layout strategies). Flat scenes that never reparent may omit. */
|
|
1237
|
+
setParent?(id: string, parentId: string | null): void;
|
|
1238
|
+
/** Optional: see SceneAdapter.applyOps. */
|
|
1239
|
+
applyOps?(ops: Op[], label: string): void;
|
|
1240
|
+
findSnapTarget?(draggedId: string, worldX: number, worldY: number): SnapTarget<TPose> | null;
|
|
1241
|
+
/** Optional: direct children of `id`. When present (alongside the
|
|
1242
|
+
* `cascadeWorldPose` option on `useMove`), dragging a node
|
|
1243
|
+
* auto-cascades its descendants in the live overlay so structurally-
|
|
1244
|
+
* grouped children visually follow the parent during the drag. No
|
|
1245
|
+
* additional ops are generated — children's local poses don't change
|
|
1246
|
+
* when the parent's local pose moves. */
|
|
1247
|
+
getChildren?(id: string): string[];
|
|
1248
|
+
/** Optional: layout strategy attached to a container, or null if the
|
|
1249
|
+
* container uses absolute positioning (default behavior). When present,
|
|
1250
|
+
* `useMove` uses the strategy to compute drop targets, sibling reflow,
|
|
1251
|
+
* and the commit op batch when a drag ends over the container. */
|
|
1252
|
+
getLayout?(containerId: string): LayoutStrategy<TPose> | null;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Narrow adapter for `useInsert` and `useClipboardOps`. The kit knows
|
|
1256
|
+
* nothing about what tool is active or what shape to construct; it asks the
|
|
1257
|
+
* adapter to produce node(s) given gesture or paste inputs.
|
|
1268
1258
|
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1259
|
+
* Drag-rectangle path: `commitInsert(bounds)` returns one new node or null.
|
|
1260
|
+
* Clipboard paste path: `commitPaste(clipboard, offset)` returns the array of
|
|
1261
|
+
* newly-materialized nodes (in order). Both empty array and array of
|
|
1262
|
+
* length N are valid; the kit wraps each entry in an `InsertOp`.
|
|
1271
1263
|
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
* a real consumer needs it.
|
|
1264
|
+
* `snapshotSelection(ids)` builds the payload that paste later consumes.
|
|
1265
|
+
* `getPasteOffset` is optional; the kit defaults to a fixed grid-cell offset
|
|
1266
|
+
* supplied by the consumer (passed to `useClipboardOps` options if needed; see
|
|
1267
|
+
* the hook for resolution order).
|
|
1277
1268
|
*/
|
|
1278
|
-
|
|
1279
|
-
alt: boolean | 'optional';
|
|
1280
|
-
ctrl: boolean | 'optional';
|
|
1281
|
-
meta: boolean | 'optional';
|
|
1282
|
-
mod: boolean | 'optional';
|
|
1283
|
-
shift: boolean | 'optional';
|
|
1284
|
-
}>;
|
|
1285
|
-
/** Target selector for click and drag gesture specs. String forms are sugar
|
|
1286
|
-
* for the kit-owned object-kind registry (TODO.md Tier 1 follow-up); until
|
|
1287
|
-
* that ships, consumers can pass `{ kindOf: predicate }` to classify hits
|
|
1288
|
-
* themselves. */
|
|
1289
|
-
type TargetSpec = 'empty' | 'selected-body' | 'unselected-body' | `kind:${string}` | `kind:${string}:selected` | `affordance:${string}` | {
|
|
1290
|
-
/** Predicate. `hit` is the raw target (affordance for drag,
|
|
1291
|
-
* `e.target` otherwise); `bodyTarget` is the optional body-class
|
|
1292
|
-
* string ('empty' | 'selected-body' | 'unselected-body') when
|
|
1293
|
-
* `classifyTarget` is wired. Predicates that only need one of the
|
|
1294
|
-
* two can ignore the other. */
|
|
1295
|
-
kindOf: (hit: unknown, bodyTarget?: string) => boolean;
|
|
1296
|
-
};
|
|
1297
|
-
/** Phase qualifier on a gesture spec. Restricts when the spec matches based
|
|
1298
|
-
* on per-tool gesture-lifecycle state.
|
|
1299
|
-
*
|
|
1300
|
-
* Shorthand forms (most common case — gate on the binding's own tool):
|
|
1301
|
-
* `'engaged'` → `[{ channel: '&', phase: 'engaged' }]` // self mid-gesture
|
|
1302
|
-
* `'initial'` → `[{ channel: '&', phase: 'initial' }]` // self idle
|
|
1303
|
-
* `'*'` → `[{ channel: '&', phase: '*' }]` // either self phase
|
|
1304
|
-
*
|
|
1305
|
-
* Array form for explicit channel:phase atoms — e.g. `[{ channel: 'rect',
|
|
1306
|
-
* phase: 'engaged' }]` for "when the rect tool is mid-gesture, regardless of
|
|
1307
|
-
* which scope I'm in." See the v3 route grammar in
|
|
1308
|
-
* `@weasel-js/gestures/grammar` for the full lattice.
|
|
1309
|
-
*
|
|
1310
|
-
* When omitted, matches in any phase (preserves pre-phase behavior). */
|
|
1311
|
-
type PhaseSpec = 'initial' | 'engaged' | '*' | readonly PhaseAtom[];
|
|
1312
|
-
/** Single-keystroke gesture (keydown). */
|
|
1313
|
-
interface KeySpec {
|
|
1314
|
-
kind: 'key';
|
|
1315
|
-
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
1316
|
-
key: string | string[];
|
|
1317
|
-
mods?: ModSpec;
|
|
1318
|
-
phase?: PhaseSpec;
|
|
1319
|
-
}
|
|
1320
|
-
/** Key-held gesture (keydown opens, keyup closes). Drives "hold space for
|
|
1321
|
-
* hand tool"-style interactions. */
|
|
1322
|
-
interface KeyHeldSpec {
|
|
1323
|
-
kind: 'key-held';
|
|
1324
|
-
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
1325
|
-
key: string | string[];
|
|
1326
|
-
mods?: ModSpec;
|
|
1327
|
-
phase?: PhaseSpec;
|
|
1328
|
-
}
|
|
1329
|
-
/** Wheel-event gesture. `direction` filters by deltaY sign; default `'*'`.
|
|
1330
|
-
* - `'up'` → matches only deltaY < 0
|
|
1331
|
-
* - `'down'` → matches only deltaY > 0
|
|
1332
|
-
* - `'*'` → matches either sign (default; universal-wildcard convention) */
|
|
1333
|
-
interface WheelSpec {
|
|
1334
|
-
kind: 'wheel';
|
|
1335
|
-
direction?: 'up' | 'down' | '*';
|
|
1336
|
-
mods?: ModSpec;
|
|
1337
|
-
phase?: PhaseSpec;
|
|
1338
|
-
}
|
|
1339
|
-
/** Click gesture (pointerdown + pointerup without movement past the
|
|
1340
|
-
* threshold). */
|
|
1341
|
-
interface ClickSpec {
|
|
1342
|
-
kind: 'click';
|
|
1343
|
-
target?: TargetSpec;
|
|
1344
|
-
mods?: ModSpec;
|
|
1345
|
-
phase?: PhaseSpec;
|
|
1346
|
-
}
|
|
1347
|
-
/** Double-click: two `click` events within ~500ms and ~5px of each other.
|
|
1348
|
-
* Synthesized by `useGestureDispatcher`; emitted AFTER the second
|
|
1349
|
-
* `click`. Bindings that want to handle a double-click should declare
|
|
1350
|
-
* this kind rather than chasing two `click` events. */
|
|
1351
|
-
interface DoubleClickSpec {
|
|
1352
|
-
kind: 'doubleClick';
|
|
1353
|
-
target?: TargetSpec;
|
|
1354
|
-
mods?: ModSpec;
|
|
1355
|
-
phase?: PhaseSpec;
|
|
1356
|
-
}
|
|
1357
|
-
/** Right-click (contextmenu) gesture. The dispatcher calls
|
|
1358
|
-
* `preventDefault()` on the underlying DOM event so the native menu
|
|
1359
|
-
* doesn't appear — tools/actions fully own the right-click UX. */
|
|
1360
|
-
interface ContextMenuSpec {
|
|
1361
|
-
kind: 'contextMenu';
|
|
1362
|
-
target?: TargetSpec;
|
|
1363
|
-
mods?: ModSpec;
|
|
1364
|
-
phase?: PhaseSpec;
|
|
1365
|
-
}
|
|
1366
|
-
/** Drag gesture (pointerdown + pointermove past the threshold). */
|
|
1367
|
-
interface DragSpec {
|
|
1368
|
-
kind: 'drag';
|
|
1369
|
-
target?: TargetSpec;
|
|
1370
|
-
mods?: ModSpec;
|
|
1371
|
-
phase?: PhaseSpec;
|
|
1372
|
-
}
|
|
1373
|
-
/** Multi-touch gesture. `fingers` is the required touch count. */
|
|
1374
|
-
interface MultiTouchSpec {
|
|
1375
|
-
kind: 'multiTouch';
|
|
1376
|
-
fingers: number;
|
|
1377
|
-
mods?: ModSpec;
|
|
1378
|
-
phase?: PhaseSpec;
|
|
1379
|
-
}
|
|
1380
|
-
/** Multi-touch tap gesture — fires when N fingers touch down then release
|
|
1381
|
-
* together without movement past the tap threshold. Synthesized by the
|
|
1382
|
-
* dispatcher from the underlying multitouch tracking. */
|
|
1383
|
-
interface MultiTouchTapSpec {
|
|
1384
|
-
kind: 'multiTouchTap';
|
|
1385
|
-
fingers: number;
|
|
1386
|
-
mods?: ModSpec;
|
|
1387
|
-
phase?: PhaseSpec;
|
|
1388
|
-
}
|
|
1389
|
-
/** OS drag-and-drop of external content onto the canvas. `types` filters by
|
|
1390
|
-
* MIME glob (`'image/*'`, `'text/plain'`); the spec matches when ANY item's
|
|
1391
|
-
* MIME matches ANY glob. Omitted or empty = matches any drop. */
|
|
1392
|
-
interface DropSpec {
|
|
1393
|
-
kind: 'drop';
|
|
1394
|
-
types?: string[];
|
|
1395
|
-
mods?: ModSpec;
|
|
1396
|
-
phase?: PhaseSpec;
|
|
1397
|
-
}
|
|
1398
|
-
/** System-clipboard paste of external content. Same `types` semantics as
|
|
1399
|
-
* {@link DropSpec} — omitted or empty = matches any paste. */
|
|
1400
|
-
interface PasteSpec {
|
|
1401
|
-
kind: 'paste';
|
|
1402
|
-
types?: string[];
|
|
1403
|
-
mods?: ModSpec;
|
|
1404
|
-
phase?: PhaseSpec;
|
|
1405
|
-
}
|
|
1406
|
-
/** The full union of supported gesture spec kinds. New invocation forms
|
|
1407
|
-
* (long-press, two-stage, modal-dialog) extend this union without touching
|
|
1408
|
-
* the `Action` type. */
|
|
1409
|
-
type GestureSpec = KeySpec | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
|
|
1410
|
-
|
|
1411
|
-
type ContainerBounds = {
|
|
1412
|
-
x: number;
|
|
1413
|
-
y: number;
|
|
1414
|
-
width: number;
|
|
1415
|
-
height: number;
|
|
1416
|
-
};
|
|
1417
|
-
interface LayoutChild<TPose> {
|
|
1269
|
+
interface InsertAdapter<TNode extends {
|
|
1418
1270
|
id: string;
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
x: number;
|
|
1427
|
-
y: number;
|
|
1428
|
-
};
|
|
1429
|
-
/** Optional axis-aligned region (world units) used by region-aware snaps
|
|
1430
|
-
* (e.g. `containedThenNearest`). When present, a pointer inside this rect
|
|
1431
|
-
* is treated as a containment hit on this target. Strategies that emit
|
|
1432
|
-
* region-shaped targets (gutters, drop-zones) should populate this.
|
|
1433
|
-
* Strategies whose targets are point-like (free-form, snap-point) can omit
|
|
1434
|
-
* it and rely on `origin`-distance snaps. */
|
|
1435
|
-
hitBounds?: {
|
|
1271
|
+
}> {
|
|
1272
|
+
/** Materialize a new node from drag-rect bounds (drag-to-insert tools).
|
|
1273
|
+
* Optional — hooks that don't drive insertion (e.g. `useClone`,
|
|
1274
|
+
* read-only clipboard) won't call it, and `sceneToAdapter` only fills
|
|
1275
|
+
* it in when `options.commitInsert` is supplied. Hooks that *do* call
|
|
1276
|
+
* it (insert tools) document the requirement at their own surface. */
|
|
1277
|
+
commitInsert?(bounds: {
|
|
1436
1278
|
x: number;
|
|
1437
1279
|
y: number;
|
|
1438
1280
|
width: number;
|
|
1439
1281
|
height: number;
|
|
1282
|
+
}): TNode | null;
|
|
1283
|
+
/** Materialize one or more new nodes from a clipboard snapshot. Optional
|
|
1284
|
+
* — required by `useClipboard.paste`, ignored by other consumers. */
|
|
1285
|
+
commitPaste?(clipboard: ClipboardSnapshot, offset: {
|
|
1286
|
+
dx: number;
|
|
1287
|
+
dy: number;
|
|
1288
|
+
}, ctx?: {
|
|
1289
|
+
dropPoint?: {
|
|
1290
|
+
worldX: number;
|
|
1291
|
+
worldY: number;
|
|
1292
|
+
};
|
|
1293
|
+
}): TNode[];
|
|
1294
|
+
/** Snapshot the current selection into the clipboard payload shape.
|
|
1295
|
+
* Optional — required by `useClipboard.copy` / `cut` and `useClone`'s
|
|
1296
|
+
* ghost capture, ignored by other consumers. */
|
|
1297
|
+
snapshotSelection?(ids: string[]): ClipboardSnapshot;
|
|
1298
|
+
getPasteOffset?(clipboard: ClipboardSnapshot): {
|
|
1299
|
+
dx: number;
|
|
1300
|
+
dy: number;
|
|
1440
1301
|
};
|
|
1441
|
-
/**
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
pickTarget(targets: DropTarget<TPose>[], pointer: {
|
|
1446
|
-
x: number;
|
|
1447
|
-
y: number;
|
|
1448
|
-
}): DropTarget<TPose> | null;
|
|
1449
|
-
}
|
|
1450
|
-
interface LayoutContainer {
|
|
1451
|
-
id: string;
|
|
1452
|
-
bounds: ContainerBounds;
|
|
1453
|
-
}
|
|
1454
|
-
interface LayoutDragged<TPose> {
|
|
1455
|
-
id: string;
|
|
1456
|
-
/** The pose the dragged child currently has (pre-drop). */
|
|
1457
|
-
originPose: TPose;
|
|
1458
|
-
/** The pose the gesture proposes (pointer-driven, pre-snap). */
|
|
1459
|
-
pose: TPose;
|
|
1460
|
-
sourceContainerId: string | null;
|
|
1461
|
-
}
|
|
1462
|
-
interface LayoutStrategy<TPose> {
|
|
1463
|
-
childPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>): Map<string, TPose>;
|
|
1464
|
-
getDropTargets(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>): DropTarget<TPose>[];
|
|
1465
|
-
reflowPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Map<string, TPose>;
|
|
1466
|
-
commitDrop(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Op[];
|
|
1467
|
-
snap: LayoutSnap<TPose>;
|
|
1468
|
-
/** Optional: predicate for whether a world-space point is inside this
|
|
1469
|
-
* container. When absent, callers fall back to an axis-aligned bounding-box
|
|
1470
|
-
* test on the container's pose. Strategies whose containers aren't
|
|
1471
|
-
* rectangular (circles, irregular zones) implement this to override the
|
|
1472
|
-
* AABB default. */
|
|
1473
|
-
contains?(containerPose: TPose, point: {
|
|
1474
|
-
x: number;
|
|
1475
|
-
y: number;
|
|
1476
|
-
}): boolean;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
/**
|
|
1480
|
-
* SnapTarget — where a dragged node would re-parent to if released.
|
|
1481
|
-
*
|
|
1482
|
-
* `slotPose` is the pose (in world coordinates) the node should snap to
|
|
1483
|
-
* within the target. `metadata` is an opaque pass-through for app-specific
|
|
1484
|
-
* snap details (slot index, visual hint, etc.).
|
|
1485
|
-
*/
|
|
1486
|
-
interface SnapTarget<TPose = unknown> {
|
|
1487
|
-
parentId: string;
|
|
1488
|
-
slotPose: TPose;
|
|
1489
|
-
metadata?: unknown;
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* Narrow adapter for `useMove`. Includes optional snap-target
|
|
1493
|
-
* lookup; apps without container-snapping leave it out.
|
|
1494
|
-
*/
|
|
1495
|
-
interface MoveAdapter<TNode extends {
|
|
1496
|
-
id: string;
|
|
1497
|
-
}, TPose> {
|
|
1498
|
-
getNode(id: string): TNode | undefined;
|
|
1499
|
-
/** Enumerate all nodes. `<Canvas>` derives a default rect-pose `pickEvery`
|
|
1500
|
-
* and the scene-iteration loop from this. */
|
|
1501
|
-
getNodes(): TNode[];
|
|
1502
|
-
getPose(id: string): TPose;
|
|
1503
|
-
/** Optional. Required only by hierarchy-aware paths: layout-pass drop
|
|
1504
|
-
* targeting (`getLayout` present), nested-hit collapse
|
|
1505
|
-
* (`pickTopMostHit`), and group-pose composition. Flat scenes may omit. */
|
|
1506
|
-
getParent?(id: string): string | null;
|
|
1507
|
-
setPose(id: string, pose: TPose): void;
|
|
1508
|
-
/** Optional. Used only by reparent ops (e.g. drag-into-container drops via
|
|
1509
|
-
* layout strategies). Flat scenes that never reparent may omit. */
|
|
1510
|
-
setParent?(id: string, parentId: string | null): void;
|
|
1302
|
+
/** Mutator wired by `insertNode`-using ops (kit-side InsertOp). */
|
|
1303
|
+
insertNode(node: TNode): void;
|
|
1304
|
+
/** Mutator wired by `setSelection` ops batched alongside paste. */
|
|
1305
|
+
setSelection(ids: string[]): void;
|
|
1511
1306
|
/** Optional: see SceneAdapter.applyOps. */
|
|
1512
1307
|
applyOps?(ops: Op[], label: string): void;
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
* `cascadeWorldPose` option on `useMove`), dragging a node
|
|
1516
|
-
* auto-cascades its descendants in the live overlay so structurally-
|
|
1517
|
-
* grouped children visually follow the parent during the drag. No
|
|
1518
|
-
* additional ops are generated — children's local poses don't change
|
|
1519
|
-
* when the parent's local pose moves. */
|
|
1520
|
-
getChildren?(id: string): string[];
|
|
1521
|
-
/** Optional: layout strategy attached to a container, or null if the
|
|
1522
|
-
* container uses absolute positioning (default behavior). When present,
|
|
1523
|
-
* `useMove` uses the strategy to compute drop targets, sibling reflow,
|
|
1524
|
-
* and the commit op batch when a drag ends over the container. */
|
|
1525
|
-
getLayout?(containerId: string): LayoutStrategy<TPose> | null;
|
|
1308
|
+
/** Returns the current selection. Used by clone behaviors. */
|
|
1309
|
+
getSelection(): string[];
|
|
1526
1310
|
}
|
|
1527
1311
|
|
|
1528
1312
|
/** Snapshot of modifier-key state at gesture dispatch. */
|
|
@@ -1581,25 +1365,1836 @@ interface ActionBehavior<TPose, TProposed, TMoveResult> {
|
|
|
1581
1365
|
onMove?(ctx: GestureContext<TPose>, proposed: TProposed): TMoveResult | void;
|
|
1582
1366
|
onEnd?(ctx: GestureContext<TPose>): Op[] | null | void;
|
|
1583
1367
|
}
|
|
1368
|
+
/** Which corner/edge of the rect stays fixed during a resize. */
|
|
1369
|
+
type ResizeAnchor = {
|
|
1370
|
+
x: 'min' | 'max' | 'free';
|
|
1371
|
+
y: 'min' | 'max' | 'free';
|
|
1372
|
+
};
|
|
1373
|
+
/** Minimum rect-shaped pose required by the resize machinery. */
|
|
1374
|
+
interface ResizePose {
|
|
1375
|
+
x: number;
|
|
1376
|
+
y: number;
|
|
1377
|
+
width: number;
|
|
1378
|
+
height: number;
|
|
1379
|
+
}
|
|
1380
|
+
/** Per-frame proposed resize: pose plus the anchor pinning the opposite corner. */
|
|
1381
|
+
interface ResizeProposed<TPose extends ResizePose> {
|
|
1382
|
+
pose: TPose;
|
|
1383
|
+
anchor: ResizeAnchor;
|
|
1384
|
+
}
|
|
1385
|
+
/** Per-frame result a `BoundsConstraint.onMove` can return to override the proposed pose. */
|
|
1386
|
+
interface ResizeMoveResult<TPose extends ResizePose> {
|
|
1387
|
+
pose?: TPose;
|
|
1388
|
+
}
|
|
1389
|
+
/** A bounds-frame constraint plugged into `useResize` / `resizeAction`.
|
|
1390
|
+
* Reads/writes `{x,y,width,height}` and can override the proposed pose
|
|
1391
|
+
* on each frame (e.g. lock-aspect, clamp-min-size, snap-to-grid). */
|
|
1392
|
+
type BoundsConstraint<TPose extends ResizePose> = ActionBehavior<TPose, ResizeProposed<TPose>, ResizeMoveResult<TPose>>;
|
|
1393
|
+
/** Frames a point-snap behavior can return for the hook to back-solve. */
|
|
1394
|
+
type PointSnapFrame = 'dragged-corner' | 'fixed-corner' | 'center' | 'origin';
|
|
1395
|
+
/** Per-frame world-space context handed to `PointSnapBehavior.onMove`.
|
|
1396
|
+
* `draggedCorner` and `fixedCorner` are `null` for edge drags
|
|
1397
|
+
* (`anchor.x === 'free'` or `anchor.y === 'free'`). `center` and
|
|
1398
|
+
* `origin` are always present. */
|
|
1399
|
+
interface PointSnapContext<TPose extends ResizePose> {
|
|
1400
|
+
draggedCorner: {
|
|
1401
|
+
worldX: number;
|
|
1402
|
+
worldY: number;
|
|
1403
|
+
} | null;
|
|
1404
|
+
fixedCorner: {
|
|
1405
|
+
worldX: number;
|
|
1406
|
+
worldY: number;
|
|
1407
|
+
} | null;
|
|
1408
|
+
center: {
|
|
1409
|
+
worldX: number;
|
|
1410
|
+
worldY: number;
|
|
1411
|
+
};
|
|
1412
|
+
origin: {
|
|
1413
|
+
worldX: number;
|
|
1414
|
+
worldY: number;
|
|
1415
|
+
};
|
|
1416
|
+
rotation: number;
|
|
1417
|
+
anchor: ResizeAnchor;
|
|
1418
|
+
proposed: TPose;
|
|
1419
|
+
modifiers: ModifierState;
|
|
1420
|
+
}
|
|
1421
|
+
/** Per-frame snap result. A behavior returns at most one. */
|
|
1422
|
+
interface PointSnapResult {
|
|
1423
|
+
frame: PointSnapFrame;
|
|
1424
|
+
worldX: number;
|
|
1425
|
+
worldY: number;
|
|
1426
|
+
}
|
|
1427
|
+
/** A point-snap behavior plugged into `useResize`'s `pointSnapBehaviors`. */
|
|
1428
|
+
interface PointSnapBehavior<TPose extends ResizePose> {
|
|
1429
|
+
id?: string;
|
|
1430
|
+
onMove(ctx: PointSnapContext<TPose>): PointSnapResult | null | undefined;
|
|
1431
|
+
}
|
|
1584
1432
|
|
|
1585
|
-
/**
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1433
|
+
/**
|
|
1434
|
+
* @experimental
|
|
1435
|
+
* Result of an affordance hit — what the region computed about itself.
|
|
1436
|
+
*
|
|
1437
|
+
* `initialScratch` is the payload: what the region already knows (which
|
|
1438
|
+
* corner, which target id) so the action that picks up the drag doesn't
|
|
1439
|
+
* re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
|
|
1440
|
+
* it into `AffordanceHit`, which flows to the matching action through
|
|
1441
|
+
* `InvocationCtx.drag.affordance`.
|
|
1442
|
+
*
|
|
1443
|
+
* This used to also carry a `drag: DragChannel` naming the handlers the
|
|
1444
|
+
* tool-routing dispatcher should wire up. Every implementation supplied a
|
|
1445
|
+
* no-op stub that claimed, because the real routing had already moved to
|
|
1446
|
+
* bindings; the field went with that dispatcher.
|
|
1447
|
+
*/
|
|
1448
|
+
interface AffordanceBinding<TScratch = unknown> {
|
|
1449
|
+
initialScratch?: TScratch;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Canvas size in CSS pixels — passed to `draw` for layers that anchor to
|
|
1454
|
+
* canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
|
|
1455
|
+
* supplies it explicitly so layers don't have to know about DPR.
|
|
1456
|
+
*/
|
|
1457
|
+
interface Dims {
|
|
1458
|
+
width: number;
|
|
1459
|
+
height: number;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* A single named render sub-layer within a canvas renderer.
|
|
1463
|
+
*
|
|
1464
|
+
* @template TData - The data object passed to each draw call.
|
|
1465
|
+
*/
|
|
1466
|
+
interface RenderLayer<TData> {
|
|
1467
|
+
/** Unique identifier used in visibility maps and ordering arrays. */
|
|
1468
|
+
id: string;
|
|
1469
|
+
/** Human-readable name for UI toggles. */
|
|
1470
|
+
label: string;
|
|
1471
|
+
/**
|
|
1472
|
+
* Emit a DrawCommand tree for the GL backend to dispatch.
|
|
1473
|
+
*
|
|
1474
|
+
* For world-space layers (the default), emit commands in WORLD COORDS —
|
|
1475
|
+
* `drawLayers` automatically wraps them in `{ kind: 'group', transform:
|
|
1476
|
+
* viewToMat3(view), ... }` before handing them to the renderer. Do NOT
|
|
1477
|
+
* apply the view transform yourself.
|
|
1478
|
+
*
|
|
1479
|
+
* For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
|
|
1480
|
+
* coords directly; `drawLayers` passes them through unchanged. If part
|
|
1481
|
+
* of a screen-space layer's output needs to track the view, wrap that
|
|
1482
|
+
* subset manually with `viewToMat3(view)`.
|
|
1483
|
+
*/
|
|
1484
|
+
draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
|
|
1485
|
+
/**
|
|
1486
|
+
* Whether the layer is shown when no explicit visibility entry exists.
|
|
1487
|
+
* Defaults to `true` when absent.
|
|
1488
|
+
*/
|
|
1489
|
+
defaultVisible?: boolean;
|
|
1490
|
+
/**
|
|
1491
|
+
* When true, the layer is always drawn regardless of the visibility map.
|
|
1492
|
+
* Useful for layers that must never be hidden (e.g. base grid).
|
|
1493
|
+
*/
|
|
1494
|
+
alwaysOn?: boolean;
|
|
1495
|
+
/**
|
|
1496
|
+
* Coordinate space the layer draws in.
|
|
1497
|
+
*
|
|
1498
|
+
* - `'world'` (default): the layer's `draw` returns world-space commands;
|
|
1499
|
+
* `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
|
|
1500
|
+
* automatically.
|
|
1501
|
+
* - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
|
|
1502
|
+
* commands; `drawLayers` passes them through unchanged. World-anchored
|
|
1503
|
+
* chrome inside a screen-space layer must call `worldToScreen` or wrap
|
|
1504
|
+
* the relevant subset with `viewToMat3(view)` manually.
|
|
1505
|
+
*/
|
|
1506
|
+
space?: 'world' | 'screen';
|
|
1507
|
+
/**
|
|
1508
|
+
* Optional hit-test for **consumer-attached** layers.
|
|
1509
|
+
*
|
|
1510
|
+
* Only layers registered through `CanvasExtensionApi.registerLayer` are
|
|
1511
|
+
* hit-tested: `hitTestExtras` walks them last-registered-first on
|
|
1512
|
+
* pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
|
|
1513
|
+
* thunk ahead of the kit's own selection chrome. First non-null result
|
|
1514
|
+
* wins; null means "I don't claim this hit, try the next layer."
|
|
1515
|
+
*
|
|
1516
|
+
* Layers that reach the draw stack some other way — a `Tool.overlay`, an
|
|
1517
|
+
* entry in the `layers` map — are painted but never hit-tested, so defining
|
|
1518
|
+
* `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
|
|
1519
|
+
* goes through `buildAffordanceAt`.)
|
|
1520
|
+
*
|
|
1521
|
+
* Coordinates are world-space. The `data` arg is the layer's
|
|
1522
|
+
* configured data slot (same as `draw`); `view` and `dims` mirror
|
|
1523
|
+
* `draw`'s arguments.
|
|
1524
|
+
*/
|
|
1525
|
+
hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
|
|
1526
|
+
/** Chrome-caps visibility predicate. When supplied, the layer must
|
|
1527
|
+
* not return a hit from any chrome element whose id reports
|
|
1528
|
+
* `false`. Absent → every element is hittable. */
|
|
1529
|
+
isVisible?: (id: string) => boolean) => AffordanceBinding | null;
|
|
1530
|
+
/**
|
|
1531
|
+
* Called on every pointermove when no gesture is currently captured.
|
|
1532
|
+
* Lets layers (e.g. HUD widgets) track hover state without participating
|
|
1533
|
+
* in the drag pipeline. Coords are world-space; the layer is responsible
|
|
1534
|
+
* for any further conversion (e.g. world→screen for screen-space layers)
|
|
1535
|
+
* and for its own throttling.
|
|
1536
|
+
*/
|
|
1537
|
+
onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
|
|
1538
|
+
/**
|
|
1539
|
+
* Called when the cursor leaves the canvas element. Lets layers clear
|
|
1540
|
+
* any hover state they're holding.
|
|
1541
|
+
*/
|
|
1542
|
+
onUncapturedLeave?: () => void;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/** The full vocabulary of capability tags shipped in the default preset.
|
|
1546
|
+
* Apps and other consumers can add their own tags; this list is what
|
|
1547
|
+
* `weasel-modes` itself uses. */
|
|
1548
|
+
declare const ALL_TAGS: readonly ["navigation", "creates-selection", "creates-paths", "creates-shapes", "creates-text", "edits-anchors", "edits-text", "transforms-selection", "samples-color", "applies-fill", "edits-page"];
|
|
1549
|
+
type CapabilityTag = (typeof ALL_TAGS)[number] | (string & {});
|
|
1550
|
+
|
|
1551
|
+
interface WorkspaceVisual {
|
|
1552
|
+
tint?: string;
|
|
1553
|
+
gradient?: 'top-down' | 'bottom-up';
|
|
1554
|
+
intensity?: number;
|
|
1555
|
+
}
|
|
1556
|
+
interface ModeDefinition {
|
|
1557
|
+
id: string;
|
|
1558
|
+
kind: 'soft' | 'strict';
|
|
1559
|
+
/** Capability tags this mode allows beyond IMPLICIT_TAGS. */
|
|
1560
|
+
allows: CapabilityTag[];
|
|
1561
|
+
/** When true, out-of-target objects dim at the renderer layer. */
|
|
1562
|
+
scoping: boolean;
|
|
1563
|
+
workspace?: WorkspaceVisual;
|
|
1564
|
+
entry?: {
|
|
1565
|
+
shortcut?: string;
|
|
1566
|
+
trigger?: 'double-click-target';
|
|
1567
|
+
};
|
|
1568
|
+
exit?: {
|
|
1569
|
+
shortcut?: string;
|
|
1570
|
+
};
|
|
1571
|
+
commit?: {
|
|
1572
|
+
shortcut?: string;
|
|
1573
|
+
};
|
|
1574
|
+
cancel?: {
|
|
1575
|
+
shortcut?: string;
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
interface ModeRegistry {
|
|
1580
|
+
current(): ModeDefinition;
|
|
1581
|
+
setMode(id: string): void;
|
|
1582
|
+
byId(id: string): ModeDefinition;
|
|
1583
|
+
getVersion(): number;
|
|
1584
|
+
subscribe(listener: () => void): () => void;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Live state read by rule evaluation. Built once per frame on the consuming
|
|
1589
|
+
* surface — chrome-caps, the affordance pipeline, the dispatcher's
|
|
1590
|
+
* eligibility filter — and discarded.
|
|
1591
|
+
*
|
|
1592
|
+
* Adding a new field is additive: existing rules don't change, new
|
|
1593
|
+
* selector atoms can read it.
|
|
1594
|
+
*/
|
|
1595
|
+
interface RuleCtx {
|
|
1596
|
+
readonly focused: boolean;
|
|
1597
|
+
readonly selection: readonly NodeId[];
|
|
1598
|
+
readonly multiActive: boolean;
|
|
1599
|
+
readonly modifiers: ModifierState;
|
|
1600
|
+
readonly action: {
|
|
1601
|
+
readonly kind: string | null;
|
|
1602
|
+
readonly id: string | null;
|
|
1603
|
+
};
|
|
1604
|
+
readonly hover: NodeId | null;
|
|
1605
|
+
readonly view: View;
|
|
1606
|
+
/** Active mode id. `'normal'` when no non-default mode is engaged. */
|
|
1607
|
+
readonly mode: string;
|
|
1608
|
+
/** Capability tags allowed by the active mode (the union of
|
|
1609
|
+
* `ModeDefinition.allows` plus implicit tags). The `capability:`
|
|
1610
|
+
* selector reads this to determine whether a tag is permitted. */
|
|
1611
|
+
readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
|
|
1612
|
+
/** Whether the current selection may be resized. `<SceneCanvas>` folds
|
|
1613
|
+
* `selectTool.resize.resizable` over the selection (true only when every
|
|
1614
|
+
* selected node is resizable). Read by the `resizable:` selector to gate
|
|
1615
|
+
* `selection.resize-handles`. Absent (legacy ctx builders) is treated as
|
|
1616
|
+
* resizable — back-compat: handles show unless a consumer opts a node out. */
|
|
1617
|
+
readonly selectionResizable?: boolean;
|
|
1618
|
+
/** Whether a path is currently in anchor-edit mode. Read by the
|
|
1619
|
+
* `editingAnchors:` selector, which gates the path-edit chrome.
|
|
1620
|
+
*
|
|
1621
|
+
* This is deliberately a fact about state, not about permission: the
|
|
1622
|
+
* anchor overlay and the anchor hit-test must agree, and the thing they
|
|
1623
|
+
* must agree on is "is there an edited path right now", which no
|
|
1624
|
+
* capability or mode id answers. A mode that allows `edits-anchors`
|
|
1625
|
+
* with nothing being edited should draw no anchors. Absent is treated
|
|
1626
|
+
* as false. */
|
|
1627
|
+
readonly editingAnchors?: boolean;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* A selector is a conjunction of key/value tests. Multiple keys at the same
|
|
1632
|
+
* level AND together. Each key maps to a selector primitive in the evaluator.
|
|
1633
|
+
*/
|
|
1634
|
+
interface Selector {
|
|
1635
|
+
selection?: {
|
|
1636
|
+
is?: number;
|
|
1637
|
+
atLeast?: number;
|
|
1638
|
+
empty?: boolean;
|
|
1639
|
+
};
|
|
1640
|
+
mode?: string | {
|
|
1641
|
+
not: string;
|
|
1642
|
+
} | {
|
|
1643
|
+
in: readonly string[];
|
|
1644
|
+
};
|
|
1645
|
+
capability?: CapabilityTag | readonly CapabilityTag[] | {
|
|
1646
|
+
in: readonly CapabilityTag[];
|
|
1647
|
+
} | {
|
|
1648
|
+
not: CapabilityTag;
|
|
1649
|
+
};
|
|
1650
|
+
gesturing?: boolean;
|
|
1651
|
+
actionIs?: string;
|
|
1652
|
+
modifierHeld?: keyof ModifierState;
|
|
1653
|
+
focused?: boolean;
|
|
1654
|
+
hovering?: boolean;
|
|
1655
|
+
hoveringSelected?: boolean;
|
|
1656
|
+
zoomAtLeast?: number;
|
|
1657
|
+
/** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
|
|
1658
|
+
* mode. Absent flag is treated as `false`. */
|
|
1659
|
+
editingAnchors?: boolean;
|
|
1660
|
+
/** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
|
|
1661
|
+
* (resizable), so `{ resizable: true }` passes for legacy ctx builders
|
|
1662
|
+
* that don't compute it. */
|
|
1663
|
+
resizable?: boolean;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
|
|
1667
|
+
* over `Selector` leaves. `when` is the escape hatch — its closure is
|
|
1668
|
+
* opaque to introspection and should be avoided when a declarative form
|
|
1669
|
+
* exists. Empty `all` is true; empty `any` is false.
|
|
1670
|
+
*/
|
|
1671
|
+
type Rule = Selector | {
|
|
1672
|
+
all: readonly Rule[];
|
|
1673
|
+
} | {
|
|
1674
|
+
any: readonly Rule[];
|
|
1675
|
+
} | {
|
|
1676
|
+
not: Rule;
|
|
1677
|
+
} | {
|
|
1678
|
+
when: (ctx: RuleCtx) => boolean;
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Composable visibility predicate with fluent surface. Carries its underlying
|
|
1683
|
+
* `Rule` tree at `.rule` so the resolver can introspect / share trees with
|
|
1684
|
+
* the affordance pipeline and the dispatcher's eligibility filter.
|
|
1685
|
+
*
|
|
1686
|
+
* Callable form `cond(ctx)` evaluates the tree against ctx. The fluent
|
|
1687
|
+
* methods return new Conditions wrapping new trees.
|
|
1688
|
+
*
|
|
1689
|
+
* **Chain semantics: strict left-to-right, no precedence.**
|
|
1690
|
+
* `a.and(b).or(c)` is `(a && b) || c`; `a.or(b).and(c)` is
|
|
1691
|
+
* `(a || b) && c`. Mix `.and` and `.or` only when you mean
|
|
1692
|
+
* left-to-right evaluation. For grouped disjunction, name the
|
|
1693
|
+
* subexpression or use the top-level `or(...)`.
|
|
1694
|
+
*/
|
|
1695
|
+
interface Condition {
|
|
1696
|
+
(ctx: RuleCtx): boolean;
|
|
1697
|
+
readonly rule: Rule;
|
|
1698
|
+
/** `this && other` */
|
|
1699
|
+
and(other: Condition | Rule): Condition;
|
|
1700
|
+
/** `this || other` */
|
|
1701
|
+
or(other: Condition | Rule): Condition;
|
|
1702
|
+
/** `this && !other` */
|
|
1703
|
+
andNot(other: Condition | Rule): Condition;
|
|
1704
|
+
/** `this || !other` */
|
|
1705
|
+
orNot(other: Condition | Rule): Condition;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/** Phase of a gesture lifecycle. `initial` means the tool is idle
|
|
1709
|
+
* (scratch null); `engaged` means a gesture is in progress (scratch
|
|
1710
|
+
* populated). The route-grammar's `[phase]` slot draws from this set. */
|
|
1711
|
+
type RoutePhase = 'initial' | 'engaged';
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Route-string grammar v3:
|
|
1715
|
+
*
|
|
1716
|
+
* route = phaseSlot WS gesture WS argSlot? WS targetSlot? WS modSlot?
|
|
1717
|
+
* phaseSlot = '[' phaseList ']'
|
|
1718
|
+
* phaseList = phaseAtom (WS ',' WS phaseAtom)*
|
|
1719
|
+
* phaseAtom = (channel ':')? phaseValue -- bare phaseValue ≡ '&:phaseValue'
|
|
1720
|
+
* channel = '&' | '*' | toolId -- '&' = the binding's own tool
|
|
1721
|
+
* phaseValue = 'initial' | 'engaged' | '*'
|
|
1722
|
+
* argSlot = '(' argValue ')' -- whitespace inside parens is significant
|
|
1723
|
+
* targetSlot = '=>' WS targetValue -- omitted slot defaults to '*' for hasTarget
|
|
1724
|
+
* modSlot = modAtom (WS modAtom)*
|
|
1725
|
+
* modAtom = sigil modName
|
|
1726
|
+
* sigil = '+' | '?' -- ! @ # $ % ^ & * reserved as id-prefix
|
|
1727
|
+
* modName = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
|
|
1728
|
+
*
|
|
1729
|
+
* Shorthand: a bare phaseValue (no `:`) implies channel `&` ("this tool's
|
|
1730
|
+
* own phase"). `[engaged]` ≡ `[&:engaged]`; `[*]` ≡ `[&:*]`. The truly-loose
|
|
1731
|
+
* form (any channel, any phase) is `[*:*]`.
|
|
1732
|
+
*
|
|
1733
|
+
* Examples:
|
|
1734
|
+
* [initial] click => empty +shift -- self idle
|
|
1735
|
+
* [engaged] wheel -- self mid-gesture
|
|
1736
|
+
* [rect:engaged] wheel -- when rect tool is mid-gesture
|
|
1737
|
+
* [*:engaged] keyDown(Delete) -- when any tool is mid-gesture
|
|
1738
|
+
* [initial,engaged] contextMenu => empty -- either self phase
|
|
1739
|
+
* [*] click => empty -- self, any phase
|
|
1740
|
+
*/
|
|
1741
|
+
|
|
1742
|
+
/** Channel reference for a phase atom. `'&'` = the binding's own tool;
|
|
1743
|
+
* `'*'` = any tool; otherwise a registered tool id. */
|
|
1744
|
+
type ChannelRef = '&' | '*' | string;
|
|
1745
|
+
/** One element of a phase list: a (channel, phase) pair. The default
|
|
1746
|
+
* channel (omitted in the shorthand) is `'&'`. `phase: '*'` means
|
|
1747
|
+
* "any phase of the given channel". */
|
|
1748
|
+
interface PhaseAtom {
|
|
1749
|
+
channel: ChannelRef;
|
|
1750
|
+
phase: RoutePhase | '*';
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* GestureSpec — describes the form of a user input event that can fire an action.
|
|
1755
|
+
*
|
|
1756
|
+
* Used by `Action.defaultBinding` (the action's preferred gesture) and by
|
|
1757
|
+
* `GestureBinding.spec` (a tool's binding table entry). The dispatcher matches
|
|
1758
|
+
* incoming input events against registered specs to determine which action to
|
|
1759
|
+
* invoke.
|
|
1760
|
+
*
|
|
1761
|
+
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md` § "Types".
|
|
1762
|
+
*/
|
|
1763
|
+
/** Optional modifier-key requirement for a gesture spec.
|
|
1764
|
+
*
|
|
1765
|
+
* Matching semantics (strict): an omitted modifier field means the
|
|
1766
|
+
* modifier MUST NOT be held — i.e., a bare `{ kind: 'key', key: 'Escape' }`
|
|
1767
|
+
* matches only unmodified Escape, NOT Cmd+Escape. A `true` means the
|
|
1768
|
+
* modifier MUST be held; `false` is the same as omitted (must be absent).
|
|
1769
|
+
* This mirrors today's `KeyBinding` matcher and keeps conflict detection
|
|
1770
|
+
* coherent.
|
|
1771
|
+
*
|
|
1772
|
+
* `mod` is a platform-aware shorthand: matches `metaKey` on mac, `ctrlKey`
|
|
1773
|
+
* elsewhere (mirrors `KeyBinding.mod`).
|
|
1774
|
+
*
|
|
1775
|
+
* `shift` additionally accepts `'optional'` meaning "shifted or unshifted
|
|
1776
|
+
* both acceptable" — the explicit opt-in for loose matching, used by
|
|
1777
|
+
* actions like nudge whose step size depends on shift but whose firing
|
|
1778
|
+
* does not. To widen other modifiers similarly, extend their type when
|
|
1779
|
+
* a real consumer needs it.
|
|
1780
|
+
*/
|
|
1781
|
+
type ModSpec = Partial<{
|
|
1782
|
+
alt: boolean | 'optional';
|
|
1783
|
+
ctrl: boolean | 'optional';
|
|
1784
|
+
meta: boolean | 'optional';
|
|
1785
|
+
mod: boolean | 'optional';
|
|
1786
|
+
shift: boolean | 'optional';
|
|
1787
|
+
}>;
|
|
1788
|
+
/** Target selector for click and drag gesture specs. String forms are sugar
|
|
1789
|
+
* for the kit-owned object-kind registry (TODO.md Tier 1 follow-up); until
|
|
1790
|
+
* that ships, consumers can pass `{ kindOf: predicate }` to classify hits
|
|
1791
|
+
* themselves. */
|
|
1792
|
+
type TargetSpec = 'empty' | 'selected-body' | 'unselected-body' | `kind:${string}` | `kind:${string}:selected` | `affordance:${string}` | {
|
|
1793
|
+
/** Predicate. `hit` is the raw target (affordance for drag,
|
|
1794
|
+
* `e.target` otherwise); `bodyTarget` is the optional body-class
|
|
1795
|
+
* string ('empty' | 'selected-body' | 'unselected-body') when
|
|
1796
|
+
* `classifyTarget` is wired. Predicates that only need one of the
|
|
1797
|
+
* two can ignore the other. */
|
|
1798
|
+
kindOf: (hit: unknown, bodyTarget?: string) => boolean;
|
|
1799
|
+
};
|
|
1800
|
+
/** Phase qualifier on a gesture spec. Restricts when the spec matches based
|
|
1801
|
+
* on per-tool gesture-lifecycle state.
|
|
1802
|
+
*
|
|
1803
|
+
* Shorthand forms (most common case — gate on the binding's own tool):
|
|
1804
|
+
* `'engaged'` → `[{ channel: '&', phase: 'engaged' }]` // self mid-gesture
|
|
1805
|
+
* `'initial'` → `[{ channel: '&', phase: 'initial' }]` // self idle
|
|
1806
|
+
* `'*'` → `[{ channel: '&', phase: '*' }]` // either self phase
|
|
1807
|
+
*
|
|
1808
|
+
* Array form for explicit channel:phase atoms — e.g. `[{ channel: 'rect',
|
|
1809
|
+
* phase: 'engaged' }]` for "when the rect tool is mid-gesture, regardless of
|
|
1810
|
+
* which scope I'm in." See the v3 route grammar in
|
|
1811
|
+
* `@weasel-js/gestures/grammar` for the full lattice.
|
|
1812
|
+
*
|
|
1813
|
+
* When omitted, matches in any phase (preserves pre-phase behavior). */
|
|
1814
|
+
type PhaseSpec = 'initial' | 'engaged' | '*' | readonly PhaseAtom[];
|
|
1815
|
+
/** Single-keystroke gesture (keydown). */
|
|
1816
|
+
interface KeySpec {
|
|
1817
|
+
kind: 'key';
|
|
1818
|
+
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
1819
|
+
key: string | string[];
|
|
1820
|
+
mods?: ModSpec;
|
|
1821
|
+
phase?: PhaseSpec;
|
|
1822
|
+
}
|
|
1823
|
+
/** Key-held gesture (keydown opens, keyup closes). Drives "hold space for
|
|
1824
|
+
* hand tool"-style interactions. */
|
|
1825
|
+
interface KeyHeldSpec {
|
|
1826
|
+
kind: 'key-held';
|
|
1827
|
+
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
1828
|
+
key: string | string[];
|
|
1829
|
+
mods?: ModSpec;
|
|
1830
|
+
phase?: PhaseSpec;
|
|
1831
|
+
}
|
|
1832
|
+
/** Wheel-event gesture. `direction` filters by deltaY sign; default `'*'`.
|
|
1833
|
+
* - `'up'` → matches only deltaY < 0
|
|
1834
|
+
* - `'down'` → matches only deltaY > 0
|
|
1835
|
+
* - `'*'` → matches either sign (default; universal-wildcard convention) */
|
|
1836
|
+
interface WheelSpec {
|
|
1837
|
+
kind: 'wheel';
|
|
1838
|
+
direction?: 'up' | 'down' | '*';
|
|
1839
|
+
mods?: ModSpec;
|
|
1840
|
+
phase?: PhaseSpec;
|
|
1841
|
+
}
|
|
1842
|
+
/** Click gesture (pointerdown + pointerup without movement past the
|
|
1843
|
+
* threshold). */
|
|
1844
|
+
interface ClickSpec {
|
|
1845
|
+
kind: 'click';
|
|
1846
|
+
target?: TargetSpec;
|
|
1847
|
+
mods?: ModSpec;
|
|
1848
|
+
phase?: PhaseSpec;
|
|
1849
|
+
}
|
|
1850
|
+
/** Double-click: two `click` events within ~500ms and ~5px of each other.
|
|
1851
|
+
* Synthesized by `useGestureDispatcher`; emitted AFTER the second
|
|
1852
|
+
* `click`. Bindings that want to handle a double-click should declare
|
|
1853
|
+
* this kind rather than chasing two `click` events. */
|
|
1854
|
+
interface DoubleClickSpec {
|
|
1855
|
+
kind: 'doubleClick';
|
|
1856
|
+
target?: TargetSpec;
|
|
1857
|
+
mods?: ModSpec;
|
|
1858
|
+
phase?: PhaseSpec;
|
|
1859
|
+
}
|
|
1860
|
+
/** Right-click (contextmenu) gesture. The dispatcher calls
|
|
1861
|
+
* `preventDefault()` on the underlying DOM event so the native menu
|
|
1862
|
+
* doesn't appear — tools/actions fully own the right-click UX. */
|
|
1863
|
+
interface ContextMenuSpec {
|
|
1864
|
+
kind: 'contextMenu';
|
|
1865
|
+
target?: TargetSpec;
|
|
1866
|
+
mods?: ModSpec;
|
|
1867
|
+
phase?: PhaseSpec;
|
|
1868
|
+
}
|
|
1869
|
+
/** Drag gesture (pointerdown + pointermove past the threshold). */
|
|
1870
|
+
interface DragSpec {
|
|
1871
|
+
kind: 'drag';
|
|
1872
|
+
target?: TargetSpec;
|
|
1873
|
+
mods?: ModSpec;
|
|
1874
|
+
phase?: PhaseSpec;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Bare pointer press, matched at down time — before the dispatcher knows
|
|
1878
|
+
* whether the gesture will become a click or a drag.
|
|
1879
|
+
*
|
|
1880
|
+
* Reach for this only when the effect must be visible while the button is
|
|
1881
|
+
* still held. Selection is the motivating case: pressing an unselected node
|
|
1882
|
+
* highlights it immediately, and the drag that may follow then starts from an
|
|
1883
|
+
* already-correct selection. Anything that can wait for the release belongs on
|
|
1884
|
+
* a `click` spec, which does not fire on a press that turns into a drag.
|
|
1885
|
+
*
|
|
1886
|
+
* A matching binding does NOT own the gesture: the same press goes on to open
|
|
1887
|
+
* a drag or synthesize a click as usual. Bind an immediate action here, not an
|
|
1888
|
+
* ongoing one.
|
|
1889
|
+
*/
|
|
1890
|
+
interface PointerDownSpec {
|
|
1891
|
+
kind: 'pointerDown';
|
|
1892
|
+
target?: TargetSpec;
|
|
1893
|
+
mods?: ModSpec;
|
|
1894
|
+
phase?: PhaseSpec;
|
|
1895
|
+
}
|
|
1896
|
+
/** Multi-touch gesture. `fingers` is the required touch count. */
|
|
1897
|
+
interface MultiTouchSpec {
|
|
1898
|
+
kind: 'multiTouch';
|
|
1899
|
+
fingers: number;
|
|
1900
|
+
mods?: ModSpec;
|
|
1901
|
+
phase?: PhaseSpec;
|
|
1902
|
+
}
|
|
1903
|
+
/** Multi-touch tap gesture — fires when N fingers touch down then release
|
|
1904
|
+
* together without movement past the tap threshold. Synthesized by the
|
|
1905
|
+
* dispatcher from the underlying multitouch tracking. */
|
|
1906
|
+
interface MultiTouchTapSpec {
|
|
1907
|
+
kind: 'multiTouchTap';
|
|
1908
|
+
fingers: number;
|
|
1909
|
+
mods?: ModSpec;
|
|
1910
|
+
phase?: PhaseSpec;
|
|
1911
|
+
}
|
|
1912
|
+
/** OS drag-and-drop of external content onto the canvas. `types` filters by
|
|
1913
|
+
* MIME glob (`'image/*'`, `'text/plain'`); the spec matches when ANY item's
|
|
1914
|
+
* MIME matches ANY glob. Omitted or empty = matches any drop. */
|
|
1915
|
+
interface DropSpec {
|
|
1916
|
+
kind: 'drop';
|
|
1917
|
+
types?: string[];
|
|
1918
|
+
mods?: ModSpec;
|
|
1919
|
+
phase?: PhaseSpec;
|
|
1920
|
+
}
|
|
1921
|
+
/** System-clipboard paste of external content. Same `types` semantics as
|
|
1922
|
+
* {@link DropSpec} — omitted or empty = matches any paste. */
|
|
1923
|
+
interface PasteSpec {
|
|
1924
|
+
kind: 'paste';
|
|
1925
|
+
types?: string[];
|
|
1926
|
+
mods?: ModSpec;
|
|
1927
|
+
phase?: PhaseSpec;
|
|
1928
|
+
}
|
|
1929
|
+
/** The full union of supported gesture spec kinds. New invocation forms
|
|
1930
|
+
* (long-press, two-stage, modal-dialog) extend this union without touching
|
|
1931
|
+
* the `Action` type. */
|
|
1932
|
+
type GestureSpec = KeySpec | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | PointerDownSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
|
|
1933
|
+
|
|
1934
|
+
/** A 2D point in either world or screen coordinates. */
|
|
1935
|
+
interface Point2 {
|
|
1936
|
+
x: number;
|
|
1937
|
+
y: number;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Information about which UI affordance was hit at pointerdown.
|
|
1941
|
+
*
|
|
1942
|
+
* Populated by the dispatcher when the `affordanceAt` thunk is provided to
|
|
1943
|
+
* `useGestureDispatcher`. Tools / action invokers that only fire on a specific
|
|
1944
|
+
* affordance (e.g. a resize handle) use this field as a guard — if the
|
|
1945
|
+
* affordance is absent or is the wrong kind, they return `{}` and let other
|
|
1946
|
+
* bindings handle the drag.
|
|
1947
|
+
*
|
|
1948
|
+
* `kind` is a discriminator string:
|
|
1949
|
+
* - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
|
|
1950
|
+
* `'handle:bottom-right'` — corner resize handles.
|
|
1951
|
+
* - `'rotate-handle'` — the rotation affordance.
|
|
1952
|
+
* - `'anchor:N'` — a path anchor at index N.
|
|
1953
|
+
*
|
|
1954
|
+
* `fixedPoint` is the world-space point that should remain stationary during
|
|
1955
|
+
* the gesture. For resize handles this is the opposite (diagonally fixed)
|
|
1956
|
+
* corner; for rotate it is the pivot.
|
|
1957
|
+
*
|
|
1958
|
+
* `targetIds` are the node ids this affordance belongs to.
|
|
1959
|
+
*/
|
|
1960
|
+
interface AffordanceHit {
|
|
1961
|
+
/** Discriminator string, e.g. `'handle:bottom-right'`. */
|
|
1962
|
+
kind: string;
|
|
1963
|
+
/** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
|
|
1964
|
+
fixedPoint?: {
|
|
1965
|
+
x: number;
|
|
1966
|
+
y: number;
|
|
1967
|
+
};
|
|
1968
|
+
/** Which nodes this affordance belongs to. */
|
|
1969
|
+
targetIds?: string[];
|
|
1970
|
+
/** Set when `kind` matches `'handle:*'`. Identifies which corner stays
|
|
1971
|
+
* fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
|
|
1972
|
+
* Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
|
|
1973
|
+
* leave this undefined. */
|
|
1974
|
+
anchor?: ResizeAnchor;
|
|
1975
|
+
/** CSS cursor to show while the pointer hovers this affordance (no
|
|
1976
|
+
* gesture in flight). Consumed by the hover-cursor pump in
|
|
1977
|
+
* `useGestureDispatcher`; unset = the pump falls through to
|
|
1978
|
+
* action-cursor prediction, then to the active tool's cursor. */
|
|
1979
|
+
cursor?: string;
|
|
1980
|
+
/**
|
|
1981
|
+
* Free-form payload from whatever produced the hit, carried through to the
|
|
1982
|
+
* matching action untouched.
|
|
1983
|
+
*
|
|
1984
|
+
* Kit affordances describe themselves fully in the fields above and leave
|
|
1985
|
+
* this unset. It exists for affordances the kit doesn't know the shape of —
|
|
1986
|
+
* a registered layer's own chrome, where the hit-test already resolved
|
|
1987
|
+
* *which* of its pieces was hit and the action would otherwise have to
|
|
1988
|
+
* redo that work. `@weasel-js/hud` passes the hit widget here.
|
|
1989
|
+
*/
|
|
1990
|
+
payload?: unknown;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* One accumulated point on a drag trail: world-space position plus whatever
|
|
1994
|
+
* stylus state the originating `PointerEvent` carried.
|
|
1995
|
+
*
|
|
1996
|
+
* The stylus fields are absent for mouse/touch on browsers that don't report
|
|
1997
|
+
* them, and for synthetic events. Consumers that want pressure-driven output
|
|
1998
|
+
* (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
|
|
1999
|
+
* their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
|
|
2000
|
+
*/
|
|
2001
|
+
interface DragSample extends Point2 {
|
|
2002
|
+
/** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
|
|
2003
|
+
pressure?: number;
|
|
2004
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
2005
|
+
tiltX?: number;
|
|
2006
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
2007
|
+
tiltY?: number;
|
|
2008
|
+
}
|
|
2009
|
+
/** Per-invocation runtime context the dispatcher hands to an Invoker.
|
|
2010
|
+
* Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
|
|
2011
|
+
* populated only for matching gesture kinds. */
|
|
2012
|
+
interface InvocationCtx {
|
|
2013
|
+
world: Point2;
|
|
2014
|
+
screen: Point2;
|
|
2015
|
+
modifiers: ModifierState;
|
|
2016
|
+
deps: ActionDeps;
|
|
2017
|
+
drag?: {
|
|
2018
|
+
start: Point2;
|
|
2019
|
+
current: Point2;
|
|
2020
|
+
delta: Point2;
|
|
2021
|
+
/**
|
|
2022
|
+
* Drag delta in client/screen coordinates (CSS pixels from the drag
|
|
2023
|
+
* origin). Use this — never `delta` — for any action whose effect
|
|
2024
|
+
* mutates the viewport itself (pan, view-zoom), because world-space
|
|
2025
|
+
* deltas become self-referential as the view shifts mid-drag.
|
|
2026
|
+
*
|
|
2027
|
+
* Populated when the dispatcher received `clientX`/`clientY` on the
|
|
2028
|
+
* underlying pointer events. Absent for legacy callers that don't
|
|
2029
|
+
* provide them.
|
|
2030
|
+
*/
|
|
2031
|
+
screenDelta?: Point2;
|
|
2032
|
+
affordance?: AffordanceHit;
|
|
2033
|
+
/**
|
|
2034
|
+
* Full pointermove history for the current drag, in world space, with
|
|
2035
|
+
* per-sample stylus state when the browser reported it.
|
|
2036
|
+
* Accumulated by the dispatcher on every `pointermove` pump event.
|
|
2037
|
+
* Available only during `onMove` and `onEnd` calls (not on `start`).
|
|
2038
|
+
* Used by `lassoSelectAction` to build its polygon vertex list and by
|
|
2039
|
+
* `insertAction`'s pencil kind to carry the freehand stroke.
|
|
2040
|
+
*/
|
|
2041
|
+
points?: DragSample[];
|
|
2042
|
+
};
|
|
2043
|
+
wheel?: {
|
|
2044
|
+
deltaX: number;
|
|
2045
|
+
deltaY: number;
|
|
2046
|
+
deltaZ: number;
|
|
2047
|
+
};
|
|
2048
|
+
multiTouch?: {
|
|
2049
|
+
centroid: Point2;
|
|
2050
|
+
spread: number;
|
|
2051
|
+
rotation: number;
|
|
2052
|
+
/**
|
|
2053
|
+
* Pinch-zoom geometry. Populated by the dispatcher when a multitouch
|
|
2054
|
+
* handle is in flight and a pointermove-pump fires.
|
|
2055
|
+
* `startSpread` is the spread at the moment the gesture began.
|
|
2056
|
+
* `currentSpread` is the spread at the current frame.
|
|
2057
|
+
*/
|
|
2058
|
+
pinch?: {
|
|
2059
|
+
startSpread: number;
|
|
2060
|
+
currentSpread: number;
|
|
2061
|
+
centroid: Point2;
|
|
2062
|
+
};
|
|
2063
|
+
};
|
|
2064
|
+
key?: {
|
|
2065
|
+
key: string;
|
|
2066
|
+
repeat: boolean;
|
|
2067
|
+
};
|
|
2068
|
+
/**
|
|
2069
|
+
* Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
|
|
2070
|
+
* UI-driven ongoing actions (color picker, opacity slider) so handles can
|
|
2071
|
+
* read the current value on `start` and updated values on `onMove`. The
|
|
2072
|
+
* gesture dispatcher does not populate this field; gesture-driven actions
|
|
2073
|
+
* receive params via `BindingOpts.params` on `start` (the `opts` arg).
|
|
2074
|
+
*/
|
|
2075
|
+
params?: Record<string, unknown>;
|
|
2076
|
+
}
|
|
2077
|
+
/** Per-invocation options the dispatcher reads from a `GestureBinding`'s
|
|
2078
|
+
* `opts` field and passes to `OngoingInvoker.start`. Today carries
|
|
2079
|
+
* behaviors; extensible. */
|
|
2080
|
+
interface BindingOpts {
|
|
2081
|
+
behaviors?: ActionBehavior<unknown, unknown, unknown>[];
|
|
2082
|
+
/** Per-binding action parameters. The action's invoker reads
|
|
2083
|
+
* these via the second arg to `run` (or via InvocationCtx for ongoing
|
|
2084
|
+
* invokers, when needed). Loose typing (Record<string, unknown>) for
|
|
2085
|
+
* now; consider per-action typing later via BindingOpts<A>.
|
|
2086
|
+
*
|
|
2087
|
+
* params may also be a thunk evaluated each time the
|
|
2088
|
+
* dispatcher (or invoker) needs the value. Thunks let tools close over
|
|
2089
|
+
* refs that mutate during a gesture (e.g. polygon `sides` adjusted
|
|
2090
|
+
* mid-drag via ArrowUp). For ongoing invokers that want the latest
|
|
2091
|
+
* values at commit, the invoker can re-call the thunk inside `onEnd`
|
|
2092
|
+
* via `resolveParams(opts?.params)`. */
|
|
2093
|
+
params?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
2094
|
+
}
|
|
2095
|
+
/** Convention-shaped action dependencies bag. Actions declare which
|
|
2096
|
+
* contexts they consume; the dispatcher composes them per call.
|
|
2097
|
+
* Consumer-side contexts (e.g. ColorContext) plug in by extending. */
|
|
2098
|
+
interface ActionDeps {
|
|
2099
|
+
selection?: unknown;
|
|
2100
|
+
view?: unknown;
|
|
2101
|
+
scene?: unknown;
|
|
2102
|
+
pointer?: unknown;
|
|
2103
|
+
activeTool?: unknown;
|
|
2104
|
+
[k: string]: unknown;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Discriminated overlay shape returned by `OngoingHandle.overlay()`.
|
|
2108
|
+
* Dispatcher-side chrome surface for in-flight
|
|
2109
|
+
* gestures that paint non-ghost visuals. The canvas's
|
|
2110
|
+
* `useDispatcherOverlayLayer` walks every in-flight handle, calls
|
|
2111
|
+
* `overlay()`, and dispatches on `kind` to draw the appropriate shape.
|
|
2112
|
+
*
|
|
2113
|
+
* `marquee` mirrors `AreaSelectOverlay`; `lasso` mirrors `LassoSelectOverlay`.
|
|
2114
|
+
* `commands` is the generic escape hatch — actions emit arbitrary
|
|
2115
|
+
* `DrawCommand[]` for previews the typed variants can't express (insert
|
|
2116
|
+
* shape outlines, paste ghosts of synthetic nodes, custom chrome). World-
|
|
2117
|
+
* space is the default; the layer wraps in `viewToMat3` so commands track
|
|
2118
|
+
* the camera. Set `space: 'screen'` for projections you've already done
|
|
2119
|
+
* yourself (rare).
|
|
2120
|
+
*/
|
|
2121
|
+
type OngoingOverlay = {
|
|
2122
|
+
kind: 'marquee';
|
|
2123
|
+
start: {
|
|
2124
|
+
x: number;
|
|
2125
|
+
y: number;
|
|
2126
|
+
};
|
|
2127
|
+
current: {
|
|
2128
|
+
x: number;
|
|
2129
|
+
y: number;
|
|
2130
|
+
};
|
|
2131
|
+
shiftHeld: boolean;
|
|
2132
|
+
} | {
|
|
2133
|
+
kind: 'lasso';
|
|
2134
|
+
vertices: ReadonlyArray<{
|
|
2135
|
+
x: number;
|
|
2136
|
+
y: number;
|
|
2137
|
+
}>;
|
|
2138
|
+
current: {
|
|
2139
|
+
x: number;
|
|
2140
|
+
y: number;
|
|
2141
|
+
};
|
|
2142
|
+
shiftHeld: boolean;
|
|
2143
|
+
} | {
|
|
2144
|
+
kind: 'commands';
|
|
2145
|
+
commands: readonly DrawCommand[];
|
|
2146
|
+
/** Coordinate space the commands are authored in. Default `'world'`
|
|
2147
|
+
* — the layer wraps them in `viewToMat3(view)` so they track the
|
|
2148
|
+
* camera. `'screen'` emits them as-is (CSS pixels). */
|
|
2149
|
+
space?: 'world' | 'screen';
|
|
2150
|
+
} | {
|
|
2151
|
+
/**
|
|
2152
|
+
* Live insert-drag preview — dispatched by `insertAction` while the
|
|
2153
|
+
* user is dragging out a new shape. Pre-commit there is no scene node
|
|
2154
|
+
* to ghost via `previewIds()`/`previewPose()`, so insert paints its
|
|
2155
|
+
* preview through the dispatcher overlay layer instead.
|
|
2156
|
+
*
|
|
2157
|
+
* `shape` is the kit's built-in insert kind. `bounds` is the AABB of
|
|
2158
|
+
* the current drag (start/current normalized). `extras` is the
|
|
2159
|
+
* per-kind extras the action already collected — the overlay
|
|
2160
|
+
* renderer rebuilds the shape using the same path builders the
|
|
2161
|
+
* commit factory uses, so the preview matches the eventual node.
|
|
2162
|
+
*
|
|
2163
|
+
* `extras` is opaque (`unknown`) at the union level; the overlay
|
|
2164
|
+
* renderer narrows on `shape` and casts the field shape it expects.
|
|
2165
|
+
*/
|
|
2166
|
+
kind: 'insertPreview';
|
|
2167
|
+
shape: 'rect' | 'ellipse' | 'line' | 'polygon' | 'star' | 'pencil';
|
|
2168
|
+
bounds: {
|
|
2169
|
+
x: number;
|
|
2170
|
+
y: number;
|
|
2171
|
+
width: number;
|
|
2172
|
+
height: number;
|
|
2173
|
+
};
|
|
2174
|
+
extras: unknown;
|
|
2175
|
+
/** World-space point to paint a small "anchor" dot at. Sells the
|
|
2176
|
+
* click point as the drag's anchor — particularly useful for
|
|
2177
|
+
* radial shapes (polygon/star) where no vertex sits on the
|
|
2178
|
+
* click point, and for any shape in center mode where the dot
|
|
2179
|
+
* marks the center the shape grows around. */
|
|
2180
|
+
anchorPoint?: {
|
|
2181
|
+
x: number;
|
|
2182
|
+
y: number;
|
|
2183
|
+
};
|
|
2184
|
+
};
|
|
2185
|
+
/** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
|
|
2186
|
+
* `onMove` on subsequent input events of the same gesture and calls
|
|
2187
|
+
* `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
|
|
2188
|
+
* on pointercancel / blur / escape). */
|
|
2189
|
+
interface OngoingHandle {
|
|
2190
|
+
/**
|
|
2191
|
+
* Optional logical action kind — a stable, human-readable tag the
|
|
2192
|
+
* dispatcher exposes via `getActiveAction()` for chrome-visibility
|
|
2193
|
+
* rules and any other surface that wants to react to "what action
|
|
2194
|
+
* is currently in flight" without inspecting handles directly.
|
|
2195
|
+
*
|
|
2196
|
+
* Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
|
|
2197
|
+
* `'pan'`, `'pinch'`.
|
|
2198
|
+
*
|
|
2199
|
+
* Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
|
|
2200
|
+
* `key-held-Space`, etc.) which keys per-pointer state and is not
|
|
2201
|
+
* meaningful to consumers.
|
|
2202
|
+
*
|
|
2203
|
+
* When omitted, the action is "anonymous" — `getActiveAction().kind`
|
|
2204
|
+
* reports `null` even though a handle is in flight. This is fine for
|
|
2205
|
+
* actions that don't have visible chrome of their own.
|
|
2206
|
+
*/
|
|
2207
|
+
kind?: string;
|
|
2208
|
+
onMove?(ctx: InvocationCtx): void;
|
|
2209
|
+
onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
|
|
2210
|
+
/**
|
|
2211
|
+
* Optional preview surface — dispatcher-side ghost overlay.
|
|
2212
|
+
*
|
|
2213
|
+
* An ongoing-action implementation may populate `previewIds()` +
|
|
2214
|
+
* `previewPose(id)` to expose its in-flight preview state for the
|
|
2215
|
+
* canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
|
|
2216
|
+
* top of the committed scene during the gesture.
|
|
2217
|
+
*
|
|
2218
|
+
* Returning `null` (or omitting the method entirely) means "no preview
|
|
2219
|
+
* this gesture" — the canvas will skip this handle as a source.
|
|
2220
|
+
*
|
|
2221
|
+
* Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
|
|
2222
|
+
* pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
|
|
2223
|
+
* returns the interim pose for one of those ids (shape opaque — the
|
|
2224
|
+
* canvas casts to its `TPose` parameter). The preview-ghost layer
|
|
2225
|
+
* merges all sources via first-non-null semantics, with tool-side
|
|
2226
|
+
* previews taking precedence over dispatcher-side (preserves
|
|
2227
|
+
* backwards-compat during the registry-unification migration).
|
|
2228
|
+
*/
|
|
2229
|
+
previewIds?(): Iterable<string> | null;
|
|
2230
|
+
previewPose?(id: string): unknown | null;
|
|
2231
|
+
/**
|
|
2232
|
+
* When `false`, the preview-ghost layer paints the ghost AND the
|
|
2233
|
+
* source node stays visible at its committed pose. Defaults to
|
|
2234
|
+
* `true` (move/resize/rotate semantics: ghost replaces the source
|
|
2235
|
+
* during the gesture). Clone overrides to `false` so the original
|
|
2236
|
+
* stays put and the ghost appears at the drag target.
|
|
2237
|
+
*/
|
|
2238
|
+
previewHidesSource?: boolean;
|
|
2239
|
+
/**
|
|
2240
|
+
* Optional per-id preview *data*. Falls back to the committed
|
|
2241
|
+
* `node.data` when null/absent. Use when the gesture mutates
|
|
2242
|
+
* `node.data` (e.g. anchor-edit on nodes that store the polygon on
|
|
2243
|
+
* `data.path`) rather than (or in addition to) the pose. The preview-
|
|
2244
|
+
* ghost layer assembles a synthetic node from `{ ...node, pose:
|
|
2245
|
+
* previewPose ?? node.pose, data: previewData ?? node.data }` before
|
|
2246
|
+
* calling the scene slot's `drawOne`.
|
|
2247
|
+
*
|
|
2248
|
+
* Sources compose first-non-null per axis: an action can emit only
|
|
2249
|
+
* `previewPose` (translation), only `previewData` (data-only edit),
|
|
2250
|
+
* or both (pose + data both change, e.g. anchor drag on a data.path
|
|
2251
|
+
* node where the bounds shift).
|
|
2252
|
+
*/
|
|
2253
|
+
previewData?(id: string): unknown | null;
|
|
2254
|
+
/**
|
|
2255
|
+
* Optional chrome surface — dispatcher-side overlay layer.
|
|
2256
|
+
*
|
|
2257
|
+
* An ongoing-action implementation may populate `overlay()` to expose a
|
|
2258
|
+
* non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
|
|
2259
|
+
* `useDispatcherOverlayLayer` to paint while the gesture is in flight.
|
|
2260
|
+
* Returning `null` (or omitting the method) means "no overlay this
|
|
2261
|
+
* gesture" — the canvas will skip this handle as a chrome source.
|
|
2262
|
+
*
|
|
2263
|
+
* Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
|
|
2264
|
+
* which paints displaced scene-node silhouettes. Marquee and lasso
|
|
2265
|
+
* gestures don't displace any node, but still need on-screen feedback.
|
|
2266
|
+
*/
|
|
2267
|
+
overlay?(): OngoingOverlay | null;
|
|
2268
|
+
}
|
|
2269
|
+
/** Fire-once invocation. Runs to completion synchronously (or fires off an
|
|
2270
|
+
* async side-effect; the registry doesn't wait). */
|
|
2271
|
+
interface ImmediateInvoker {
|
|
2272
|
+
timing: 'immediate';
|
|
2273
|
+
/** `params` carries the matched binding's opts.params. When
|
|
2274
|
+
* invoked via the legacy `Action.run` bridge or from the command palette
|
|
2275
|
+
* with no per-binding context, `params` is undefined; descriptors should
|
|
2276
|
+
* default to a sensible variant. */
|
|
2277
|
+
run(deps: ActionDeps, params?: Record<string, unknown>): void;
|
|
2278
|
+
}
|
|
2279
|
+
/** Phase-machine invocation. `start` opens the phase and returns the handle
|
|
2280
|
+
* the dispatcher pumps. */
|
|
2281
|
+
interface OngoingInvoker {
|
|
2282
|
+
timing: 'ongoing';
|
|
2283
|
+
start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
|
|
2284
|
+
}
|
|
2285
|
+
/** Pluggable invocation strategy for an Action. Future variants
|
|
2286
|
+
* (`longPress`, `twoStage`, `modal`) extend this union without touching
|
|
2287
|
+
* the `Action` type. */
|
|
2288
|
+
type Invoker = ImmediateInvoker | OngoingInvoker;
|
|
2289
|
+
|
|
2290
|
+
/** Boolean op identifiers — five Pathfinder primaries plus Crop. */
|
|
2291
|
+
type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
|
|
2292
|
+
/**
|
|
2293
|
+
* z-position descriptor for a path node. `parentId` is the direct parent
|
|
2294
|
+
* (or `null` for a top-level node); `index` is the position within that
|
|
2295
|
+
* parent's child order. Used by the optional `getZOrder` hook below to
|
|
2296
|
+
* reposition the result of a boolean op at the topmost source's slot.
|
|
2297
|
+
*/
|
|
2298
|
+
/** @internal */
|
|
2299
|
+
interface BooleanZOrder {
|
|
2300
|
+
parentId: string | null;
|
|
2301
|
+
index: number;
|
|
2302
|
+
}
|
|
2303
|
+
/** Adapter the hook and the pure core both consume. */
|
|
2304
|
+
interface BooleansAdapter {
|
|
2305
|
+
getSelection(): NodeId[];
|
|
2306
|
+
getWorldPath(id: NodeId): Path | undefined;
|
|
2307
|
+
compareZ(a: NodeId, b: NodeId): number;
|
|
2308
|
+
/**
|
|
2309
|
+
* Mint a new node from a boolean-op result `Path`. `producedBy` names the
|
|
2310
|
+
* op that synthesized it — adapters that store provenance (e.g. for a
|
|
2311
|
+
* layer-panel icon) record it; others ignore the arg.
|
|
2312
|
+
*/
|
|
2313
|
+
createPathNode(path: Path, producedBy: BooleanOp): {
|
|
2314
|
+
id: string;
|
|
2315
|
+
};
|
|
2316
|
+
/**
|
|
2317
|
+
* Optional: return the full object for an id, used by the delete ops so
|
|
2318
|
+
* their `invert` (an insert) can restore the complete object on undo.
|
|
2319
|
+
* If omitted, a `{ id }` stub is captured — undo will reinstate the id
|
|
2320
|
+
* but consumers reading other fields (path, fill, etc.) will see them as
|
|
2321
|
+
* undefined. Mirrors `DeleteAdapter.getNode`; should be provided whenever
|
|
2322
|
+
* undo over boolean ops is expected to be lossless.
|
|
2323
|
+
*/
|
|
2324
|
+
getNode?(id: NodeId): {
|
|
2325
|
+
id: string;
|
|
2326
|
+
} | undefined | null;
|
|
2327
|
+
/**
|
|
2328
|
+
* Optional: return the parent + child-index of `id` so the result of a
|
|
2329
|
+
* boolean op can be placed in the topmost source's z-slot. Adapters that
|
|
2330
|
+
* also expose `getChildren`/`setChildOrder` (the `ReorderAdapter`
|
|
2331
|
+
* contract) will have the kit emit a `createMoveToIndexOp` after the
|
|
2332
|
+
* inserts. Adapters that omit this method get v1 behavior — the result
|
|
2333
|
+
* lands wherever the adapter's plain `insertNode` defaults to.
|
|
2334
|
+
*/
|
|
2335
|
+
getZOrder?(id: NodeId): BooleanZOrder | undefined;
|
|
2336
|
+
applyOps?(ops: Op[], label?: string): void;
|
|
2337
|
+
setSelection?(ids: NodeId[]): void;
|
|
2338
|
+
insertNode?(node: {
|
|
2339
|
+
id: string;
|
|
2340
|
+
}): void;
|
|
2341
|
+
removeNode?(id: string): void;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/** API returned by {@link useSelection}. */
|
|
2345
|
+
interface SelectionApi {
|
|
2346
|
+
/** Current selection. Re-renders trigger when this reference changes. */
|
|
2347
|
+
current: readonly NodeId[];
|
|
2348
|
+
/** Imperative read for use inside event callbacks (avoids stale closures). */
|
|
2349
|
+
get(): NodeId[];
|
|
2350
|
+
/** Replace selection. */
|
|
2351
|
+
set(ids: NodeId[]): void;
|
|
2352
|
+
/** Add id (multi-mode appends; single-mode replaces). */
|
|
2353
|
+
add(id: NodeId): void;
|
|
2354
|
+
/** Remove id from selection. */
|
|
2355
|
+
remove(id: NodeId): void;
|
|
2356
|
+
/** Toggle id in/out of selection. */
|
|
2357
|
+
toggle(id: NodeId): void;
|
|
2358
|
+
/** Clear selection. */
|
|
2359
|
+
clear(): void;
|
|
2360
|
+
/** True if id is selected. */
|
|
2361
|
+
contains(id: NodeId): boolean;
|
|
2362
|
+
/**
|
|
2363
|
+
* Apply a click to the selection per the configured mode/extend key.
|
|
2364
|
+
* - `single`: replaces selection with `[id]`, regardless of modifiers.
|
|
2365
|
+
* - `multi`: with the extend key held, toggles `id` in/out of the selection;
|
|
2366
|
+
* otherwise replaces with `[id]`.
|
|
2367
|
+
*/
|
|
2368
|
+
applyClick(id: NodeId, modifiers: {
|
|
2369
|
+
shift: boolean;
|
|
2370
|
+
meta: boolean;
|
|
2371
|
+
ctrl: boolean;
|
|
2372
|
+
}): void;
|
|
2373
|
+
/** Pre-built methods for spreading into an adapter that needs them. */
|
|
2374
|
+
adapterMethods: {
|
|
2375
|
+
getSelection: () => NodeId[];
|
|
2376
|
+
setSelection: (ids: NodeId[]) => void;
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
/** Context handed to every content handler for one ingest event. */
|
|
2381
|
+
interface IngestCtx {
|
|
2382
|
+
/** World-space arrival point (drop / pointed imperative ingest); `null`
|
|
2383
|
+
* for paste and point-less calls — handlers pick their own policy
|
|
2384
|
+
* (the kit image handler centers on the viewport). */
|
|
2385
|
+
point: {
|
|
2386
|
+
x: number;
|
|
2387
|
+
y: number;
|
|
2388
|
+
} | null;
|
|
2389
|
+
/** Visible canvas area in world coordinates. */
|
|
2390
|
+
viewportWorldRect(): {
|
|
2391
|
+
x: number;
|
|
2392
|
+
y: number;
|
|
2393
|
+
width: number;
|
|
2394
|
+
height: number;
|
|
2395
|
+
};
|
|
2396
|
+
/** The kit insert dep — id/layer/undoable-op supplied; the canonical way
|
|
2397
|
+
* for a handler to mint a node (`insert.commit(bounds, { kind, ... })`). */
|
|
2398
|
+
insert: InsertDep;
|
|
2399
|
+
/** Raw op commit for handlers that build their own ops. */
|
|
2400
|
+
applyOps(ops: Op[], label?: string): void;
|
|
2401
|
+
scene: Scene<unknown, string, unknown>;
|
|
2402
|
+
selection: SelectionApi;
|
|
2403
|
+
/** Consumer file→src resolver (SceneCanvas `ingestion.resolveSrc`).
|
|
2404
|
+
* When absent, the kit image handler embeds as a `data:` URI. */
|
|
2405
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
2406
|
+
/** Kit SVG-handler options (SceneCanvas `ingestion.svg`) — e.g.
|
|
2407
|
+
* `{ unpack: unpackSvgFiles }` (from `@weasel-js/svg`) to parse SVG files
|
|
2408
|
+
* into scene nodes. */
|
|
2409
|
+
svg?: SvgIngestOptions;
|
|
2410
|
+
/** Clipboard-paste seam — present when the hosting `SceneCanvas` supplied
|
|
2411
|
+
* an adapter with `commitPaste`. `reviver` comes from
|
|
2412
|
+
* `SceneCanvasProps.ingestion.clipboard`. Absent ⇒ the kit weasel-JSON
|
|
2413
|
+
* handler declines inert (dwarn, nothing ingested) — its matched items
|
|
2414
|
+
* were already consumed at match time, so they do NOT fall through;
|
|
2415
|
+
* only match-level misses flow on to other handlers. */
|
|
2416
|
+
clipboard?: ClipboardIngestCtx;
|
|
2417
|
+
/** Set to `true` by the kit weasel-JSON handler when it successfully
|
|
2418
|
+
* pastes a payload in this event. The `ctx` object is shared across all
|
|
2419
|
+
* handlers in one `runIngest` call, and higher-priority handlers' `handle`
|
|
2420
|
+
* bodies run (synchronously) before lower ones — so `kit:svg`'s
|
|
2421
|
+
* `text/plain` SVG fallback reads this to decline the SVG flavor of a copy
|
|
2422
|
+
* whose canonical weasel-JSON flavor already ingested (avoids a
|
|
2423
|
+
* double-paste when both flavors ride one clipboard event). */
|
|
2424
|
+
consumedWeaselPayload?: boolean;
|
|
2425
|
+
/** Full action-deps bag, for consumer handlers that need more. */
|
|
2426
|
+
deps: ActionDeps;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
* @experimental
|
|
2431
|
+
* PointerContext — a tiny ambient context that publishes the world-space
|
|
2432
|
+
* position of the canvas pointer, refreshed on every `pointermove` over
|
|
2433
|
+
* the canvas. Cleared (set to `null`) on `pointerleave`.
|
|
2434
|
+
*
|
|
2435
|
+
* Why ref-based and not state-based: cursor moves fire dozens of times per
|
|
2436
|
+
* second; routing those through React state would re-render every consumer
|
|
2437
|
+
* in the tree. The context exposes a stable `pointerRef` whose `.current`
|
|
2438
|
+
* is mutated directly by the publisher, plus a thunk `getDropPoint()` that
|
|
2439
|
+
* reads it on demand. Consumers (e.g. `useClipboard`) pull via the thunk
|
|
2440
|
+
* inside their callbacks — no subscription, no re-render.
|
|
2441
|
+
*
|
|
2442
|
+
* `<SceneCanvas>` publishes automatically. `useClipboardOps` consumes when
|
|
2443
|
+
* the caller didn't pass an explicit `getDropPoint` option. Other future
|
|
2444
|
+
* hit-on-cursor consumers (drop-zone hover, context-menu anchor) can reuse
|
|
2445
|
+
* the same context.
|
|
2446
|
+
*/
|
|
2447
|
+
|
|
2448
|
+
/** @experimental World-space pointer position, or `null` when the pointer
|
|
2449
|
+
* isn't over the publishing canvas. */
|
|
2450
|
+
type PointerWorldPos = {
|
|
2451
|
+
worldX: number;
|
|
2452
|
+
worldY: number;
|
|
2453
|
+
} | null;
|
|
2454
|
+
/** @experimental */
|
|
2455
|
+
interface PointerContextValue {
|
|
2456
|
+
/** Live ref — mutate to publish, read for the latest snapshot. The
|
|
2457
|
+
* identity is stable for the lifetime of the provider. */
|
|
2458
|
+
readonly pointerRef: MutableRefObject<PointerWorldPos>;
|
|
2459
|
+
/** Convenience thunk equivalent to `() => pointerRef.current`. Stable
|
|
2460
|
+
* identity for the lifetime of the provider; safe to pass to hooks. */
|
|
2461
|
+
readonly getDropPoint: () => PointerWorldPos;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
interface ActiveToolContextValue {
|
|
2465
|
+
active: string;
|
|
2466
|
+
hotkeyStack: string[];
|
|
2467
|
+
setActive(id: string): void;
|
|
2468
|
+
pushHotkey(id: string): void;
|
|
2469
|
+
popHotkey(): void;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* `enterTextEditAction` — immediate Action descriptor for entering in-place
|
|
2474
|
+
* text editing on a selected text node.
|
|
2475
|
+
*
|
|
2476
|
+
* ## Status: REAL
|
|
2477
|
+
*
|
|
2478
|
+
* Fires via `useTextTool.bindings` when the user clicks on a
|
|
2479
|
+
* selected text node. Calls `deps.textEdit.startEdit(id)` to activate the
|
|
2480
|
+
* contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.
|
|
2481
|
+
*
|
|
2482
|
+
* ## No defaultBinding / defaultBinding
|
|
2483
|
+
*
|
|
2484
|
+
* This action has no ambient key or gesture binding — it fires ONLY via
|
|
2485
|
+
* `useTextTool`'s `Tool.bindings` entry:
|
|
2486
|
+
*
|
|
2487
|
+
* ```ts
|
|
2488
|
+
* bindings: [
|
|
2489
|
+
* { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },
|
|
2490
|
+
* ]
|
|
2491
|
+
* ```
|
|
2492
|
+
*
|
|
2493
|
+
* Keeping it binding-free avoids ambient double-fire and scopes the action to
|
|
2494
|
+
* the text tool context where `classifyTarget` is already wired.
|
|
2495
|
+
*
|
|
2496
|
+
* ## Self-guard: only act on text nodes
|
|
2497
|
+
*
|
|
2498
|
+
* The `'selected-body'` target yields a match for any selected node kind. To
|
|
2499
|
+
* avoid entering text-edit mode when the text tool happens to have a non-text
|
|
2500
|
+
* node selected, the action self-guards via an optional `isTextNode` predicate
|
|
2501
|
+
* on `TextEditDep`:
|
|
2502
|
+
*
|
|
2503
|
+
* - When `isTextNode` is absent: action fires unconditionally (the binding
|
|
2504
|
+
* spec is the real gate — consumers should only bind this action from the
|
|
2505
|
+
* text tool).
|
|
2506
|
+
* - When `isTextNode(id)` returns `false`: action is a no-op for that node.
|
|
2507
|
+
*
|
|
2508
|
+
* ### Pre-filtering at dispatch time
|
|
2509
|
+
*
|
|
2510
|
+
* `classifyTarget` now surfaces node kind, so a binding can pre-filter instead
|
|
2511
|
+
* of relying on the self-guard:
|
|
2512
|
+
*
|
|
2513
|
+
* ```ts
|
|
2514
|
+
* { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }
|
|
2515
|
+
* ```
|
|
2516
|
+
*
|
|
2517
|
+
* That reads the *routing trait's* kind, so it matches whatever names the
|
|
2518
|
+
* consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's
|
|
2519
|
+
* inferred default. `isTextNode` stays on `TextEditDep` because it also covers
|
|
2520
|
+
* consumers who bind the broader `'selected-body'` target, and because it is
|
|
2521
|
+
* the only guard for a consumer who opted out of routing entirely.
|
|
2522
|
+
*
|
|
2523
|
+
* ## Migration plan for useTextTool
|
|
2524
|
+
*
|
|
2525
|
+
* When wiring `useTextTool` to `Tool.bindings`:
|
|
2526
|
+
*
|
|
2527
|
+
* 1. Add to `useTextTool`'s `bindings`:
|
|
2528
|
+
* ```ts
|
|
2529
|
+
* { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }
|
|
2530
|
+
* ```
|
|
2531
|
+
* 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`
|
|
2532
|
+
* return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`
|
|
2533
|
+
* (or however the consumer identifies text nodes).
|
|
2534
|
+
* 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes
|
|
2535
|
+
* redundant — remove it in the same pass.
|
|
2536
|
+
*/
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* Dep for `enterTextEditAction`.
|
|
2540
|
+
*
|
|
2541
|
+
* Wrap the return value of `useTextEdit` / `useSceneTextEdit` to source this
|
|
2542
|
+
* dep. The `isTextNode` predicate is optional — when absent the action fires
|
|
2543
|
+
* unconditionally (the binding spec acts as the gate).
|
|
2544
|
+
*
|
|
2545
|
+
* @example
|
|
2546
|
+
* ```ts
|
|
2547
|
+
* const textEdit = useSceneTextEdit({ scene, container });
|
|
2548
|
+
* useDepSource('textEdit', () => ({
|
|
2549
|
+
* startEdit: textEdit.startEdit,
|
|
2550
|
+
* isTextNode: (id) => scene.get(id as NodeId)?.data?.kind === 'text',
|
|
2551
|
+
* }));
|
|
2552
|
+
* ```
|
|
2553
|
+
*/
|
|
2554
|
+
interface TextEditDep {
|
|
2555
|
+
/**
|
|
2556
|
+
* Begin editing the node with `id`. Activates the contenteditable overlay
|
|
2557
|
+
* managed by `useTextEdit` / `useSceneTextEdit`.
|
|
2558
|
+
*/
|
|
2559
|
+
startEdit(id: string, opts?: {
|
|
2560
|
+
caret?: number | 'all';
|
|
2561
|
+
}): void;
|
|
2562
|
+
/**
|
|
2563
|
+
* Optional predicate: returns `true` when the node with `id` is a text node.
|
|
2564
|
+
* When absent the action fires on any selected node (binding spec is the gate).
|
|
2565
|
+
* When present and returning `false`, the invocation is a no-op.
|
|
2566
|
+
*/
|
|
2567
|
+
isTextNode?(id: string): boolean;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/**
|
|
2571
|
+
* Consumer-supplied commit for the Slice action. `commit` receives the finite
|
|
2572
|
+
* slice segment (world coords); the consumer scans the scene, splits crossed
|
|
2573
|
+
* paths via `splitPathByLine`, and applies the result as one undoable batch.
|
|
2574
|
+
*/
|
|
2575
|
+
interface SliceDep {
|
|
2576
|
+
commit(a: Point2, b: Point2): void;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/**
|
|
2580
|
+
* Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.
|
|
2581
|
+
* The hook reads bounds via `getBounds`, runs anchor-relative math on those
|
|
2582
|
+
* bounds, then asks `remapBounds` to project the result back into TPose.
|
|
2583
|
+
*
|
|
2584
|
+
* `remapBounds(pose, src, dst)` is a single operation that subsumes both
|
|
2585
|
+
* "set my own AABB to dst" (single-leaf resize) and "scale me as a leaf
|
|
2586
|
+
* inside parent's src→dst rect" (group resize) — they're the same affine
|
|
2587
|
+
* map. For rect-shaped poses the default geometry interprets the pose as
|
|
2588
|
+
* its own bounds; for Path or polygon poses the consumer supplies a
|
|
2589
|
+
* projection that knows how to read and rewrite the underlying geometry.
|
|
2590
|
+
*/
|
|
2591
|
+
interface PoseProjection<TPose> {
|
|
2592
|
+
getBounds(pose: TPose): ResizePose;
|
|
2593
|
+
remapBounds(pose: TPose, src: ResizePose, dst: ResizePose): TPose;
|
|
2594
|
+
/** Translate the pose by (dx, dy). Optional — when omitted, callers fall
|
|
2595
|
+
* back to a translation derived from `remapBounds` (origin shifted, no
|
|
2596
|
+
* scale). Path-shaped poses should provide this for performance. */
|
|
2597
|
+
translate?(pose: TPose, dx: number, dy: number): TPose;
|
|
2598
|
+
/** True iff any portion of the pose's geometry intersects `rect`. Optional
|
|
2599
|
+
* — when omitted, area-select and similar callers test against `getBounds`
|
|
2600
|
+
* AABB (looser, but correct for axis-aligned rect poses). */
|
|
2601
|
+
intersectsRect?(pose: TPose, rect: ResizePose): boolean;
|
|
2602
|
+
/** Interpolate between two poses. Optional — animation helpers fall back to
|
|
2603
|
+
* rect-shape lerp when omitted (which fails for non-rect poses). */
|
|
2604
|
+
lerp?(a: TPose, b: TPose, t: number): TPose;
|
|
2605
|
+
/** Read the pose's rotation in radians. Pivot is the AABB center
|
|
2606
|
+
* (`getBounds(pose)` center). Default 0 when omitted — descriptor
|
|
2607
|
+
* declares "this pose has no rotation." When supplied and non-zero,
|
|
2608
|
+
* `useResize` projects the drag delta into the leaf's local frame,
|
|
2609
|
+
* runs anchor math there, and translates the resulting pose so the
|
|
2610
|
+
* diagonally opposite world-space corner is pinned. */
|
|
2611
|
+
getRotation?(pose: TPose): number;
|
|
2612
|
+
/** True iff this pose shape can carry a rotation. Consulted by the
|
|
2613
|
+
* rotation affordance to decide whether to render the rotate cursor /
|
|
2614
|
+
* drag-band over a selection. When omitted, the kit assumes `true` for
|
|
2615
|
+
* back-compat — descriptors whose poses lack `x/y/width/height/rotation`
|
|
2616
|
+
* fields (e.g. polygon Paths) should return `false` so the affordance
|
|
2617
|
+
* hides instead of exposing a non-functional rotate cursor. */
|
|
2618
|
+
supportsRotation?(pose: TPose): boolean;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* 2D affine transforms in canvas/DOMMatrix order: [a, b, c, d, e, f].
|
|
2623
|
+
* x' = a·x + c·y + e
|
|
2624
|
+
* y' = b·x + d·y + f
|
|
2625
|
+
* Represented as a 6-element number[] (f64). The affine tier of the kernel.
|
|
2626
|
+
*
|
|
2627
|
+
* Convention alignment: the renderer already has a `Mat3` in
|
|
2628
|
+
* `src/renderer/math/mat3.ts`. That one is a 9-element column-major
|
|
2629
|
+
* `Float32Array` (a full 3×3) shaped for `uniformMatrix3fv` — a deliberately
|
|
2630
|
+
* different *representation* for the WebGL upload path. Its *logical element
|
|
2631
|
+
* order* is identical to ours: `create(a, b, c, d, tx, ty)` maps
|
|
2632
|
+
* `x' = a·x + c·y + tx`, `y' = b·x + d·y + ty` (canvas/DOMMatrix a,b,c,d,e,f).
|
|
2633
|
+
* We keep the pure 6-tuple f64 form here (the kernel form); the 9-element f32
|
|
2634
|
+
* form stays a render-layer concern. No second logical convention is created.
|
|
2635
|
+
*/
|
|
2636
|
+
type Mat3 = number[];
|
|
2637
|
+
|
|
2638
|
+
/** Optional consumer seam: given a node and the affine `m` that a pose-transform
|
|
2639
|
+
* action applied to the node's POSE, return updated `data` with the node's
|
|
2640
|
+
* data-held geometry transformed by `m`, or `null` if this node has no
|
|
2641
|
+
* data-held geometry (the kit leaves `data` alone). */
|
|
2642
|
+
interface GeometryProjection {
|
|
2643
|
+
transform(node: {
|
|
2644
|
+
id?: string;
|
|
2645
|
+
data: unknown;
|
|
2646
|
+
pose: unknown;
|
|
2647
|
+
}, m: Mat3): unknown | null;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/** Minimal view API the action layer consumes. May be refined later. */
|
|
2651
|
+
interface ViewApi {
|
|
2652
|
+
get(): View;
|
|
2653
|
+
set(v: View): void;
|
|
2654
|
+
/** Optional recenter callback. When wired, `viewportZoomAction`'s
|
|
2655
|
+
* `reset` branch (Cmd-0) calls this instead of resetting to identity —
|
|
2656
|
+
* letting consumers re-fit the page (or other reference bounds) into
|
|
2657
|
+
* the workspace. Receives no args; the consumer reads its own bounds
|
|
2658
|
+
* + host dims and dispatches `setView(...)`. */
|
|
2659
|
+
recenter?(): void;
|
|
2660
|
+
/** Optional canvas-local host dimensions (CSS px). When wired,
|
|
2661
|
+
* `viewportZoomAction`'s keyboard branches (Cmd+= / Cmd+-) anchor at the
|
|
2662
|
+
* host center instead of the top-left origin. Null when the host isn't
|
|
2663
|
+
* measurable (unmounted). */
|
|
2664
|
+
hostSize?(): {
|
|
2665
|
+
width: number;
|
|
2666
|
+
height: number;
|
|
2667
|
+
} | null;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Adapter dep for `areaSelectAction`.
|
|
2671
|
+
*
|
|
2672
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>` via AABB
|
|
2673
|
+
* overlap over scene nodes. Consumers with custom hit-testing override this
|
|
2674
|
+
* dep entry in their own registrar.
|
|
2675
|
+
*/
|
|
2676
|
+
/**
|
|
2677
|
+
* Topmost-node-at-world-point dep, consumed by `moveAction` for
|
|
2678
|
+
* reparent-on-drop and available to any action that needs a single-best
|
|
2679
|
+
* pick. Mirrors the same hit-test plumbing `<SceneCanvas>` feeds to the
|
|
2680
|
+
* tool dispatcher; consumers with custom hit-testing override here.
|
|
2681
|
+
*
|
|
2682
|
+
* `exclude` is iterated once per call and treated as a set membership
|
|
2683
|
+
* test — the dep walks hits front-to-back and returns the first id not
|
|
2684
|
+
* in the exclude set. Pass moving-node roots + their descendants when
|
|
2685
|
+
* the caller wants to ignore the nodes it's manipulating.
|
|
2686
|
+
*/
|
|
2687
|
+
type NodeAtPointDep = (point: {
|
|
2688
|
+
x: number;
|
|
2689
|
+
y: number;
|
|
2690
|
+
}, exclude?: Iterable<NodeId>) => NodeId | null;
|
|
2691
|
+
interface AreaSelectDep {
|
|
2692
|
+
/** Return ids of all scene nodes whose AABB overlaps `bounds`. */
|
|
2693
|
+
hitTestArea(bounds: {
|
|
2694
|
+
x: number;
|
|
2695
|
+
y: number;
|
|
2696
|
+
width: number;
|
|
2697
|
+
height: number;
|
|
2698
|
+
}): NodeId[];
|
|
2699
|
+
/** Return the current selection id list. */
|
|
2700
|
+
getSelection(): NodeId[];
|
|
2701
|
+
/** Replace the current selection. */
|
|
2702
|
+
setSelection(ids: NodeId[]): void;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Adapter dep for `editAnchorsAction`.
|
|
2706
|
+
*
|
|
2707
|
+
* Provides narrow read/write access to the editable polygon for a single
|
|
2708
|
+
* node. Consumers register this dep so anchor-edit actions can read/write
|
|
2709
|
+
* the polygon WITHOUT knowing whether it lives directly on the node's
|
|
2710
|
+
* pose (`pose.kind === 'polygon'`) or on `node.data.path` (the kit's
|
|
2711
|
+
* built-in pen-tool default, also WeaselDraw's shape).
|
|
2712
|
+
*
|
|
2713
|
+
* Note on live previews: in-flight edit state is surfaced through the
|
|
2714
|
+
* dispatcher's standard `OngoingHandle.previewIds/previewPose/previewData`
|
|
2715
|
+
* triple (not this dep), so chrome and preview-ghost stay in lock-step
|
|
2716
|
+
* via one source of truth.
|
|
2717
|
+
*/
|
|
2718
|
+
interface EditAnchorsDep {
|
|
2719
|
+
/** Id of the node currently being edited. Empty string means no node is
|
|
2720
|
+
* currently in edit mode — the chrome and gesture both opt out. */
|
|
2721
|
+
editingId: string;
|
|
2722
|
+
/** Enter/exit edit mode for a specific node. Pass `null` (or an empty
|
|
2723
|
+
* string) to exit. `enterPathEditAction` and `exitPathEditAction` call
|
|
2724
|
+
* this; consumers can call it directly to drive edit mode programmatically. */
|
|
2725
|
+
setEditingId(id: string | null): void;
|
|
2726
|
+
/** Returns the COMMITTED editable polygon in world coordinates, or
|
|
2727
|
+
* null if this node has no editable polygon. Does NOT consult in-
|
|
2728
|
+
* flight previews — callers that need live state read the dispatcher's
|
|
2729
|
+
* in-flight handles. */
|
|
2730
|
+
getEditablePath(id: string): unknown;
|
|
2731
|
+
/** Returns where the polygon is stored — `'pose'` when `node.pose`
|
|
2732
|
+
* IS the polygon, `'data'` when it lives on `node.data.path` with a
|
|
2733
|
+
* rect pose, or `null` when the node has no editable polygon. The
|
|
2734
|
+
* action uses this to know which preview-ghost axis to populate
|
|
2735
|
+
* (`previewPose` only / `previewData` + `previewPose` for data.path). */
|
|
2736
|
+
getStorageKind(id: string): 'pose' | 'data' | null;
|
|
2737
|
+
/** Returns the node's raw `pose` and `data` so storage-aware actions
|
|
2738
|
+
* can capture origin state at gesture-start and synthesize a matching
|
|
2739
|
+
* `previewPose` / `previewData` during `onMove`. Used by
|
|
2740
|
+
* `editAnchorsAction` for the data.path branch (rect pose + data
|
|
2741
|
+
* carrying extra fields like fill / stroke that must be preserved
|
|
2742
|
+
* through the preview). Returns null when the node is gone. */
|
|
2743
|
+
getNodeShape(id: string): {
|
|
2744
|
+
pose: unknown;
|
|
2745
|
+
data: unknown;
|
|
2746
|
+
} | null;
|
|
2747
|
+
/** Commit `worldPath` as the new value for `id`. Implementation routes
|
|
2748
|
+
* to setPose (when pose IS the polygon) or batched setPose+update
|
|
2749
|
+
* (when the polygon lives on data.path). Records one history entry
|
|
2750
|
+
* labelled `label`. */
|
|
2751
|
+
applyEdit(id: string, worldPath: unknown, label: string): void;
|
|
2752
|
+
/**
|
|
2753
|
+
* Anchors currently selected within the edited path, as **flat anchor
|
|
2754
|
+
* indices** — the same numbering `enumerateAnchors` produces and the
|
|
2755
|
+
* `anchor:N` affordance kinds carry.
|
|
2756
|
+
*
|
|
2757
|
+
* Selection is transient UI state, deliberately not part of the scene:
|
|
2758
|
+
* it is cleared whenever `editingId` changes, and any edit that
|
|
2759
|
+
* renumbers anchors (insert, delete) is responsible for leaving it
|
|
2760
|
+
* coherent. Empty means "no anchor selected" — the keyboard actions
|
|
2761
|
+
* (nudge, delete) no-op rather than acting on all anchors, matching
|
|
2762
|
+
* Illustrator.
|
|
2763
|
+
*/
|
|
2764
|
+
selectedAnchors: ReadonlySet<number>;
|
|
2765
|
+
/** Replace the anchor selection. Pass an empty iterable to clear. */
|
|
2766
|
+
setSelectedAnchors(next: Iterable<number>): void;
|
|
2767
|
+
/**
|
|
2768
|
+
* In-flight anchor-marquee rect in world coords, or null when no
|
|
2769
|
+
* marquee drag is active. Written by `marqueeAnchorsAction` and read by
|
|
2770
|
+
* the path-editing overlay — the same "ongoing action owns the preview,
|
|
2771
|
+
* chrome just draws it" split the move/resize ghosts use.
|
|
2772
|
+
*/
|
|
2773
|
+
marquee: {
|
|
2774
|
+
x: number;
|
|
2775
|
+
y: number;
|
|
2776
|
+
width: number;
|
|
2777
|
+
height: number;
|
|
2778
|
+
} | null;
|
|
2779
|
+
/** Set or clear the in-flight marquee rect. */
|
|
2780
|
+
setMarquee(rect: {
|
|
2781
|
+
x: number;
|
|
2782
|
+
y: number;
|
|
2783
|
+
width: number;
|
|
2784
|
+
height: number;
|
|
2785
|
+
} | null): void;
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Adapter dep for `lassoSelectAction`.
|
|
2789
|
+
*
|
|
2790
|
+
* Provides polygon-lasso hit-testing + selection read/write.
|
|
2791
|
+
* Consumers that don't implement `hitTestLasso` can omit it; the action
|
|
2792
|
+
* falls back to a bounding-box AABB test via `hitTestArea`.
|
|
2793
|
+
*/
|
|
2794
|
+
interface LassoSelectDep {
|
|
2795
|
+
/**
|
|
2796
|
+
* Hit-test against a closed polygon (vertex order CW or CCW; last→first
|
|
2797
|
+
* closing edge is implicit). Returns matching node ids.
|
|
2798
|
+
* Optional — when absent, `lassoSelectAction` falls back to AABB via
|
|
2799
|
+
* `hitTestArea`.
|
|
2800
|
+
*/
|
|
2801
|
+
hitTestLasso?(polygon: ReadonlyArray<{
|
|
2802
|
+
x: number;
|
|
2803
|
+
y: number;
|
|
2804
|
+
}>, mode: 'centers' | 'intersect' | 'enclosed'): string[];
|
|
2805
|
+
/** Return ids of nodes whose AABB overlaps the given rect (fallback). */
|
|
2806
|
+
hitTestArea(bounds: {
|
|
2807
|
+
x: number;
|
|
2808
|
+
y: number;
|
|
2809
|
+
width: number;
|
|
2810
|
+
height: number;
|
|
2811
|
+
}): string[];
|
|
2812
|
+
/** Return the current selection id list. */
|
|
2813
|
+
getSelection(): string[];
|
|
2814
|
+
/** Replace the current selection. */
|
|
2815
|
+
setSelection(ids: string[]): void;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Options for the kit `image/svg+xml` content handler, threaded from
|
|
2819
|
+
* SceneCanvas's `ingestion={{ svg }}` prop.
|
|
2820
|
+
*/
|
|
2821
|
+
interface SvgIngestOptions {
|
|
2822
|
+
/** Parse dropped/pasted/picked SVG files into native scene nodes (path /
|
|
2823
|
+
* text leaves under containers mirroring the source `<g>` structure)
|
|
2824
|
+
* instead of the default single embedded-image node.
|
|
2825
|
+
*
|
|
2826
|
+
* Pass `unpackSvgFiles` from `@weasel-js/svg`:
|
|
2827
|
+
*
|
|
2828
|
+
* ```ts
|
|
2829
|
+
* import { unpackSvgFiles } from '@weasel-js/svg';
|
|
2830
|
+
* <SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }} />
|
|
2831
|
+
* ```
|
|
2832
|
+
*
|
|
2833
|
+
* It is injected rather than flagged on with `true` because the SVG parser
|
|
2834
|
+
* lives in `@weasel-js/svg`, which depends on this package — core importing
|
|
2835
|
+
* it back would make the two mutually dependent and unpublishable
|
|
2836
|
+
* separately. Passing the function keeps the parser out of core's bundle
|
|
2837
|
+
* for consumers who never unpack. */
|
|
2838
|
+
unpack?: SvgUnpacker;
|
|
2839
|
+
}
|
|
2840
|
+
/** Parses SVG files and inserts the resulting nodes into `ctx.scene`, as one
|
|
2841
|
+
* `applyOps` batch per file. Implemented by `unpackSvgFiles` in
|
|
2842
|
+
* `@weasel-js/svg`; see {@link SvgIngestOptions.unpack}. */
|
|
2843
|
+
type SvgUnpacker = (files: File[], ctx: IngestCtx) => Promise<void>;
|
|
2844
|
+
/**
|
|
2845
|
+
* Clipboard-paste seam consumed by the kit weasel-JSON content handler
|
|
2846
|
+
* (`IngestCtx.clipboard`). Built by `<SceneCanvas>` from its own synthesized
|
|
2847
|
+
* adapter + the `ingestion.clipboard` prop; absent when the consumer set
|
|
2848
|
+
* `ingestion.clipboard.enabled === false` or the adapter lacks `commitPaste`.
|
|
2849
|
+
* Absence makes the handler decline inert (dwarn, nothing ingested) — its
|
|
2850
|
+
* matched items were already consumed at match time and do not fall through
|
|
2851
|
+
* to other handlers.
|
|
2852
|
+
*/
|
|
2853
|
+
interface ClipboardIngestCtx {
|
|
2854
|
+
/** The hosting canvas's adapter — `commitPaste` materializes the pasted
|
|
2855
|
+
* nodes (fresh ids, offset applied); insertion still goes through ops. */
|
|
2856
|
+
adapter: InsertAdapter<{
|
|
2857
|
+
id: string;
|
|
2858
|
+
}>;
|
|
2859
|
+
/** JSON reviver for the weasel wire payload (typed arrays etc.) — from
|
|
2860
|
+
* `SceneCanvasProps.ingestion.clipboard.reviver`. */
|
|
2861
|
+
reviver?: (key: string, value: unknown) => unknown;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Dep for the `ingest` action (external-content ingestion).
|
|
2865
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
2866
|
+
* `useIngestionDepSource` — canvas rect + current view.
|
|
2867
|
+
*/
|
|
2868
|
+
interface IngestionDep {
|
|
2869
|
+
/** Visible canvas area in world coordinates. */
|
|
2870
|
+
viewportWorldRect(): {
|
|
2871
|
+
x: number;
|
|
2872
|
+
y: number;
|
|
2873
|
+
width: number;
|
|
2874
|
+
height: number;
|
|
2875
|
+
};
|
|
2876
|
+
/** Consumer file→src resolver (from SceneCanvas's `ingestion` prop).
|
|
2877
|
+
* Live accessor — read it at use time. Destructuring (or copying the
|
|
2878
|
+
* property early) snapshots the current value and won't track later
|
|
2879
|
+
* prop changes across an `await`. */
|
|
2880
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
2881
|
+
/** Kit SVG-handler options (from SceneCanvas's `ingestion` prop).
|
|
2882
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
2883
|
+
svg?: SvgIngestOptions;
|
|
2884
|
+
/** Clipboard-paste seam for the kit weasel-JSON handler.
|
|
2885
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
2886
|
+
clipboard?: ClipboardIngestCtx;
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Per-kind extra geometry passed to `InsertDep.commit`.
|
|
2890
|
+
*
|
|
2891
|
+
* Built-in tools populate a typed variant so the kit's default factory can
|
|
2892
|
+
* render the true tool params (line endpoints, polygon side count, star
|
|
2893
|
+
* geometry, pencil sample list). Consumer-defined tools may pass any
|
|
2894
|
+
* `{ kind: string; ... }` payload; the kit's factory falls back to AABB
|
|
2895
|
+
* inscription for unknown kinds.
|
|
2896
|
+
*
|
|
2897
|
+
* `bounds` is still passed alongside as a useful AABB pose hint — factories
|
|
2898
|
+
* may use it as the node's pose even when richer geometry is available.
|
|
2899
|
+
*/
|
|
2900
|
+
type InsertExtras = {
|
|
2901
|
+
kind: 'rect';
|
|
2902
|
+
} | {
|
|
2903
|
+
kind: 'ellipse';
|
|
2904
|
+
} | {
|
|
2905
|
+
kind: 'line';
|
|
2906
|
+
a: {
|
|
2907
|
+
x: number;
|
|
2908
|
+
y: number;
|
|
2909
|
+
};
|
|
2910
|
+
b: {
|
|
2911
|
+
x: number;
|
|
2912
|
+
y: number;
|
|
2913
|
+
};
|
|
2914
|
+
} | {
|
|
2915
|
+
kind: 'polygon';
|
|
2916
|
+
sides: number;
|
|
2917
|
+
rotation: number;
|
|
2918
|
+
center?: {
|
|
2919
|
+
x: number;
|
|
2920
|
+
y: number;
|
|
2921
|
+
};
|
|
2922
|
+
radius?: number;
|
|
2923
|
+
} | {
|
|
2924
|
+
kind: 'star';
|
|
2925
|
+
points: number;
|
|
2926
|
+
innerRadiusRatio: number;
|
|
2927
|
+
rotation: number;
|
|
2928
|
+
center?: {
|
|
2929
|
+
x: number;
|
|
2930
|
+
y: number;
|
|
2931
|
+
};
|
|
2932
|
+
outerRadius?: number;
|
|
2933
|
+
} | {
|
|
2934
|
+
kind: 'pencil';
|
|
2935
|
+
samples: ReadonlyArray<DragSample>;
|
|
2936
|
+
} | {
|
|
2937
|
+
kind: 'text';
|
|
2938
|
+
text?: string;
|
|
2939
|
+
} | {
|
|
2940
|
+
kind: string;
|
|
2941
|
+
[extra: string]: unknown;
|
|
2942
|
+
};
|
|
2943
|
+
/**
|
|
2944
|
+
* World-space point snapping — grid, guides, or any consumer rule.
|
|
2945
|
+
*
|
|
2946
|
+
* Sourced by `<SceneCanvas>` from its `toolOptions.snapPoint`. Actions apply
|
|
2947
|
+
* it to the coords they ingest so the live preview and the committed
|
|
2948
|
+
* geometry agree; `insertAction` snaps the drag's start and current point.
|
|
2949
|
+
*
|
|
2950
|
+
* Optional: when the dep is absent, actions treat it as identity.
|
|
2951
|
+
*/
|
|
2952
|
+
interface SnapDep {
|
|
2953
|
+
/** Snap a world-space point. Return `p` unchanged to opt out. */
|
|
2954
|
+
point(p: {
|
|
2955
|
+
x: number;
|
|
2956
|
+
y: number;
|
|
2957
|
+
}): {
|
|
2958
|
+
x: number;
|
|
2959
|
+
y: number;
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Adapter dep for `insertAction`.
|
|
2964
|
+
*
|
|
2965
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>`. The `extras`
|
|
2966
|
+
* carry the active tool's kind + per-kind geometry. Callers
|
|
2967
|
+
* that need typed data must supply a richer `insert` dep.
|
|
2968
|
+
*/
|
|
2969
|
+
interface InsertDep {
|
|
2970
|
+
/**
|
|
2971
|
+
* Materialise a new node from the given drag-rect bounds and typed
|
|
2972
|
+
* per-kind extras. Returns the new node's id, or `null` if the consumer
|
|
2973
|
+
* rejected the insert (e.g. sub-threshold bounds, unknown kind).
|
|
2974
|
+
*/
|
|
2975
|
+
commit(bounds: {
|
|
2976
|
+
x: number;
|
|
2977
|
+
y: number;
|
|
2978
|
+
width: number;
|
|
2979
|
+
height: number;
|
|
2980
|
+
}, extras: InsertExtras): NodeId | null;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Adapter dep for `resizeAction`.
|
|
2984
|
+
*
|
|
2985
|
+
* Carries the four behavior-shaping options the legacy `useResize` hook
|
|
2986
|
+
* exposed through `UseResizeOptions`: bounds-frame behaviors (e.g.
|
|
2987
|
+
* `lockAspectWithModifier`), world-space anchor-point snap behaviors (e.g.
|
|
2988
|
+
* `pointSnapToGrid`), group-expansion (`expandIds`), and pose↔bounds
|
|
2989
|
+
* projection (`geometry`).
|
|
2990
|
+
*
|
|
2991
|
+
* Optional in `DepSchema`: when absent, `resizeAction` falls back to
|
|
2992
|
+
* identity defaults (no behaviors, identity expandIds, `RECT_POSE_DESCRIPTOR`
|
|
2993
|
+
* geometry). Consumers wire the dep via `useDepSource('resizePolicy', ...)`
|
|
2994
|
+
* from any descendant of `<DepRegistryProvider>` / `<SceneCanvas>`.
|
|
2995
|
+
*
|
|
2996
|
+
* The generic is erased to `unknown` at the schema entry; consumers cast at
|
|
2997
|
+
* the call site (mirrors the `scene` entry's convention).
|
|
2998
|
+
*/
|
|
2999
|
+
interface ResizePolicy<TPose> {
|
|
3000
|
+
/** Bounds-frame constraints. Constrained to `TPose extends ResizePose` since
|
|
3001
|
+
* constraints read/write `{x,y,width,height}`. For non-rect TPose pass `[]`. */
|
|
3002
|
+
constraints: TPose extends ResizePose ? BoundsConstraint<TPose>[] : never[];
|
|
3003
|
+
/** World-space anchor-point snap behaviors. Same TPose constraint as
|
|
3004
|
+
* `constraints`. */
|
|
3005
|
+
pointSnap: TPose extends ResizePose ? PointSnapBehavior<TPose>[] : never[];
|
|
3006
|
+
/** Group-expansion at gesture start. Identity (`ids => ids`) when group
|
|
3007
|
+
* resize isn't wanted. */
|
|
3008
|
+
expandIds: (ids: string[]) => string[];
|
|
3009
|
+
/** Projection from `TPose` to bounds and back. Use `RECT_POSE_DESCRIPTOR`
|
|
3010
|
+
* for plain rect poses. */
|
|
3011
|
+
projection: PoseProjection<TPose>;
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Layout-strategy lookup by container id, consumed by `moveAction` to run
|
|
3015
|
+
* the drag-time reflow pass. Sourced by `<SceneCanvas>` from its `layouts`
|
|
3016
|
+
* prop. Optional: `getLayout` returns null for any container when no layout
|
|
3017
|
+
* is configured, so the reflow pass is a no-op then.
|
|
3018
|
+
*/
|
|
3019
|
+
interface LayoutDep {
|
|
3020
|
+
getLayout(containerId: string): LayoutStrategy<unknown> | null;
|
|
3021
|
+
}
|
|
3022
|
+
interface DepSchema {
|
|
3023
|
+
/** Kit selection state — ids of currently selected nodes. */
|
|
3024
|
+
selection: SelectionApi;
|
|
3025
|
+
/** Current viewport — camera position + scale. */
|
|
3026
|
+
view: ViewApi;
|
|
3027
|
+
/**
|
|
3028
|
+
* Scene tree — structural reads + undoable mutations.
|
|
3029
|
+
*
|
|
3030
|
+
* The entry uses the fully-erased form `Scene<unknown, string, unknown>`
|
|
3031
|
+
* because `DepSchema` must be concrete. Actions that need a typed scene
|
|
3032
|
+
* should cast: `deps.scene as Scene<MyData, MyLayer, MyPose>`.
|
|
3033
|
+
*/
|
|
3034
|
+
scene: Scene<unknown, string, unknown>;
|
|
3035
|
+
/** Undo/redo history bound to the current scene. */
|
|
3036
|
+
history: History;
|
|
3037
|
+
/**
|
|
3038
|
+
* Canvas pointer position in world space.
|
|
3039
|
+
*
|
|
3040
|
+
* Exposes `pointerRef` (mutable live ref) and `getDropPoint()` thunk.
|
|
3041
|
+
* Marked `@experimental` in the source.
|
|
3042
|
+
*/
|
|
3043
|
+
pointer: PointerContextValue;
|
|
3044
|
+
/** Currently active tool id + hotkey-hold stack. */
|
|
3045
|
+
activeTool: ActiveToolContextValue;
|
|
3046
|
+
/**
|
|
3047
|
+
* Area-select dep — AABB hit-test + selection read/write.
|
|
3048
|
+
*
|
|
3049
|
+
* Sourced from `<SceneCanvas>` via AABB overlap over all scene
|
|
3050
|
+
* nodes. Override per-consumer for custom hit-testing (e.g. contain-mode,
|
|
3051
|
+
* lock-aware filtering).
|
|
3052
|
+
*/
|
|
3053
|
+
areaSelect: AreaSelectDep;
|
|
3054
|
+
/**
|
|
3055
|
+
* Topmost node at a world-space point. Sourced by `<SceneCanvas>` from
|
|
3056
|
+
* the same picker that feeds the tool dispatcher's `getNodeAtPoint`.
|
|
3057
|
+
* Optional: actions that read this (e.g. `moveAction` reparent-on-drop)
|
|
3058
|
+
* fall back to a no-op when the dep isn't registered.
|
|
3059
|
+
*/
|
|
3060
|
+
nodeAtPoint?: NodeAtPointDep;
|
|
3061
|
+
/**
|
|
3062
|
+
* Insert dep — node factory for drag-to-insert.
|
|
3063
|
+
*
|
|
3064
|
+
* Sourced from `<SceneCanvas>`. The `kind` param comes from
|
|
3065
|
+
* the active binding's `opts.params.kind`. Override per-consumer to
|
|
3066
|
+
* provide a typed node factory (e.g. with custom data payloads).
|
|
3067
|
+
*/
|
|
3068
|
+
insert: InsertDep;
|
|
3069
|
+
/**
|
|
3070
|
+
* Snap dep — world-space point snapping (grid / guides).
|
|
3071
|
+
*
|
|
3072
|
+
* Sourced by `<SceneCanvas>` from `toolOptions.snapPoint`. Optional:
|
|
3073
|
+
* absent means no snapping (identity).
|
|
3074
|
+
*/
|
|
3075
|
+
snap?: SnapDep;
|
|
3076
|
+
/**
|
|
3077
|
+
* Lasso-select dep — polygon hit-test + selection read/write.
|
|
3078
|
+
*
|
|
3079
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>`.
|
|
3080
|
+
* Falls back to AABB hit-test when `hitTestLasso` is absent.
|
|
3081
|
+
*/
|
|
3082
|
+
lassoSelect: LassoSelectDep;
|
|
3083
|
+
/**
|
|
3084
|
+
* Edit-anchors dep — narrow read/write of one polygon's path pose.
|
|
3085
|
+
*
|
|
3086
|
+
* Sourced from consumer. Wraps `getPose`/`setPose`/`applyOps`
|
|
3087
|
+
* for the currently-being-edited polygon node.
|
|
3088
|
+
*
|
|
3089
|
+
* The `editAnchorsAction` requires this dep to be registered when anchor
|
|
3090
|
+
* editing is active. If absent, `start` returns an empty handle (no-op).
|
|
3091
|
+
*/
|
|
3092
|
+
editAnchors: EditAnchorsDep;
|
|
3093
|
+
/**
|
|
3094
|
+
* Text-edit dep — activates the in-place text editing overlay.
|
|
3095
|
+
*
|
|
3096
|
+
* Sourced from consumer via `useTextEdit` / `useSceneTextEdit`.
|
|
3097
|
+
* The `enterTextEditAction` requires this dep to be registered by the text
|
|
3098
|
+
* tool when text editing is available.
|
|
3099
|
+
*
|
|
3100
|
+
* The optional `isTextNode` predicate guards against entering edit mode on
|
|
3101
|
+
* non-text nodes. A binding can pre-filter instead with a
|
|
3102
|
+
* `target: 'kind:text:selected'` spec; the guard remains for consumers who
|
|
3103
|
+
* bind the broader `'selected-body'` target or opted out of routing.
|
|
3104
|
+
*/
|
|
3105
|
+
textEdit: TextEditDep;
|
|
3106
|
+
/**
|
|
3107
|
+
* Resize-policy dep — bounds constraints, point-snap behaviors,
|
|
3108
|
+
* group expansion, and pose↔bounds projection for `resizeAction`.
|
|
3109
|
+
*
|
|
3110
|
+
* Optional: when omitted, `resizeAction` falls back to identity defaults
|
|
3111
|
+
* (no constraints, no snap, identity expandIds, `RECT_POSE_DESCRIPTOR`).
|
|
3112
|
+
* Consumers wire via `useDepSource('resizePolicy', ...)` or the
|
|
3113
|
+
* `useResizePolicy` helper.
|
|
3114
|
+
*/
|
|
3115
|
+
resizePolicy?: ResizePolicy<unknown>;
|
|
3116
|
+
/**
|
|
3117
|
+
* Booleans adapter — read selection ids, fetch world-space `Path`s,
|
|
3118
|
+
* compare z-order, and mint result nodes for Pathfinder ops.
|
|
3119
|
+
*
|
|
3120
|
+
* Consumers wire via `useBooleansAdapter(adapter)` (a thin wrapper
|
|
3121
|
+
* around `useDepSource('booleansAdapter', ...)`). The descriptor's
|
|
3122
|
+
* `enabled` predicate reads `deps.selection` for the count check; the
|
|
3123
|
+
* invoker reads `deps.booleansAdapter` to execute the op.
|
|
3124
|
+
*/
|
|
3125
|
+
booleansAdapter?: BooleansAdapter;
|
|
3126
|
+
/**
|
|
3127
|
+
* Gesture dispatcher control surface — exposes `cancelAll(reason)` so
|
|
3128
|
+
* actions that need to abort an in-flight handle (Escape cancels a
|
|
3129
|
+
* drag, etc.) can do so. Sourced by `<SceneCanvas>` from the
|
|
3130
|
+
* dispatcher instance it already owns.
|
|
3131
|
+
*/
|
|
3132
|
+
dispatcher?: {
|
|
3133
|
+
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
3134
|
+
};
|
|
3135
|
+
/**
|
|
3136
|
+
* Layout-strategy lookup. Sourced by `<SceneCanvas>` from `layouts`.
|
|
3137
|
+
* Optional: absent (or all-null) → `moveAction` skips reflow.
|
|
3138
|
+
*/
|
|
3139
|
+
layout?: LayoutDep;
|
|
3140
|
+
/**
|
|
3141
|
+
* Slice dep — consumer-supplied commit for the Slice action.
|
|
3142
|
+
*
|
|
3143
|
+
* Receives the finite slice segment in world coordinates; the consumer
|
|
3144
|
+
* scans the scene, splits crossed paths via `splitPathByLine`, and
|
|
3145
|
+
* applies the result as one undoable batch.
|
|
3146
|
+
*
|
|
3147
|
+
* Optional: when absent, `sliceAction` is a no-op.
|
|
3148
|
+
*/
|
|
3149
|
+
slice?: SliceDep;
|
|
3150
|
+
/**
|
|
3151
|
+
* Optional consumer commit hook. When present, `moveAction` (and other
|
|
3152
|
+
* default actions) submit their committed ops through it instead of
|
|
3153
|
+
* `scene.applyBatch`, so apps with their own history integration
|
|
3154
|
+
* (checkpoint + push entry) capture the gesture as one undo entry.
|
|
3155
|
+
* When absent, commits fall back to `scene.applyBatch`.
|
|
3156
|
+
*/
|
|
3157
|
+
applyOps?: (ops: Op[], label: string) => void;
|
|
3158
|
+
/** Optional pose-composition strategy for hierarchical (local-pose) scenes.
|
|
3159
|
+
* When absent, defaults to IDENTITY (absolute-pose: nodes store world
|
|
3160
|
+
* coords). Local-pose consumers supply { compose: composeRectPose,
|
|
3161
|
+
* decompose: decomposeRectPose } (or their pose shape's equivalent). */
|
|
3162
|
+
poseComposition?: PoseComposition<unknown>;
|
|
3163
|
+
/**
|
|
3164
|
+
* Ingestion dep — canvas viewport rect + consumer file→src resolver.
|
|
1594
3165
|
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
|
|
3166
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
3167
|
+
* `useIngestionDepSource`. Feeds `ingestAction` with the world-space
|
|
3168
|
+
* viewport rect for paste-placement and image fit-clamping, and forwards
|
|
3169
|
+
* the consumer's optional `resolveSrc` seam.
|
|
3170
|
+
*
|
|
3171
|
+
* Optional: when absent, the `ingest` action no-ops (there is no
|
|
3172
|
+
* placement geometry to work with).
|
|
3173
|
+
*/
|
|
3174
|
+
ingestion?: IngestionDep;
|
|
3175
|
+
/**
|
|
3176
|
+
* Optional consumer seam for the eager-sync layer: lets pose-transform
|
|
3177
|
+
* actions (resize/move/nudge/flip — NOT rotate) ALSO rewrite a node's
|
|
3178
|
+
* data-held geometry. Given a node and the affine `m` applied to its pose,
|
|
3179
|
+
* `transform(node, m)` returns updated `data` (geometry mapped by `m`) or
|
|
3180
|
+
* `null` for nodes with no data-held geometry.
|
|
3181
|
+
*
|
|
3182
|
+
* Strictly opt-in: when absent (or when `transform` returns null), the kit
|
|
3183
|
+
* emits only the pose op and leaves `data` untouched. apps/draw wires this
|
|
3184
|
+
* to mirror `data.path` through `transformPath`. Rotate intentionally never
|
|
3185
|
+
* consults this seam (rotation lives on the pose, baked at render).
|
|
3186
|
+
*/
|
|
3187
|
+
geometryProjection?: GeometryProjection;
|
|
1602
3188
|
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Every dep name the registry knows about — derived from {@link DepSchema} so
|
|
3191
|
+
* the two can't drift.
|
|
3192
|
+
*
|
|
3193
|
+
* Declared here rather than beside the registry so that this `keyof` reference
|
|
3194
|
+
* resolves to the exported `DepSchema` declaration; from another module it
|
|
3195
|
+
* resolves to that module's import alias, which the API docs can't link.
|
|
3196
|
+
*/
|
|
3197
|
+
type DepName = keyof DepSchema;
|
|
1603
3198
|
|
|
1604
3199
|
/**
|
|
1605
3200
|
* GestureBinding — connects a GestureSpec to an Action id (with per-binding
|
|
@@ -1615,136 +3210,50 @@ interface GestureBinding {
|
|
|
1615
3210
|
opts?: BindingOpts;
|
|
1616
3211
|
}
|
|
1617
3212
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
};
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
};
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
/**
|
|
1661
|
-
* Configurable activation-key descriptor for tools that expose their
|
|
1662
|
-
* keybinding to the host (currently Lasso and Eyedropper). Captures
|
|
1663
|
-
* only the fields meaningful to a caller-supplied tool-select key —
|
|
1664
|
-
* dispatcher-internal fields (`skipInEditable`, `enabled`,
|
|
1665
|
-
* `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
|
|
1666
|
-
* not part of the configurable surface.
|
|
1667
|
-
*/
|
|
1668
|
-
interface ToolKeybinding {
|
|
1669
|
-
/** Key or list of keys to match (case-insensitive against `event.key`). */
|
|
1670
|
-
key: string | readonly string[];
|
|
1671
|
-
/** Require Cmd (mac) / Ctrl (others). Default `false`. */
|
|
1672
|
-
mod?: boolean;
|
|
1673
|
-
/** Require Alt. Default `false`. */
|
|
1674
|
-
alt?: boolean;
|
|
1675
|
-
/**
|
|
1676
|
-
* Shift policy. `undefined`/`false` forbids shift, `true` requires
|
|
1677
|
-
* shift, `'optional'` allows either.
|
|
1678
|
-
*/
|
|
1679
|
-
shift?: boolean | 'optional';
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
/** Common payload for any hit that references a scene node. */
|
|
1683
|
-
interface NodeRef {
|
|
1684
|
-
id: NodeId;
|
|
1685
|
-
pose: unknown;
|
|
1686
|
-
data: unknown;
|
|
1687
|
-
meta?: Record<string, unknown>;
|
|
1688
|
-
}
|
|
1689
|
-
/** No hit — pointer landed on the background. */
|
|
1690
|
-
interface EmptyHit {
|
|
1691
|
-
category: 'empty';
|
|
1692
|
-
kind: 'empty';
|
|
1693
|
-
}
|
|
1694
|
-
/** Hit on a scene node's body. */
|
|
1695
|
-
interface NodeHit extends NodeRef {
|
|
1696
|
-
category: 'node';
|
|
1697
|
-
kind: string;
|
|
1698
|
-
}
|
|
1699
|
-
/** Hit on a node's affordance chrome (handle, anchor, etc.). */
|
|
1700
|
-
interface AffordanceHit extends NodeRef {
|
|
1701
|
-
category: 'affordance';
|
|
1702
|
-
kind: string;
|
|
1703
|
-
}
|
|
1704
|
-
/** Hit on a tool-defined target (anchor, handle, segment, etc.) supplied
|
|
1705
|
-
* via the tool's `hitOverride`. The `kind` string is the tool's own
|
|
1706
|
-
* vocabulary; the dispatcher does not interpret it. */
|
|
1707
|
-
interface ToolHit {
|
|
1708
|
-
category: 'tool';
|
|
1709
|
-
kind: string;
|
|
1710
|
-
extra?: unknown;
|
|
3213
|
+
type HandleKind = 'corner' | 'rotation' | 'anchor';
|
|
3214
|
+
type HitShape = {
|
|
3215
|
+
kind: 'rect';
|
|
3216
|
+
x: number;
|
|
3217
|
+
y: number;
|
|
3218
|
+
width: number;
|
|
3219
|
+
height: number;
|
|
3220
|
+
rotation?: number;
|
|
3221
|
+
} | {
|
|
3222
|
+
kind: 'circle';
|
|
3223
|
+
cx: number;
|
|
3224
|
+
cy: number;
|
|
3225
|
+
r: number;
|
|
3226
|
+
} | {
|
|
3227
|
+
kind: 'path';
|
|
3228
|
+
d: Path2D;
|
|
3229
|
+
};
|
|
3230
|
+
interface DebugSink {
|
|
3231
|
+
recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
|
|
3232
|
+
recordHandle(id: string, position: {
|
|
3233
|
+
x: number;
|
|
3234
|
+
y: number;
|
|
3235
|
+
}, kind: HandleKind): void;
|
|
3236
|
+
recordBounds(id: string, bounds: {
|
|
3237
|
+
x: number;
|
|
3238
|
+
y: number;
|
|
3239
|
+
width: number;
|
|
3240
|
+
height: number;
|
|
3241
|
+
}): void;
|
|
3242
|
+
recordOrigin(id: string, point: {
|
|
3243
|
+
x: number;
|
|
3244
|
+
y: number;
|
|
3245
|
+
}): void;
|
|
3246
|
+
recordSnapCandidate(point: {
|
|
3247
|
+
x: number;
|
|
3248
|
+
y: number;
|
|
3249
|
+
}, accepted: boolean): void;
|
|
3250
|
+
recordLayer(id: string, label: string, space: 'world' | 'screen', index: number): void;
|
|
3251
|
+
/** Clears every non-snap array. Called at the start of each Canvas render. */
|
|
3252
|
+
beginFrame(): void;
|
|
3253
|
+
/** Clears the snap array. Called at gesture end. */
|
|
3254
|
+
clearSnap(): void;
|
|
1711
3255
|
}
|
|
1712
|
-
/** Full discriminated union — every routed action's `ctx.target`. */
|
|
1713
|
-
type HitResult = EmptyHit | NodeHit | AffordanceHit | ToolHit;
|
|
1714
3256
|
|
|
1715
|
-
/** Phase the route was resolved against. Mirrors the spec's two-phase
|
|
1716
|
-
* vocabulary — `initial` (idle, scratch null) or `engaged` (mid-gesture). */
|
|
1717
|
-
type RoutePhase = 'initial' | 'engaged';
|
|
1718
|
-
/** Gesture channel the route fired on. */
|
|
1719
|
-
type RouteGesture = GestureName;
|
|
1720
|
-
/** Snapshot of one route resolution, emitted by the factory on each
|
|
1721
|
-
* successful lookup. Captured by the dispatcher as the "last resolved
|
|
1722
|
-
* route" for debug-overlay consumers. */
|
|
1723
|
-
interface RouteResolvedInfo {
|
|
1724
|
-
toolId: string;
|
|
1725
|
-
phase: RoutePhase;
|
|
1726
|
-
gesture: RouteGesture;
|
|
1727
|
-
/** Argument captured at match time for arg-bearing gestures
|
|
1728
|
-
* (`wheel` direction, `keyDown`/`keyUp` key, `multiTouchTap` fingers).
|
|
1729
|
-
* Undefined for no-arg gestures. */
|
|
1730
|
-
arg: string | undefined;
|
|
1731
|
-
/** Route-table key that matched (post-precedence). E.g. 'rect:selected',
|
|
1732
|
-
* '*:selected', 'rect', '*', 'empty'. For function-form `drag` (no
|
|
1733
|
-
* table), this is '*'. For keyDown/keyUp, it's the key name ('Escape',
|
|
1734
|
-
* 'Enter', etc.). */
|
|
1735
|
-
matchedKey: string;
|
|
1736
|
-
modifiers: ModifierCombo;
|
|
1737
|
-
/** The full HitResult at resolution time (snapshot — safe to read). */
|
|
1738
|
-
target: HitResult;
|
|
1739
|
-
/** Monotonic timestamp (ms since page load via performance.now()).
|
|
1740
|
-
* Used for "resolved Nms ago" displays. */
|
|
1741
|
-
timestamp: number;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
/** Outcome of a channel handler. `'claim'` stops dispatch for this event;
|
|
1745
|
-
* `'pass'` lets the next slot try. Handlers that return nothing are
|
|
1746
|
-
* treated as `'pass'`. */
|
|
1747
|
-
type Decision = 'claim' | 'pass' | void;
|
|
1748
3257
|
/** Modifier-key snapshot at event dispatch time. `space` is included
|
|
1749
3258
|
* because tools commonly use space as a hotkey-slot trigger and may
|
|
1750
3259
|
* also want to read it as a flag mid-gesture. */
|
|
@@ -1763,11 +3272,6 @@ interface ToolCtx<TScratch = unknown> {
|
|
|
1763
3272
|
worldX: number;
|
|
1764
3273
|
worldY: number;
|
|
1765
3274
|
modifiers: ToolModifiers;
|
|
1766
|
-
/** Hit-test result for the current event. Populated by the dispatcher
|
|
1767
|
-
* before each handler call. Tools that don't use declarative routing
|
|
1768
|
-
* can ignore this. Optional for migration; will become
|
|
1769
|
-
* required once the routing migration is complete. */
|
|
1770
|
-
target?: HitResult;
|
|
1771
3275
|
selection: SelectionApi;
|
|
1772
3276
|
/** Adapter/scene access — opaque at this layer; tools that need it
|
|
1773
3277
|
* cast to a known shape. This layer doesn't constrain it. */
|
|
@@ -1797,40 +3301,8 @@ interface ToolCtx<TScratch = unknown> {
|
|
|
1797
3301
|
* rotation handle, etc.) lands in the same overlay as Canvas's own
|
|
1798
3302
|
* bounds/origin records. Tools should call this conditionally with `?.`. */
|
|
1799
3303
|
debug?: DebugSink;
|
|
1800
|
-
/** Kit-internal: route-resolution reporter. The dispatcher populates
|
|
1801
|
-
* this; the declarative routing factory calls it after each successful
|
|
1802
|
-
* resolveRoute() hit so the dispatcher can publish the last-resolved
|
|
1803
|
-
* snapshot to debug-overlay consumers. Underscore prefix signals
|
|
1804
|
-
* "do not consume in tool code." */
|
|
1805
|
-
__reportRoute?: (info: RouteResolvedInfo) => void;
|
|
1806
3304
|
scratch: TScratch;
|
|
1807
3305
|
}
|
|
1808
|
-
interface PointerChannel<TScratch> {
|
|
1809
|
-
onDown?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1810
|
-
onClick?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1811
|
-
}
|
|
1812
|
-
interface DragChannel<TScratch> {
|
|
1813
|
-
onStart?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1814
|
-
onMove?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1815
|
-
onEnd?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1816
|
-
onCancel?: (ctx: ToolCtx<TScratch>) => void;
|
|
1817
|
-
}
|
|
1818
|
-
interface KeyboardChannel<TScratch> {
|
|
1819
|
-
onDown?: (e: KeyboardEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1820
|
-
onUp?: (e: KeyboardEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1821
|
-
}
|
|
1822
|
-
interface WheelChannel<TScratch> {
|
|
1823
|
-
onWheel?: (e: WheelEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1824
|
-
}
|
|
1825
|
-
/** Double-tap (double-click) channel. Fires on the pointerup of the second
|
|
1826
|
-
* sub-threshold tap when it follows a previous sub-threshold tap within the
|
|
1827
|
-
* dispatcher's `dblTap.windowMs` and `dblTap.maxDistance` (CSS px). The
|
|
1828
|
-
* scratch handed to the handler is fresh — `dblTap` is not part of a drag
|
|
1829
|
-
* pipeline, so `initScratch()` runs immediately before the call. A `'claim'`
|
|
1830
|
-
* return suppresses the regular `pointer.onClick` for this gesture. */
|
|
1831
|
-
interface DblTapChannel<TScratch> {
|
|
1832
|
-
onTap?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
|
|
1833
|
-
}
|
|
1834
3306
|
/** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
|
|
1835
3307
|
* `Bounds` type — the optional `rotation` field carries through so a tool
|
|
1836
3308
|
* can report an oriented preview rect (e.g. mid-rotate). */
|
|
@@ -1867,6 +3339,22 @@ interface Tool<TScratch = unknown> {
|
|
|
1867
3339
|
* every implicit-or-declared tag (i.e. `normal` in the default preset).
|
|
1868
3340
|
*/
|
|
1869
3341
|
capabilities?: CapabilityTag[];
|
|
3342
|
+
/**
|
|
3343
|
+
* Actions this tool owns and needs registered while it is in the tools
|
|
3344
|
+
* registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
|
|
3345
|
+
* reference by id.
|
|
3346
|
+
*
|
|
3347
|
+
* Declared here rather than registered by the hook with `useAction`,
|
|
3348
|
+
* because tool hooks run wherever the consumer calls them — for
|
|
3349
|
+
* `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
|
|
3350
|
+
* `useActionsRegistry()` returns null and `useAction` silently no-ops. The
|
|
3351
|
+
* result was a binding pointing at an action id nothing had registered, so
|
|
3352
|
+
* the gesture fell through to whatever matched next (polygon's
|
|
3353
|
+
* wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
|
|
3354
|
+
* selection instead). `<ToolActionsMounter>` registers these from inside
|
|
3355
|
+
* the provider.
|
|
3356
|
+
*/
|
|
3357
|
+
actions?: Action[];
|
|
1870
3358
|
/** Optional caller-supplied key. Most built-in tools have their activation
|
|
1871
3359
|
* key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
|
|
1872
3360
|
* is for tools that want their activation key to be configurable by the
|
|
@@ -1878,25 +3366,6 @@ interface Tool<TScratch = unknown> {
|
|
|
1878
3366
|
initScratch?: () => TScratch;
|
|
1879
3367
|
onActivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1880
3368
|
onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1881
|
-
pointer?: PointerChannel<TScratch>;
|
|
1882
|
-
drag?: DragChannel<TScratch>;
|
|
1883
|
-
keyboard?: KeyboardChannel<TScratch>;
|
|
1884
|
-
wheel?: WheelChannel<TScratch>;
|
|
1885
|
-
/** Double-tap channel — fires when two sub-threshold taps land within
|
|
1886
|
-
* `dblTap.windowMs` / `dblTap.maxDistance` of each other. Lets tools
|
|
1887
|
-
* enter modal modes (e.g. select → edit-anchors) without consumers
|
|
1888
|
-
* attaching `onDoubleClick` to a wrapper DOM node. */
|
|
1889
|
-
dblTap?: DblTapChannel<TScratch>;
|
|
1890
|
-
/**
|
|
1891
|
-
* State-aware predicate. When true, this tool claims every pointerdown
|
|
1892
|
-
* and bypasses the affordance layer hit-test pipeline. Used by tools
|
|
1893
|
-
* in modal states (pen mid-path, text mid-edit) where affordance hits
|
|
1894
|
-
* would otherwise interrupt the in-progress gesture.
|
|
1895
|
-
*
|
|
1896
|
-
* Default: undefined (treated as false). Called once per pointerdown
|
|
1897
|
-
* with the tool's current ctx (scratch + view + modifiers).
|
|
1898
|
-
*/
|
|
1899
|
-
claimsAll?: (ctx: ToolCtx<TScratch>) => boolean;
|
|
1900
3369
|
cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
|
|
1901
3370
|
/** Presentation metadata for tool palettes. See `ToolPresentation`. */
|
|
1902
3371
|
presentation?: ToolPresentation<TScratch>;
|
|
@@ -1924,37 +3393,18 @@ interface Tool<TScratch = unknown> {
|
|
|
1924
3393
|
* nothing — typically gated on a scratch field like
|
|
1925
3394
|
* `if (!scratch.overlay) return`. */
|
|
1926
3395
|
overlay?: RenderLayer<unknown>;
|
|
1927
|
-
/** Declarative gesture
|
|
1928
|
-
* dispatcher consults while this tool is
|
|
1929
|
-
*
|
|
3396
|
+
/** Declarative gesture bindings — the tool's entire input surface. The
|
|
3397
|
+
* gesture dispatcher consults these at active scope while this tool is
|
|
3398
|
+
* active, and at hotkey scope while it is held. See
|
|
1930
3399
|
* `docs/superpowers/specs/2026-05-16-registry-unification-design.md`. */
|
|
1931
3400
|
bindings?: GestureBinding[];
|
|
1932
|
-
/**
|
|
1933
|
-
* Optional. When set, the dispatcher consults this before its built-in
|
|
1934
|
-
* node/empty hit-test. If it returns a value, that target replaces the
|
|
1935
|
-
* default `target` on the routed action's ctx. The string `target` is
|
|
1936
|
-
* the tool's own vocabulary — the dispatcher does not interpret it.
|
|
1937
|
-
*
|
|
1938
|
-
* Used for tools that need richer sub-object hit categories (e.g., pen
|
|
1939
|
-
* edit-mode's anchor/handle/segment vs the default node/empty).
|
|
1940
|
-
*/
|
|
1941
|
-
hitOverride?(ctx: {
|
|
1942
|
-
worldX: number;
|
|
1943
|
-
worldY: number;
|
|
1944
|
-
scratch: TScratch;
|
|
1945
|
-
view: View;
|
|
1946
|
-
modifiers: ToolModifiers;
|
|
1947
|
-
}): {
|
|
1948
|
-
target: string;
|
|
1949
|
-
extra?: unknown;
|
|
1950
|
-
} | null;
|
|
1951
3401
|
/** Reflection escape hatch: when this `Tool` was produced by `defineTool`,
|
|
1952
3402
|
* the source `ToolDef` is attached here so introspection consumers
|
|
1953
|
-
* (`
|
|
1954
|
-
* reflection demo) can
|
|
1955
|
-
*
|
|
1956
|
-
* may leave this undefined. Typed as `unknown` to keep this
|
|
1957
|
-
* importing the routing types — consumers cast at the use site. */
|
|
3403
|
+
* (`buildRouteRegistry`, `findConflicts`, the toolkit-builder UI, the
|
|
3404
|
+
* reflection demo) can read the authored form — `hookName` in particular,
|
|
3405
|
+
* which the runtime `Tool` doesn't carry. Tools constructed without
|
|
3406
|
+
* `defineTool` may leave this undefined. Typed as `unknown` to keep this
|
|
3407
|
+
* file from importing the routing types — consumers cast at the use site. */
|
|
1958
3408
|
def?: unknown;
|
|
1959
3409
|
}
|
|
1960
3410
|
/** Internal alias for "a Tool of any scratch type" — used in registries and
|
|
@@ -1965,161 +3415,187 @@ type AnyTool = Tool<any>;
|
|
|
1965
3415
|
|
|
1966
3416
|
/**
|
|
1967
3417
|
* @experimental
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
* state from what its region's binding already computed (anchor: 'br',
|
|
1975
|
-
* targetId: 'g1', etc.) so the tool's onStart doesn't re-derive it. */
|
|
1976
|
-
initialScratch?: TScratch;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
/**
|
|
1980
|
-
* Canvas size in CSS pixels — passed to `draw` for layers that anchor to
|
|
1981
|
-
* canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
|
|
1982
|
-
* supplies it explicitly so layers don't have to know about DPR.
|
|
3418
|
+
* A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
|
|
3419
|
+
* (no per-binding opts) or an object form that pairs a spec with
|
|
3420
|
+
* `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
|
|
3421
|
+
* Use the object form when two bindings for the same action differ only in
|
|
3422
|
+
* a runtime parameter — the dispatcher extracts `opts.params` and passes
|
|
3423
|
+
* them to `ImmediateInvoker.run` as its second argument.
|
|
1983
3424
|
*/
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
}
|
|
3425
|
+
type BoundGesture = GestureSpec | {
|
|
3426
|
+
spec: GestureSpec;
|
|
3427
|
+
opts: BindingOpts;
|
|
3428
|
+
};
|
|
1988
3429
|
/**
|
|
1989
|
-
*
|
|
1990
|
-
*
|
|
1991
|
-
* @template TData - The data object passed to each draw call.
|
|
3430
|
+
* @experimental
|
|
3431
|
+
* Single registered action. v1: one binding per action.
|
|
1992
3432
|
*/
|
|
1993
|
-
interface
|
|
1994
|
-
/** Unique identifier used in visibility maps and ordering arrays. */
|
|
3433
|
+
interface Action {
|
|
1995
3434
|
id: string;
|
|
1996
|
-
/** Human-readable name for UI toggles. */
|
|
1997
3435
|
label: string;
|
|
3436
|
+
/** The gesture-spec form of the binding, read by the gesture dispatcher.
|
|
3437
|
+
* May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
|
|
3438
|
+
* or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
|
|
3439
|
+
* `{ spec, opts }` — use the object form for parametric actions where two
|
|
3440
|
+
* bindings for the same action differ only by `opts.params` (e.g. `flip`
|
|
3441
|
+
* with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
|
|
3442
|
+
* passes them to `ImmediateInvoker.run` as its second argument. */
|
|
3443
|
+
defaultBinding?: GestureSpec | BoundGesture[];
|
|
3444
|
+
/** Names of the deps this action's invoker reads (keys of `DepSchema`).
|
|
3445
|
+
* The dispatcher (and `trigger`, when `requires` is present) resolves
|
|
3446
|
+
* each name against the `DepRegistry` at invocation time and passes the
|
|
3447
|
+
* resulting bag to the invoker. Dev builds warn when the invoker reads a
|
|
3448
|
+
* dep it didn't declare here — see `buildDepsFromRequires`. */
|
|
3449
|
+
requires?: readonly DepName[];
|
|
3450
|
+
/** Inline-SVG icon for palette / toolbar surfaces. Mirrors
|
|
3451
|
+
* `ToolPresentation.icon` so a generic `<ActionBar>` can render from
|
|
3452
|
+
* action metadata the same way `<ToolPalette>` renders from tool
|
|
3453
|
+
* metadata. May be a static `ReactNode` or a function (rare; useful
|
|
3454
|
+
* for state-aware icons like a "lock" toggle). */
|
|
3455
|
+
icon?: ReactNode | (() => ReactNode);
|
|
3456
|
+
/** Grouping key for palette/menu surfaces. Free-form string; the kit
|
|
3457
|
+
* ships defaults for `'align'` (six edges/centers), `'distribute'`
|
|
3458
|
+
* (two axes), and recommends `'pathfinder'` for boolean ops. */
|
|
3459
|
+
group?: string;
|
|
3460
|
+
/** Display override for the keyboard shortcut. When omitted, palette
|
|
3461
|
+
* surfaces derive a label from `defaultBinding` via their own
|
|
3462
|
+
* formatter. */
|
|
3463
|
+
shortcut?: string;
|
|
3464
|
+
/** Pluggable invocation strategy. The gesture dispatcher routes matched
|
|
3465
|
+
* bindings through `invoker.start` / `invoker.run` depending on timing.
|
|
3466
|
+
* All kit-standard descriptors ship one; consumer-supplied actions
|
|
3467
|
+
* without an invoker can still register but won't be triggered. */
|
|
3468
|
+
invoker?: Invoker;
|
|
3469
|
+
/** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
|
|
3470
|
+
* `BindingScope` instead of the ambient scope — meaning it beats any
|
|
3471
|
+
* active-tool binding on the same input shape. Use for tool-switch
|
|
3472
|
+
* shortcuts and global held-key triggers. Default: ambient. */
|
|
3473
|
+
scope?: 'hotkey';
|
|
1998
3474
|
/**
|
|
1999
|
-
*
|
|
3475
|
+
* @experimental
|
|
3476
|
+
* Optional predicate the command palette consults when rendering. Return
|
|
3477
|
+
* `true` when the action is currently triggerable. Return a reason string
|
|
3478
|
+
* (e.g. `'Selection required'`) when disabled — the palette greys out
|
|
3479
|
+
* the row, skips it in keyboard nav, ignores clicks, and shows the
|
|
3480
|
+
* reason next to the label. Keystroke dispatch (the registered binding)
|
|
3481
|
+
* is unaffected; the action's own `run` should self-guard.
|
|
2000
3482
|
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
3483
|
+
* **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
|
|
3484
|
+
* must not throw. If a call throws or exceeds the budget in dev mode,
|
|
3485
|
+
* `evaluateEnabled` logs a one-time warning per action id; throws are
|
|
3486
|
+
* caught and treated as disabled with reason `'(predicate threw)'`.
|
|
2005
3487
|
*
|
|
2006
|
-
*
|
|
2007
|
-
*
|
|
2008
|
-
*
|
|
2009
|
-
* subset manually with `viewToMat3(view)`.
|
|
2010
|
-
*/
|
|
2011
|
-
draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
|
|
2012
|
-
/**
|
|
2013
|
-
* Whether the layer is shown when no explicit visibility entry exists.
|
|
2014
|
-
* Defaults to `true` when absent.
|
|
2015
|
-
*/
|
|
2016
|
-
defaultVisible?: boolean;
|
|
2017
|
-
/**
|
|
2018
|
-
* When true, the layer is always drawn regardless of the visibility map.
|
|
2019
|
-
* Useful for layers that must never be hidden (e.g. base grid).
|
|
2020
|
-
*/
|
|
2021
|
-
alwaysOn?: boolean;
|
|
2022
|
-
/**
|
|
2023
|
-
* Coordinate space the layer draws in.
|
|
3488
|
+
* Snapshot-on-open semantics: the palette evaluates `enabled` once when
|
|
3489
|
+
* opened and does NOT re-evaluate on selection changes while open. Live
|
|
3490
|
+
* reactive updates are deferred — palette is short-lived.
|
|
2024
3491
|
*
|
|
2025
|
-
*
|
|
2026
|
-
*
|
|
2027
|
-
*
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
3492
|
+
* The reason set is a closed enum — to add a new reason, edit
|
|
3493
|
+
* `ActionDisabledReason` and the consumer's display map.
|
|
3494
|
+
*
|
|
3495
|
+
* The optional `deps` argument is the same bag passed to
|
|
3496
|
+
* `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
|
|
3497
|
+
* synthesize it from the surrounding `DepRegistry` so predicates can
|
|
3498
|
+
* inspect selection / scene / etc. Predicates that don't need deps just
|
|
3499
|
+
* ignore the arg.
|
|
2032
3500
|
*/
|
|
2033
|
-
|
|
3501
|
+
enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
|
|
2034
3502
|
/**
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
* "I don't claim this hit, try the next layer."
|
|
3503
|
+
* Declarative eligibility rule, evaluated against the current
|
|
3504
|
+
* `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
|
|
3505
|
+
* always eligible.
|
|
2039
3506
|
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
3507
|
+
* Accepts either a fluent `Condition` (callable with `.rule`) or a
|
|
3508
|
+
* raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
|
|
3509
|
+
*
|
|
3510
|
+
* Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
|
|
3511
|
+
* over `mode:` rules — capability rules survive new modes being added
|
|
3512
|
+
* that allow the same capability.
|
|
2043
3513
|
*/
|
|
2044
|
-
|
|
2045
|
-
/** Chrome-caps visibility predicate. When supplied, the layer must
|
|
2046
|
-
* not return a hit from any chrome element whose id reports
|
|
2047
|
-
* `false`. Absent → every element is hittable. */
|
|
2048
|
-
isVisible?: (id: string) => boolean) => AffordanceBinding | null;
|
|
3514
|
+
eligible?: Rule | Condition;
|
|
2049
3515
|
/**
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
* and
|
|
3516
|
+
* CSS cursor shown while the pointer hovers a spot where this action
|
|
3517
|
+
* would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
|
|
3518
|
+
* runs `Dispatcher.resolveOnly` on each idle pointermove — the same
|
|
3519
|
+
* match walk a real pointerdown takes — and applies the winning
|
|
3520
|
+
* action's `cursor`, so the hint and the actual click target stay in
|
|
3521
|
+
* sync by construction. Omitted = no override (the active tool's
|
|
3522
|
+
* `Tool.cursor` shows). Affordance hits are resolved earlier in the
|
|
3523
|
+
* pump via `AffordanceRegion.cursor` and never reach this field.
|
|
3524
|
+
*
|
|
3525
|
+
* Static string only. Prediction runs `enabled()` but cannot run the
|
|
3526
|
+
* invoker, so an action that matches yet bails at `start()` (empty
|
|
3527
|
+
* handle) may still show its cursor — keep `enabled` accurate for
|
|
3528
|
+
* actions that declare one.
|
|
2055
3529
|
*/
|
|
2056
|
-
|
|
3530
|
+
cursor?: string;
|
|
2057
3531
|
/**
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
3532
|
+
* CSS cursor shown while THIS action's ongoing handle is in flight —
|
|
3533
|
+
* grabbing while panning, `move` while dragging a selection, `crosshair`
|
|
3534
|
+
* while pulling a marquee.
|
|
3535
|
+
*
|
|
3536
|
+
* Separate from `cursor` because the two answer different questions:
|
|
3537
|
+
* `cursor` is a prediction ("a drag from here would pan"), this is a state
|
|
3538
|
+
* ("you are panning"). An action can declare either, both, or neither;
|
|
3539
|
+
* with only `cursor` set, the hover hint holds for the duration of the
|
|
3540
|
+
* gesture.
|
|
3541
|
+
*
|
|
3542
|
+
* This is where mid-gesture cursors live now. They used to come from the
|
|
3543
|
+
* tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
|
|
3544
|
+
* or a function-form `Tool.cursor` reading the gesture scratch out of the
|
|
3545
|
+
* tool-routing dispatcher. Both belonged to a pipeline whose whole job was
|
|
3546
|
+
* being taken over by bindings, and neither could describe a cursor for an
|
|
3547
|
+
* action a tool doesn't own.
|
|
2060
3548
|
*/
|
|
2061
|
-
|
|
3549
|
+
activeCursor?: string;
|
|
2062
3550
|
}
|
|
3551
|
+
/**
|
|
3552
|
+
* @experimental
|
|
3553
|
+
* Closed enum of reasons an action might report itself as disabled. The
|
|
3554
|
+
* consumer (palette, menu, etc.) maps these symbolic values to display
|
|
3555
|
+
* strings via its own label map — see `demo/CommandPalette.tsx` for the
|
|
3556
|
+
* canonical mapping.
|
|
3557
|
+
*/
|
|
3558
|
+
declare const ActionDisabledReason: {
|
|
3559
|
+
readonly SelectionRequired: "selection-required";
|
|
3560
|
+
readonly SceneEmpty: "scene-empty";
|
|
3561
|
+
readonly NotApplicable: "not-applicable";
|
|
3562
|
+
/** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
|
|
3563
|
+
readonly PredicateThrew: "predicate-threw";
|
|
3564
|
+
};
|
|
3565
|
+
type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
|
|
2063
3566
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
/**
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
/**
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
/** Resolve a synthetic (phase, gesture, hit, modifiers) query against the
|
|
2085
|
-
* current slot occupants WITHOUT executing the matched action. Walks slots
|
|
2086
|
-
* in real precedence order (hotkey > active > ambient) and consults each
|
|
2087
|
-
* tool's attached `def` (the declarative `ToolDef` produced by
|
|
2088
|
-
* `defineTool`). Tools without an attached `def` (imperative-only) are
|
|
2089
|
-
* invisible to this query. Returns the first match, or null if no slot
|
|
2090
|
-
* resolves the query. Pure: no scratch mutation, no scene mutation, no
|
|
2091
|
-
* RouteResolvedInfo emission. */
|
|
2092
|
-
resolveOnly: (query: ResolveQuery) => ResolveResult | null;
|
|
2093
|
-
}
|
|
2094
|
-
/** Synthetic resolution query — what the static widget asks "if a pointer
|
|
2095
|
-
* event landed on `hit` with these `modifiers`, which declarative route
|
|
2096
|
-
* would the dispatcher fire in this phase + gesture?" */
|
|
2097
|
-
interface ResolveQuery {
|
|
2098
|
-
phase: 'initial' | 'engaged';
|
|
2099
|
-
gesture: 'click' | 'drag' | 'pointerDown' | 'dblTap' | 'wheel';
|
|
2100
|
-
hit: HitResult;
|
|
2101
|
-
modifiers: ToolModifiers;
|
|
2102
|
-
}
|
|
2103
|
-
/** Successful resolution: which tool, in which slot, matched which route-table
|
|
2104
|
-
* key. `matchedKey` is `'*'` for function-form drag (no table to discriminate)
|
|
2105
|
-
* and for wheel routes (single ActionFn). */
|
|
2106
|
-
interface ResolveResult {
|
|
2107
|
-
toolId: string;
|
|
2108
|
-
slot: 'hotkey' | 'active' | 'ambient';
|
|
2109
|
-
gesture: 'click' | 'drag' | 'pointerDown' | 'dblTap' | 'wheel';
|
|
2110
|
-
phase: 'initial' | 'engaged';
|
|
2111
|
-
matchedKey: string;
|
|
3567
|
+
/**
|
|
3568
|
+
* Configurable activation-key descriptor for tools that expose their
|
|
3569
|
+
* keybinding to the host (currently Lasso and Eyedropper). Captures
|
|
3570
|
+
* only the fields meaningful to a caller-supplied tool-select key —
|
|
3571
|
+
* dispatcher-internal fields (`skipInEditable`, `enabled`,
|
|
3572
|
+
* `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
|
|
3573
|
+
* not part of the configurable surface.
|
|
3574
|
+
*/
|
|
3575
|
+
interface ToolKeybinding {
|
|
3576
|
+
/** Key or list of keys to match (case-insensitive against `event.key`). */
|
|
3577
|
+
key: string | readonly string[];
|
|
3578
|
+
/** Require Cmd (mac) / Ctrl (others). Default `false`. */
|
|
3579
|
+
mod?: boolean;
|
|
3580
|
+
/** Require Alt. Default `false`. */
|
|
3581
|
+
alt?: boolean;
|
|
3582
|
+
/**
|
|
3583
|
+
* Shift policy. `undefined`/`false` forbids shift, `true` requires
|
|
3584
|
+
* shift, `'optional'` allows either.
|
|
3585
|
+
*/
|
|
3586
|
+
shift?: boolean | 'optional';
|
|
2112
3587
|
}
|
|
2113
3588
|
|
|
2114
3589
|
interface ToolsApi {
|
|
2115
3590
|
/** Current active-slot tool id. */
|
|
2116
3591
|
active: string;
|
|
2117
|
-
/** Set the active-slot tool.
|
|
3592
|
+
/** Set the active-slot tool. The gesture dispatcher watches the active
|
|
3593
|
+
* tool and cancels any in-flight handle itself. */
|
|
2118
3594
|
setActive: (id: string) => void;
|
|
2119
3595
|
/** Currently hotkey-engaged tool id (or `null`). Derived as the top of
|
|
2120
3596
|
* the hotkey stack for backwards compat with the pre-stack API. */
|
|
2121
3597
|
hotkeyEngaged: string | null;
|
|
2122
|
-
/** Engage a hotkey-slot tool by id.
|
|
3598
|
+
/** Engage a hotkey-slot tool by id. */
|
|
2123
3599
|
engageHotkey: (id: string) => void;
|
|
2124
3600
|
/** Disengage the hotkey-slot tool, if any. */
|
|
2125
3601
|
disengageHotkey: () => void;
|
|
@@ -2127,13 +3603,6 @@ interface ToolsApi {
|
|
|
2127
3603
|
ambient: readonly AnyTool[];
|
|
2128
3604
|
/** Full registry — for userland UI (palette buttons, etc.). */
|
|
2129
3605
|
registry: Readonly<Record<string, AnyTool>>;
|
|
2130
|
-
/** The dispatcher `<Canvas>` wires to its DOM events. */
|
|
2131
|
-
dispatcher: ToolsDispatcher;
|
|
2132
|
-
/** Increments whenever an in-flight gesture starts, transitions phase, or
|
|
2133
|
-
* ends. Consumers (e.g. `<Canvas>` cursor resolution) include this in
|
|
2134
|
-
* their render deps to re-evaluate derived state on real DOM events
|
|
2135
|
-
* rather than waiting for an unrelated re-render. */
|
|
2136
|
-
gestureTick: number;
|
|
2137
3606
|
/** Returns true if a tool with the given id is in the registry or ambient list. */
|
|
2138
3607
|
has(id: string): boolean;
|
|
2139
3608
|
/** All overlay layers from currently-engaged tools (active slot, hotkey
|
|
@@ -2445,6 +3914,13 @@ type SelectOption = {
|
|
|
2445
3914
|
value: string;
|
|
2446
3915
|
label: ReactNode;
|
|
2447
3916
|
isDisabled?: boolean;
|
|
3917
|
+
/**
|
|
3918
|
+
* Plain-text form of `label`, for type-to-select and screen readers.
|
|
3919
|
+
* Only needed when `label` isn't a bare string — a label built from
|
|
3920
|
+
* elements has no text React Aria can read off it. A string label
|
|
3921
|
+
* supplies this itself.
|
|
3922
|
+
*/
|
|
3923
|
+
textValue?: string;
|
|
2448
3924
|
};
|
|
2449
3925
|
type Key$1 = string | number;
|
|
2450
3926
|
type SelectProps<T extends Key$1 = string> = Omit<SelectProps$1<object>, 'children' | 'className' | 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange'> & {
|
|
@@ -2475,7 +3951,7 @@ type SelectItemProps = Omit<ListBoxItemProps, 'className' | 'children'> & {
|
|
|
2475
3951
|
children?: ReactNode;
|
|
2476
3952
|
className?: string;
|
|
2477
3953
|
};
|
|
2478
|
-
declare function SelectItem({ children, className, ...rest }: SelectItemProps): react_jsx_runtime.JSX.Element;
|
|
3954
|
+
declare function SelectItem({ children, className, textValue, ...rest }: SelectItemProps): react_jsx_runtime.JSX.Element;
|
|
2479
3955
|
|
|
2480
3956
|
type ComboBoxOption = {
|
|
2481
3957
|
value: string;
|
|
@@ -2600,7 +4076,7 @@ interface Plot2DProps {
|
|
|
2600
4076
|
/** Pointer down on the SVG. Receives both plot- and model-space coords
|
|
2601
4077
|
* pre-computed so consumers don't repeat the rect/transform dance. */
|
|
2602
4078
|
onPointerDown?: (e: PointerEvent$1<SVGSVGElement>, coords: Plot2DCoords) => void;
|
|
2603
|
-
onKeyDown?: (e: KeyboardEvent
|
|
4079
|
+
onKeyDown?: (e: KeyboardEvent<SVGSVGElement>) => void;
|
|
2604
4080
|
children?: ReactNode;
|
|
2605
4081
|
}
|
|
2606
4082
|
interface Plot2DHandle {
|