@weasel-js/core 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +9 -5
- package/dist/animation/useAnimator.js +20 -13
- package/dist/animation/useAnimator.js.map +1 -1
- package/dist/{builtins-BW32dOXy.d.ts → builtins-CQcvcLXh.d.ts} +105 -31
- package/dist/canvas/DrawCanvas.js +33 -33
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/MinimapCanvas.js +29 -29
- package/dist/canvas/MinimapCanvas.js.map +1 -1
- package/dist/canvas/NodeShape.js +63 -54
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +26 -5
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +3 -2
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +526 -529
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +35 -35
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/defaultDrawOne.js +6 -1
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/useHitObserver.js +23 -0
- package/dist/canvas/useHitObserver.js.map +1 -0
- package/dist/canvas/useHostAnchor.js.map +1 -1
- package/dist/core/device/useDeviceProfile.js.map +1 -1
- package/dist/core/scene/derivedPath.js.map +1 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/kitRegistry.js.map +1 -1
- package/dist/core/scene/scene.js +9 -0
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/scene/useScene.js.map +1 -1
- package/dist/core/selection/useSelection.js +27 -33
- package/dist/core/selection/useSelection.js.map +1 -1
- package/dist/core/stableStringify.js +8 -0
- package/dist/core/stableStringify.js.map +1 -0
- package/dist/core/viewport/fitViewToBounds.js +3 -3
- package/dist/core/viewport/fitViewToBounds.js.map +1 -1
- package/dist/core/viewport/screenSlop.js +8 -2
- package/dist/core/viewport/screenSlop.js.map +1 -1
- package/dist/features/minimap/actions.js.map +1 -1
- package/dist/features/paths/markerCommands.js +95 -63
- package/dist/features/paths/markerCommands.js.map +1 -1
- package/dist/features/paths/tessellate/dash.js +52 -0
- package/dist/features/paths/tessellate/dash.js.map +1 -0
- package/dist/features/paths/tessellate/insets.js +32 -0
- package/dist/features/paths/tessellate/insets.js.map +1 -0
- package/dist/features/paths/tessellate/metric.js +72 -0
- package/dist/features/paths/tessellate/metric.js.map +1 -0
- package/dist/features/paths/tessellate/stroke.js +74 -134
- package/dist/features/paths/tessellate/stroke.js.map +1 -1
- package/dist/features/paths/tessellate/vertexWidths.js +32 -0
- package/dist/features/paths/tessellate/vertexWidths.js.map +1 -0
- package/dist/features/pointer/PointerContext.js +2 -2
- package/dist/features/pointer/PointerContext.js.map +1 -1
- package/dist/features/poseRun/usePoseRun.js.map +1 -1
- package/dist/features/text/useTextEdit.js +86 -84
- package/dist/features/text/useTextEdit.js.map +1 -1
- package/dist/index.d.ts +84 -45
- package/dist/index.js +269 -268
- package/dist/interactions/actions/defaults/align.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/booleans.js.map +1 -1
- package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
- package/dist/interactions/actions/defaults/clearSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
- package/dist/interactions/actions/defaults/clone.js.map +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
- package/dist/interactions/actions/defaults/delete.js.map +1 -1
- package/dist/interactions/actions/defaults/distribute.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/editAnchors.js.map +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/escape.js +1 -1
- package/dist/interactions/actions/defaults/escape.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/flip.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/insert.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
- package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/move.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
- package/dist/interactions/actions/defaults/reorder.js.map +1 -1
- package/dist/interactions/actions/defaults/requiresSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/resize.js.map +1 -1
- package/dist/interactions/actions/defaults/rotate.js.map +1 -1
- package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
- package/dist/interactions/actions/defaults/slice.js.map +1 -1
- package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
- package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
- package/dist/interactions/actions/planeInput.js.map +1 -1
- package/dist/interactions/gestures/shared/screenTolerance.js.map +1 -1
- package/dist/{registerProgram-ateHDGjw.d.ts → registerProgram-BMZRDgrh.d.ts} +4 -0
- package/dist/renderer/cache/markerCommandCache.js +46 -42
- package/dist/renderer/cache/markerCommandCache.js.map +1 -1
- package/dist/renderer/cache/outlineStrokeMeshCache.js +30 -25
- package/dist/renderer/cache/outlineStrokeMeshCache.js.map +1 -1
- package/dist/renderer/cache/strokeMeshCache.js +42 -31
- package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
- package/dist/renderer/cullDrawCommands.js +32 -31
- package/dist/renderer/cullDrawCommands.js.map +1 -1
- package/dist/renderer/draw.js +315 -362
- package/dist/renderer/draw.js.map +1 -1
- package/dist/renderer/drawBatch.js.map +1 -1
- package/dist/renderer/glyphOutlineMesh.js +67 -0
- package/dist/renderer/glyphOutlineMesh.js.map +1 -0
- package/dist/renderer/math/mat3.js +8 -3
- package/dist/renderer/math/mat3.js.map +1 -1
- package/dist/renderer/shaders/batchFill.js +3 -2
- package/dist/renderer/shaders/batchFill.js.map +1 -1
- package/dist/renderer/strokeLengths.js +50 -0
- package/dist/renderer/strokeLengths.js.map +1 -0
- package/dist/renderer.d.ts +2 -2
- package/dist/{test-seams-fYf6m5pu.d.ts → test-seams-CuIxe0xY.d.ts} +1 -1
- package/dist/test-seams.d.ts +2 -2
- package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +1 -1
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/select/selectionContributions.js +43 -23
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editAnchors.js","names":["parseAnchorAffordance","m","kind","selectionAfterAnchorPress","additive","next","current","anchorIndex","editAnchorsAction","inPlane","isAnchorOrControl","dep","ctx","affordance","anchorInfo","worldPath","editingId","storageKind","decoded","pathToAnchors","origin","anchorAt","dragging","nodeShape","originRectPose","originData","scratch","active","dx","moveCtx","dy","editAnchorSet","flat","set","side","moveHandleTo","reason","id","worldEditToStorage","editingLayer"],"sources":["../../../../src/interactions/actions/defaults/editAnchors.ts"],"sourcesContent":["/**\n * `editAnchorsAction` — ongoing Action descriptor for editing polygon anchors.\n *\n * Owns the whole press-on-an-anchor gesture, because pressing and dragging\n * an anchor are two outcomes of one interaction rather than two\n * interactions:\n *\n * - **Press** selects. Bare press replaces the anchor selection; Shift\n * toggles the pressed anchor in or out of it. Pressing an anchor that\n * is already part of a multi-anchor selection leaves the selection\n * alone, so you can grab a set and move it.\n * - **Drag** then moves whatever the press left selected — one anchor,\n * or every selected anchor together.\n * - **Dragging a control handle** moves just that handle, mirroring the\n * opposite one while the anchor is smooth. Alt breaks the mirror.\n *\n * Selection lives on the `editAnchors` dep (flat anchor indices) rather\n * than in this handle's scratch, because the keyboard actions\n * (`nudgeAnchors`, `deleteAnchors`) and the overlay all need to read it\n * between gestures.\n *\n * ## Wiring\n *\n * The descriptor is operational when the consumer wires:\n * 1. `buildAffordanceAt` with a `getAnchorState` thunk so the dispatcher\n * classifies anchor/control-handle hits as `anchor:N`, `controlIn:N`,\n * or `controlOut:N` affordances on pointerdown.\n * 2. The `editAnchors` dep in the DepSchema.\n *\n * When `ctx.drag.affordance` is absent or is not an anchor/control kind,\n * `start` returns `{}` (no-op) so other bindings can handle the drag.\n *\n * @see buildAffordanceAt — the classifier that produces anchor affordances.\n * @see anchorEdits — the pure geometry these handlers drive.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { EditAnchorsDep } from '../depSchema';\nimport { isAnchorOrControl } from '@weasel-js/routing';\nimport {\n anchorAt,\n editAnchorSet,\n moveHandleTo,\n translateAnchorBy,\n type AnchorSet,\n} from 'features/paths/anchorEdits';\nimport { pathToAnchors } from 'features/paths/anchors';\nimport { worldEditToStorage } from 'features/paths/pathInWorld';\n// Commit goes through dep.applyEdit (routes setPose or setPose+update\n// based on the node's path-storage shape); no direct op or dispatch\n// helpers needed here.\nimport type { PolygonPath } from '@weasel-js/geom';\nimport { inPlane, editingLayer } from '../planeInput';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Parse an anchor affordance kind string into the anchor it names.\n * Returns `null` when the affordance is not an anchor kind.\n *\n * Format: `'anchor:N'` | `'controlIn:N'` | `'controlOut:N'`, where N is the\n * flat anchor index — the same numbering `enumerateAnchors` produces and\n * `anchorEdits` addresses by.\n */\nfunction parseAnchorAffordance(\n kind: string,\n): { anchorIndex: number; part: 'anchor' | 'controlIn' | 'controlOut' } | null {\n const m = kind.match(/^(anchor|controlIn|controlOut):(\\d+)$/);\n if (!m) return null;\n return {\n anchorIndex: parseInt(m[2], 10),\n part: m[1] as 'anchor' | 'controlIn' | 'controlOut',\n };\n}\n\n/**\n * Resolve what a press on `anchorIndex` should leave selected.\n *\n * Shift toggles. A bare press on an anchor that's already part of the\n * selection keeps the selection intact (so the drag moves the whole set);\n * a bare press anywhere else collapses to just that anchor.\n */\nexport function selectionAfterAnchorPress(\n current: ReadonlySet<number>,\n anchorIndex: number,\n additive: boolean,\n): Set<number> {\n if (additive) {\n const next = new Set(current);\n if (next.has(anchorIndex)) next.delete(anchorIndex);\n else next.add(anchorIndex);\n return next;\n }\n if (current.has(anchorIndex) && current.size > 1) return new Set(current);\n return new Set([anchorIndex]);\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface EditAnchorsScratch {\n dep: EditAnchorsDep;\n id: string;\n anchorIndex: number;\n /** `'anchor'` drags on-curve points (and their handles) by a delta.\n * `'controlIn'` / `'controlOut'` drag one control point to an absolute\n * world coord. */\n part: 'anchor' | 'controlIn' | 'controlOut';\n /** Anchors this drag moves — the pressed one, or the whole anchor\n * selection when the press landed inside it. Flat indices. */\n dragging: readonly number[];\n /** World position of the pressed anchor at drag start; the drag delta is\n * measured from here rather than accumulated per move, so the geometry\n * is always derived from `originPose` in one step. */\n anchorOrigin: { x: number; y: number };\n originPose: PolygonPath;\n currentPose: PolygonPath;\n /** Where the polygon lives on the node — drives which preview-ghost\n * axes the handle populates (`previewPose` only for pose-as-polygon,\n * `previewPose` + `previewData` for data.path). */\n storageKind: 'pose' | 'data';\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * Static descriptor for the `editAnchors` Action.\n *\n * Requires dep-schema entries: `selection`, `editAnchors`.\n */\nexport const editAnchorsAction: Action & { requires: string[] } = inPlane({\n id: 'editAnchors',\n label: 'Edit Anchors',\n defaultBinding: {\n kind: 'drag',\n // Only matches drags whose pointerdown hit an anchor or control handle.\n // Ties moveAction's `selected-body` on specificity and registers after\n // it, so move declines these presses in `start`.\n target: { kindOf: isAnchorOrControl },\n },\n eligible: { capability: 'edits-anchors' },\n requires: ['selection', 'editAnchors'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const dep = ctx.deps.editAnchors as EditAnchorsDep | undefined;\n if (!dep) return {};\n\n const affordance = ctx.drag?.affordance;\n if (!affordance) {\n // No affordance hit → this drag doesn't target an anchor handle.\n // Let other bindings handle it (the handle returns {}, dispatcher moves on).\n return {};\n }\n\n const anchorInfo = parseAnchorAffordance(affordance.kind);\n if (!anchorInfo) {\n // Affordance is a non-anchor kind (e.g. resize handle, rotate handle).\n return {};\n }\n\n const { editingId } = dep;\n // World-coord polygon. For pose-as-polygon nodes this equals\n // node.pose; for data.path nodes it's pathInWorld-projected (translate +\n // rotation) so the anchor world coords match what the user clicked.\n const worldPath = dep.getEditablePath(editingId) as PolygonPath | undefined;\n if (!worldPath || worldPath.kind !== 'polygon') return {};\n const storageKind = dep.getStorageKind(editingId);\n if (storageKind !== 'pose' && storageKind !== 'data') return {};\n\n // Decode once to validate the affordance index against the actual\n // geometry and to read the press origin. A stale affordance (the\n // path changed between pointerdown and now) fails here rather than\n // silently editing whichever anchor now holds that index.\n const decoded = pathToAnchors(worldPath) as AnchorSet;\n const origin = anchorAt(decoded, anchorInfo.anchorIndex);\n if (!origin) return {};\n if (anchorInfo.part === 'controlIn' && !origin.inHandle) return {};\n if (anchorInfo.part === 'controlOut' && !origin.outHandle) return {};\n\n // --- Press semantics: selection updates immediately, before any move.\n let dragging: readonly number[] = [anchorInfo.anchorIndex];\n if (anchorInfo.part === 'anchor') {\n const next = selectionAfterAnchorPress(\n dep.selectedAnchors,\n anchorInfo.anchorIndex,\n ctx.modifiers.shift,\n );\n dep.setSelectedAnchors(next);\n // Shift-pressing an anchor OUT of the selection must not then drag\n // it; drag whatever the press left selected, intersected with the\n // pressed anchor's membership.\n dragging = next.has(anchorInfo.anchorIndex)\n ? [...next]\n : [];\n }\n\n // For data.path nodes, we also need the original rect pose + data\n // so previewPose / previewData can be derived from currentPose.\n // Captured at start so onMove doesn't re-read the scene.\n const nodeShape = storageKind === 'data' ? dep.getNodeShape(editingId) : null;\n const originRectPose =\n nodeShape ? (nodeShape.pose as { x: number; y: number; width: number; height: number }) : undefined;\n const originData = nodeShape ? (nodeShape.data as object) : undefined;\n\n const scratch: EditAnchorsScratch = {\n dep,\n id: editingId,\n anchorIndex: anchorInfo.anchorIndex,\n part: anchorInfo.part,\n dragging,\n anchorOrigin: { x: origin.x, y: origin.y },\n originPose: worldPath,\n currentPose: worldPath,\n storageKind,\n };\n\n let active = false;\n\n return {\n kind: 'edit-anchors',\n onMove(moveCtx: InvocationCtx): void {\n if (scratch.part === 'anchor') {\n if (scratch.dragging.length === 0) return;\n const dx = moveCtx.world.x - scratch.anchorOrigin.x;\n const dy = moveCtx.world.y - scratch.anchorOrigin.y;\n const next = editAnchorSet(scratch.originPose, (set) => {\n for (const flat of scratch.dragging) translateAnchorBy(set, flat, dx, dy);\n });\n if (next) scratch.currentPose = next;\n } else {\n const side = scratch.part === 'controlIn' ? 'in' : 'out';\n const next = editAnchorSet(scratch.originPose, (set) =>\n moveHandleTo(\n set,\n scratch.anchorIndex,\n side,\n moveCtx.world.x,\n moveCtx.world.y,\n // Alt breaks the smooth mirror for this drag — read live so\n // the user can press or release Alt mid-drag.\n moveCtx.modifiers.alt,\n ),\n );\n if (next) scratch.currentPose = next;\n }\n active = true;\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n active = false;\n if (reason === 'cancel') return;\n if (scratch.originPose === scratch.currentPose) return;\n scratch.dep.applyEdit(scratch.id, scratch.currentPose, 'Edit anchors');\n },\n previewIds: () => (active ? [scratch.id] : null),\n previewPose: (id: string): unknown | null => {\n if (!active || id !== scratch.id) return null;\n if (scratch.storageKind === 'pose') {\n // Pose IS the polygon — preview pose is the polygon itself.\n return scratch.currentPose;\n }\n // data.path: invert the world edit the same way the commit does, so\n // the preview ghost (which re-applies pose.rotation via the render\n // wrap) matches the committed result — no double-rotation.\n if (!originRectPose) return null;\n return worldEditToStorage(originRectPose, scratch.currentPose).pose;\n },\n previewData: (id: string): unknown | null => {\n if (!active || id !== scratch.id) return null;\n if (scratch.storageKind !== 'data') return null;\n if (!originData) return null;\n // Stored path is the unrotated, origin-aligned contour; the render\n // invariant `pathInWorld(stored, pose) === world` holds.\n const { path } = worldEditToStorage(originRectPose!, scratch.currentPose);\n return { ...originData, path };\n },\n };\n },\n },\n enabled: () => true,\n}, editingLayer);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"editAnchors.js","names":["parseAnchorAffordance","m","kind","selectionAfterAnchorPress","additive","next","current","anchorIndex","editAnchorsAction","inPlane","isAnchorOrControl","dep","ctx","affordance","anchorInfo","worldPath","editingId","storageKind","decoded","pathToAnchors","origin","anchorAt","dragging","nodeShape","originRectPose","originData","scratch","active","dx","moveCtx","dy","editAnchorSet","flat","set","side","moveHandleTo","reason","id","worldEditToStorage","editingLayer"],"sources":["../../../../src/interactions/actions/defaults/editAnchors.ts"],"sourcesContent":["/**\n * `editAnchorsAction` — ongoing Action descriptor for editing polygon anchors.\n *\n * Owns the whole press-on-an-anchor gesture, because pressing and dragging\n * an anchor are two outcomes of one interaction rather than two\n * interactions:\n *\n * - **Press** selects. Bare press replaces the anchor selection; Shift\n * toggles the pressed anchor in or out of it. Pressing an anchor that\n * is already part of a multi-anchor selection leaves the selection\n * alone, so you can grab a set and move it.\n * - **Drag** then moves whatever the press left selected — one anchor,\n * or every selected anchor together.\n * - **Dragging a control handle** moves just that handle, mirroring the\n * opposite one while the anchor is smooth. Alt breaks the mirror.\n *\n * Selection lives on the `editAnchors` dep (flat anchor indices) rather\n * than in this handle's scratch, because the keyboard actions\n * (`nudgeAnchors`, `deleteAnchors`) and the overlay all need to read it\n * between gestures.\n *\n * ## Wiring\n *\n * The descriptor is operational when the consumer wires:\n * 1. `buildAffordanceAt` with a `getAnchorState` thunk so the dispatcher\n * classifies anchor/control-handle hits as `anchor:N`, `controlIn:N`,\n * or `controlOut:N` affordances on pointerdown.\n * 2. The `editAnchors` dep in the DepSchema.\n *\n * When `ctx.drag.affordance` is absent or is not an anchor/control kind,\n * `start` returns `{}` (no-op) so other bindings can handle the drag.\n *\n * @see buildAffordanceAt — the classifier that produces anchor affordances.\n * @see anchorEdits — the pure geometry these handlers drive.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { EditAnchorsDep } from '../depSchema';\nimport { isAnchorOrControl } from '@weasel-js/routing';\nimport {\n anchorAt,\n editAnchorSet,\n moveHandleTo,\n translateAnchorBy,\n type AnchorSet,\n} from 'features/paths/anchorEdits';\nimport { pathToAnchors } from 'features/paths/anchors';\nimport { worldEditToStorage } from 'features/paths/pathInWorld';\n// Commit goes through dep.applyEdit (routes setPose or setPose+update\n// based on the node's path-storage shape); no direct op or dispatch\n// helpers needed here.\nimport type { PolygonPath } from '@weasel-js/geom';\nimport { inPlane, editingLayer } from '../planeInput';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Parse an anchor affordance kind string into the anchor it names.\n * Returns `null` when the affordance is not an anchor kind.\n *\n * Format: `'anchor:N'` | `'controlIn:N'` | `'controlOut:N'`, where N is the\n * flat anchor index — the same numbering `enumerateAnchors` produces and\n * `anchorEdits` addresses by.\n */\nfunction parseAnchorAffordance(\n kind: string,\n): { anchorIndex: number; part: 'anchor' | 'controlIn' | 'controlOut' } | null {\n const m = kind.match(/^(anchor|controlIn|controlOut):(\\d+)$/);\n if (!m) return null;\n return {\n anchorIndex: parseInt(m[2], 10),\n part: m[1] as 'anchor' | 'controlIn' | 'controlOut',\n };\n}\n\n/**\n * Resolve what a press on `anchorIndex` should leave selected.\n *\n * Shift toggles. A bare press on an anchor that's already part of the\n * selection keeps the selection intact (so the drag moves the whole set);\n * a bare press anywhere else collapses to just that anchor.\n */\nexport function selectionAfterAnchorPress(\n current: ReadonlySet<number>,\n anchorIndex: number,\n additive: boolean,\n): Set<number> {\n if (additive) {\n const next = new Set(current);\n if (next.has(anchorIndex)) next.delete(anchorIndex);\n else next.add(anchorIndex);\n return next;\n }\n if (current.has(anchorIndex) && current.size > 1) return new Set(current);\n return new Set([anchorIndex]);\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface EditAnchorsScratch {\n dep: EditAnchorsDep;\n id: string;\n anchorIndex: number;\n /** `'anchor'` drags on-curve points (and their handles) by a delta.\n * `'controlIn'` / `'controlOut'` drag one control point to an absolute\n * world coord. */\n part: 'anchor' | 'controlIn' | 'controlOut';\n /** Anchors this drag moves — the pressed one, or the whole anchor\n * selection when the press landed inside it. Flat indices. */\n dragging: readonly number[];\n /** World position of the pressed anchor at drag start; the drag delta is\n * measured from here rather than accumulated per move, so the geometry\n * is always derived from `originPose` in one step. */\n anchorOrigin: { x: number; y: number };\n originPose: PolygonPath;\n currentPose: PolygonPath;\n /** Where the polygon lives on the node — drives which preview-ghost\n * axes the handle populates (`previewPose` only for pose-as-polygon,\n * `previewPose` + `previewData` for data.path). */\n storageKind: 'pose' | 'data';\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * Static descriptor for the `editAnchors` Action.\n *\n * Requires dep-schema entries: `selection`, `editAnchors`.\n */\nexport const editAnchorsAction: Action & { requires: string[] } = inPlane({\n id: 'editAnchors',\n label: 'Edit Anchors',\n defaultBinding: {\n kind: 'drag',\n // Only matches drags whose pointerdown hit an anchor or control handle.\n // Ties moveAction's `selected-body` on specificity and registers after\n // it, so move declines these presses in `start`.\n target: { kindOf: isAnchorOrControl },\n },\n eligible: { capability: 'edits-anchors' },\n requires: ['selection', 'editAnchors'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const dep = ctx.deps.editAnchors;\n if (!dep) return {};\n\n const affordance = ctx.drag?.affordance;\n if (!affordance) {\n // No affordance hit → this drag doesn't target an anchor handle.\n // Let other bindings handle it (the handle returns {}, dispatcher moves on).\n return {};\n }\n\n const anchorInfo = parseAnchorAffordance(affordance.kind);\n if (!anchorInfo) {\n // Affordance is a non-anchor kind (e.g. resize handle, rotate handle).\n return {};\n }\n\n const { editingId } = dep;\n // World-coord polygon. For pose-as-polygon nodes this equals\n // node.pose; for data.path nodes it's pathInWorld-projected (translate +\n // rotation) so the anchor world coords match what the user clicked.\n const worldPath = dep.getEditablePath(editingId) as PolygonPath | undefined;\n if (!worldPath || worldPath.kind !== 'polygon') return {};\n const storageKind = dep.getStorageKind(editingId);\n if (storageKind !== 'pose' && storageKind !== 'data') return {};\n\n // Decode once to validate the affordance index against the actual\n // geometry and to read the press origin. A stale affordance (the\n // path changed between pointerdown and now) fails here rather than\n // silently editing whichever anchor now holds that index.\n const decoded = pathToAnchors(worldPath) as AnchorSet;\n const origin = anchorAt(decoded, anchorInfo.anchorIndex);\n if (!origin) return {};\n if (anchorInfo.part === 'controlIn' && !origin.inHandle) return {};\n if (anchorInfo.part === 'controlOut' && !origin.outHandle) return {};\n\n // --- Press semantics: selection updates immediately, before any move.\n let dragging: readonly number[] = [anchorInfo.anchorIndex];\n if (anchorInfo.part === 'anchor') {\n const next = selectionAfterAnchorPress(\n dep.selectedAnchors,\n anchorInfo.anchorIndex,\n ctx.modifiers.shift,\n );\n dep.setSelectedAnchors(next);\n // Shift-pressing an anchor OUT of the selection must not then drag\n // it; drag whatever the press left selected, intersected with the\n // pressed anchor's membership.\n dragging = next.has(anchorInfo.anchorIndex)\n ? [...next]\n : [];\n }\n\n // For data.path nodes, we also need the original rect pose + data\n // so previewPose / previewData can be derived from currentPose.\n // Captured at start so onMove doesn't re-read the scene.\n const nodeShape = storageKind === 'data' ? dep.getNodeShape(editingId) : null;\n const originRectPose =\n nodeShape ? (nodeShape.pose as { x: number; y: number; width: number; height: number }) : undefined;\n const originData = nodeShape ? (nodeShape.data as object) : undefined;\n\n const scratch: EditAnchorsScratch = {\n dep,\n id: editingId,\n anchorIndex: anchorInfo.anchorIndex,\n part: anchorInfo.part,\n dragging,\n anchorOrigin: { x: origin.x, y: origin.y },\n originPose: worldPath,\n currentPose: worldPath,\n storageKind,\n };\n\n let active = false;\n\n return {\n kind: 'edit-anchors',\n onMove(moveCtx: InvocationCtx): void {\n if (scratch.part === 'anchor') {\n if (scratch.dragging.length === 0) return;\n const dx = moveCtx.world.x - scratch.anchorOrigin.x;\n const dy = moveCtx.world.y - scratch.anchorOrigin.y;\n const next = editAnchorSet(scratch.originPose, (set) => {\n for (const flat of scratch.dragging) translateAnchorBy(set, flat, dx, dy);\n });\n if (next) scratch.currentPose = next;\n } else {\n const side = scratch.part === 'controlIn' ? 'in' : 'out';\n const next = editAnchorSet(scratch.originPose, (set) =>\n moveHandleTo(\n set,\n scratch.anchorIndex,\n side,\n moveCtx.world.x,\n moveCtx.world.y,\n // Alt breaks the smooth mirror for this drag — read live so\n // the user can press or release Alt mid-drag.\n moveCtx.modifiers.alt,\n ),\n );\n if (next) scratch.currentPose = next;\n }\n active = true;\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n active = false;\n if (reason === 'cancel') return;\n if (scratch.originPose === scratch.currentPose) return;\n scratch.dep.applyEdit(scratch.id, scratch.currentPose, 'Edit anchors');\n },\n previewIds: () => (active ? [scratch.id] : null),\n previewPose: (id: string): unknown | null => {\n if (!active || id !== scratch.id) return null;\n if (scratch.storageKind === 'pose') {\n // Pose IS the polygon — preview pose is the polygon itself.\n return scratch.currentPose;\n }\n // data.path: invert the world edit the same way the commit does, so\n // the preview ghost (which re-applies pose.rotation via the render\n // wrap) matches the committed result — no double-rotation.\n if (!originRectPose) return null;\n return worldEditToStorage(originRectPose, scratch.currentPose).pose;\n },\n previewData: (id: string): unknown | null => {\n if (!active || id !== scratch.id) return null;\n if (scratch.storageKind !== 'data') return null;\n if (!originData) return null;\n // Stored path is the unrotated, origin-aligned contour; the render\n // invariant `pathInWorld(stored, pose) === world` holds.\n const { path } = worldEditToStorage(originRectPose!, scratch.currentPose);\n return { ...originData, path };\n },\n };\n },\n },\n enabled: () => true,\n}, editingLayer);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enterPathEdit.js","names":["enterPathEditAction","isBody","editAnchors","deps","selection","ids","id"],"sources":["../../../../src/interactions/actions/defaults/enterPathEdit.ts"],"sourcesContent":["/**\n * `enterPathEditAction` — immediate Action that enters path-anchor edit\n * mode on a double-click against a selected polygon. Sets\n * `editAnchors.editingId` to the first selected polygon's id; the\n * gesture-side hit-test (via `buildAffordanceAt`) and the\n * `pathEditingOverlayLayer` chrome both gate on that id.\n *\n * No-op when the selection contains no polygon. The companion\n * `exitPathEditAction` (Escape) clears edit mode.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\nimport type {
|
|
1
|
+
{"version":3,"file":"enterPathEdit.js","names":["enterPathEditAction","isBody","editAnchors","deps","selection","ids","id"],"sources":["../../../../src/interactions/actions/defaults/enterPathEdit.ts"],"sourcesContent":["/**\n * `enterPathEditAction` — immediate Action that enters path-anchor edit\n * mode on a double-click against a selected polygon. Sets\n * `editAnchors.editingId` to the first selected polygon's id; the\n * gesture-side hit-test (via `buildAffordanceAt`) and the\n * `pathEditingOverlayLayer` chrome both gate on that id.\n *\n * No-op when the selection contains no polygon. The companion\n * `exitPathEditAction` (Escape) clears edit mode.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\nimport type { NodeId } from 'core/scene/types';\nimport { isBody } from '@weasel-js/routing';\n\nexport const enterPathEditAction: Action & { requires: string[] } = {\n id: 'enterPathEdit',\n label: 'Edit path anchors',\n // Double-click on a polygon body (selected OR unselected) — matches the\n // Figma/Illustrator convention where double-clicking a path enters edit\n // mode regardless of prior selection state. The `kindOf` predicate fires\n // on either body class but not on empty space, so a double-click on\n // empty canvas doesn't accidentally enter edit mode.\n defaultBinding: {\n kind: 'doubleClick',\n target: { kindOf: isBody },\n },\n eligible: { mode: { in: ['normal', 'isolation'] } },\n requires: ['editAnchors', 'selection'],\n invoker: {\n timing: 'immediate',\n run(deps) {\n const editAnchors = deps.editAnchors;\n const selection = deps.selection;\n if (!editAnchors || !selection) return;\n const ids = selection.get() as NodeId[];\n // Use the dep's editability check rather than poking at node.pose\n // directly — the dep knows whether a node has an editable polygon\n // (pose-as-polygon OR data.path) so this picks up both shapes.\n for (const id of ids) {\n if (editAnchors.getEditablePath(id)) {\n editAnchors.setEditingId(id);\n return;\n }\n }\n },\n } as ImmediateInvoker,\n enabled: () => true,\n};\n"],"mappings":";;AAgBA,IAAaA,IAAuD;CAClE,IAAI;CACJ,OAAO;CAMP,gBAAgB;EACd,MAAM;EACN,QAAQ,EAAE,QAAQC,EAAO;CAC3B;CACA,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,WAAW,EAAE,EAAE;CAClD,UAAU,CAAC,eAAe,WAAW;CACrC,SAAS;EACP,QAAQ;EACR,IAAI,GAAM;GACR,IAAMC,IAAcC,EAAK,aACnBC,IAAYD,EAAK;GACvB,IAAI,CAACD,KAAe,CAACE,GAAW;GAChC,IAAMC,IAAMD,EAAU,IAAI;GAI1B,KAAK,IAAME,KAAMD,GACf,IAAIH,EAAY,gBAAgBI,CAAE,GAAG;IACnC,EAAY,aAAaA,CAAE;IAC3B;GACF;EAEJ;CACF;CACA,eAAe;AACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enterTextEdit.js","names":["enterTextEditAction","sel","deps","id","textEdit"],"sources":["../../../../src/interactions/actions/defaults/enterTextEdit.ts"],"sourcesContent":["/**\n * `enterTextEditAction` — immediate Action descriptor for entering in-place\n * text editing on a selected text node.\n *\n * ## Status: REAL\n *\n * Fires via `useTextTool.bindings` when the user clicks on a\n * selected text node. Calls `deps.textEdit.startEdit(id)` to activate the\n * contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.\n *\n * ## No defaultBinding / defaultBinding\n *\n * This action has no ambient key or gesture binding — it fires ONLY via\n * `useTextTool`'s `Tool.bindings` entry:\n *\n * ```ts\n * bindings: [\n * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },\n * ]\n * ```\n *\n * Keeping it binding-free avoids ambient double-fire and scopes the action to\n * the text tool context where `classifyTarget` is already wired.\n *\n * ## Self-guard: only act on text nodes\n *\n * The `'selected-body'` target yields a match for any selected node kind. To\n * avoid entering text-edit mode when the text tool happens to have a non-text\n * node selected, the action self-guards via an optional `isTextNode` predicate\n * on `TextEditDep`:\n *\n * - When `isTextNode` is absent: action fires unconditionally (the binding\n * spec is the real gate — consumers should only bind this action from the\n * text tool).\n * - When `isTextNode(id)` returns `false`: action is a no-op for that node.\n *\n * ### Pre-filtering at dispatch time\n *\n * `classifyTarget` now surfaces node kind, so a binding can pre-filter instead\n * of relying on the self-guard:\n *\n * ```ts\n * { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }\n * ```\n *\n * That reads the *routing trait's* kind, so it matches whatever names the\n * consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's\n * inferred default. `isTextNode` stays on `TextEditDep` because it also covers\n * consumers who bind the broader `'selected-body'` target, and because it is\n * the only guard for a consumer who opted out of routing entirely.\n *\n * ## Migration plan for useTextTool\n *\n * When wiring `useTextTool` to `Tool.bindings`:\n *\n * 1. Add to `useTextTool`'s `bindings`:\n * ```ts\n * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }\n * ```\n * 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`\n * return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`\n * (or however the consumer identifies text nodes).\n * 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes\n * redundant — remove it in the same pass.\n */\n\nimport type { Action } from '@weasel-js/routing';\
|
|
1
|
+
{"version":3,"file":"enterTextEdit.js","names":["enterTextEditAction","sel","deps","id","textEdit"],"sources":["../../../../src/interactions/actions/defaults/enterTextEdit.ts"],"sourcesContent":["/**\n * `enterTextEditAction` — immediate Action descriptor for entering in-place\n * text editing on a selected text node.\n *\n * ## Status: REAL\n *\n * Fires via `useTextTool.bindings` when the user clicks on a\n * selected text node. Calls `deps.textEdit.startEdit(id)` to activate the\n * contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.\n *\n * ## No defaultBinding / defaultBinding\n *\n * This action has no ambient key or gesture binding — it fires ONLY via\n * `useTextTool`'s `Tool.bindings` entry:\n *\n * ```ts\n * bindings: [\n * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },\n * ]\n * ```\n *\n * Keeping it binding-free avoids ambient double-fire and scopes the action to\n * the text tool context where `classifyTarget` is already wired.\n *\n * ## Self-guard: only act on text nodes\n *\n * The `'selected-body'` target yields a match for any selected node kind. To\n * avoid entering text-edit mode when the text tool happens to have a non-text\n * node selected, the action self-guards via an optional `isTextNode` predicate\n * on `TextEditDep`:\n *\n * - When `isTextNode` is absent: action fires unconditionally (the binding\n * spec is the real gate — consumers should only bind this action from the\n * text tool).\n * - When `isTextNode(id)` returns `false`: action is a no-op for that node.\n *\n * ### Pre-filtering at dispatch time\n *\n * `classifyTarget` now surfaces node kind, so a binding can pre-filter instead\n * of relying on the self-guard:\n *\n * ```ts\n * { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }\n * ```\n *\n * That reads the *routing trait's* kind, so it matches whatever names the\n * consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's\n * inferred default. `isTextNode` stays on `TextEditDep` because it also covers\n * consumers who bind the broader `'selected-body'` target, and because it is\n * the only guard for a consumer who opted out of routing entirely.\n *\n * ## Migration plan for useTextTool\n *\n * When wiring `useTextTool` to `Tool.bindings`:\n *\n * 1. Add to `useTextTool`'s `bindings`:\n * ```ts\n * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }\n * ```\n * 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`\n * return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`\n * (or however the consumer identifies text nodes).\n * 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes\n * redundant — remove it in the same pass.\n */\n\nimport type { Action } from '@weasel-js/routing';\n\n// ---------------------------------------------------------------------------\n// TextEditDep\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `enterTextEdit` Action.\n *\n * Requires dep-schema entries: `textEdit`, `selection`.\n *\n * No `defaultBinding` / `defaultBinding` — fires only via `Tool.bindings`.\n * Self-guards via `TextEditDep.isTextNode` when provided.\n */\nexport const enterTextEditAction: Action & { requires: string[] } = {\n id: 'enterTextEdit',\n label: 'Enter text edit',\n // No defaultBinding / defaultBinding — fires only via Tool.bindings.\n eligible: { mode: { in: ['normal', 'isolation'] } },\n requires: ['textEdit', 'selection'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const sel = deps.selection?.get() ?? [];\n if (sel.length !== 1) return;\n\n const id = sel[0];\n const textEdit = deps.textEdit;\n if (!textEdit) return;\n\n // Self-guard: skip non-text nodes when the predicate is supplied.\n if (textEdit.isTextNode && !textEdit.isTextNode(id)) return;\n\n textEdit.startEdit(id);\n },\n },\n};\n"],"mappings":";AAqFA,IAAaA,IAAuD;CAClE,IAAI;CACJ,OAAO;CAEP,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,WAAW,EAAE,EAAE;CAClD,UAAU,CAAC,YAAY,WAAW;CAClC,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,IAAMC,IAAMC,EAAK,WAAW,IAAI,KAAK,CAAC;GACtC,IAAID,EAAI,WAAW,GAAG;GAEtB,IAAME,IAAKF,EAAI,IACTG,IAAWF,EAAK;GACtB,AAAKE,MAGD,GAAS,cAAeA,EAAS,WAAWD,CAAE,MAElDC,EAAS,UAAUD,CAAE;EACvB;CACF;AACF"}
|
|
@@ -18,7 +18,7 @@ var t = {
|
|
|
18
18
|
(e.selection?.get() ?? []).length !== 0 && e.selection?.set([]);
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
enabled: (t) => t?.editAnchors?.editingId || (
|
|
21
|
+
enabled: (t) => t?.editAnchors?.editingId || (t?.selection?.get() ?? []).length === 0 ? e.NotApplicable : !0
|
|
22
22
|
};
|
|
23
23
|
//#endregion
|
|
24
24
|
export { t as escapeAction };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escape.js","names":["escapeAction","deps","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/escape.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"escape.js","names":["escapeAction","deps","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/escape.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\n\n/**\n * @experimental\n * Static descriptor for the `escape` Action. Clears selection.\n */\n// No `eligible` field → always eligible. Escape works in every mode; the\n// runtime `enabled` thunk handles the path-edit defer-to-exit case.\nexport const escapeAction: Action & { requires: string[] } = {\n id: 'escape',\n label: 'Escape',\n // Gated to \"no tool is engaged\" so a mid-drag Escape goes to\n // `cancelGestureAction` (cancels the drag) instead of clearing\n // selection.\n defaultBinding: {\n kind: 'key',\n key: 'Escape',\n phase: [{ channel: '*', phase: 'initial' }],\n },\n requires: ['selection', 'editAnchors'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const sel = deps.selection?.get() ?? [];\n if (sel.length === 0) return;\n deps.selection?.set([]);\n },\n },\n // Defer to `exitPathEditAction` while path-anchor edit mode is active.\n // Otherwise the more-specific phase qualifier on this binding would beat\n // `exitPathEdit`'s bare key spec, swallowing the Escape that should leave\n // edit mode. With this gate, `escape` reports a disabled reason in that\n // case and the dispatcher falls through to `exitPathEdit`.\n // With nothing selected there is nothing to clear, so the press falls\n // through to `tool.resetToDefault` instead of being spent here.\n enabled: (deps) => {\n const editAnchors = deps?.editAnchors;\n if (editAnchors?.editingId) return ActionDisabledReason.NotApplicable;\n const sel = deps?.selection?.get() ?? [];\n if (sel.length === 0) return ActionDisabledReason.NotApplicable;\n return true;\n },\n};\n"],"mappings":";;AASA,IAAaA,IAAgD;CAC3D,IAAI;CACJ,OAAO;CAIP,gBAAgB;EACd,MAAM;EACN,KAAK;EACL,OAAO,CAAC;GAAE,SAAS;GAAK,OAAO;EAAU,CAAC;CAC5C;CACA,UAAU,CAAC,aAAa,aAAa;CACrC,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GAEb,CADYC,EAAK,WAAW,IAAI,KAAK,CAAC,EAAA,CAC9B,WAAW,KACnBA,EAAK,WAAW,IAAI,CAAC,CAAC;EACxB;CACF;CAQA,UAAU,MACYA,GAAM,aACT,cACLA,GAAM,WAAW,IAAI,KAAK,CAAC,EAAA,CAC/B,WAAW,IAAUC,EAAqB,gBAC3C;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exitPathEdit.js","names":["exitPathEditAction"],"sources":["../../../../src/interactions/actions/defaults/exitPathEdit.ts"],"sourcesContent":["/**\n * `exitPathEditAction` — immediate Action that exits path-anchor edit\n * mode. Bound to Escape by default; companion to `enterPathEditAction`.\n * Clears `editAnchors.editingId`, which the gesture-side hit-test and\n * `pathEditingOverlayLayer` both gate on.\n *\n * Always enabled — calling it when no path is being edited is a no-op.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\
|
|
1
|
+
{"version":3,"file":"exitPathEdit.js","names":["exitPathEditAction"],"sources":["../../../../src/interactions/actions/defaults/exitPathEdit.ts"],"sourcesContent":["/**\n * `exitPathEditAction` — immediate Action that exits path-anchor edit\n * mode. Bound to Escape by default; companion to `enterPathEditAction`.\n * Clears `editAnchors.editingId`, which the gesture-side hit-test and\n * `pathEditingOverlayLayer` both gate on.\n *\n * Always enabled — calling it when no path is being edited is a no-op.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\n\nexport const exitPathEditAction: Action & { requires: string[] } = {\n id: 'exitPathEdit',\n label: 'Exit path edit',\n defaultBinding: { kind: 'key', key: 'Escape' },\n eligible: { mode: 'path-edit' },\n requires: ['editAnchors'],\n invoker: {\n timing: 'immediate',\n run(deps) {\n const editAnchors = deps.editAnchors;\n editAnchors?.setEditingId(null);\n },\n } as ImmediateInvoker,\n enabled: () => true,\n};\n"],"mappings":";AAYA,IAAaA,IAAsD;CACjE,IAAI;CACJ,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;CAAS;CAC7C,UAAU,EAAE,MAAM,YAAY;CAC9B,UAAU,CAAC,aAAa;CACxB,SAAS;EACP,QAAQ;EACR,IAAI,GAAM;GAER,EADyB,aACZ,aAAa,IAAI;EAChC;CACF;CACA,eAAe;AACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flip.js","names":["localMirror","at","frame","id","pivotWorld","theta","mirror","axis","turned","multiply","rotate","translate","flipSelection","ids","selection","scenePoseFrame","scene","poseComposition","worlds","sharedPivot","pivot","unionAABB","visualBoundsViaDescriptor","p","geom","resolveSpatialReference","actionReferenceSources","params","pointer","ops","node","world","flipped","flipPoseAboutBounds","flipPoseViaDescriptor","createTransformOp","g","dataOp","geometryDataOp","geometryProjection","applyOps","defaultCommitAdapter","flipAction","createElement","FlipXIcon","FlipYIcon","deps","poseDescriptorOf","requiresSelection"],"sources":["../../../../src/interactions/actions/defaults/flip.ts"],"sourcesContent":["import type { Scene } from 'core/scene/types';\nimport { createElement } from 'react';\nimport { FlipXIcon, FlipYIcon } from './icons/editIcons';\nimport type { Op } from 'core/ops/types';\nimport { multiply, rotate, translate, type Mat3 } from '@weasel-js/geom';\nimport { createTransformOp } from 'core/ops/transform';\nimport type { PoseDescriptor } from '../resize/geometry';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport {\n flipPoseAboutBounds,\n flipPoseViaDescriptor,\n type FlipAxis,\n type FlipPivot,\n} from '../flip/helpers';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport { visualBoundsViaDescriptor } from '../align/align';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { actionReferenceSources, resolveSpatialReference } from '../spatialReference';\nimport type { PointerContextValue } from 'features/pointer/PointerContext';\nimport type { RectPose } from 'core/scene/types';\nimport type { Action } from '@weasel-js/routing';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport { requiresSelection } from './requiresSelection';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { geometryDataOp, type GeometryProjection } from '../geometryProjection';\n\n/**\n * The world mirror about `axis` through the center of `pivotWorld`, written in\n * the frame `id` stores its data in. `geometryProjection.transform` is given\n * the affine the node's own pose underwent, and data-held geometry is local —\n * so a world mirror has to be conjugated by the frame or an asymmetric shape\n * reflects across the wrong line while its pose lands correctly.\n *\n * The frame's turn comes back as the rotation on a rebased zero-size pose at\n * the pivot point, so no composition strategy has to expose one.\n */\nfunction localMirror(\n axis: FlipAxis,\n frame: PoseFrame<unknown>,\n id: Parameters<PoseFrame<unknown>['local']>[0],\n pivotWorld: { x: number; y: number; width: number; height: number },\n): Mat3 {\n const at = frame.local(id, {\n x: pivotWorld.x + pivotWorld.width / 2,\n y: pivotWorld.y + pivotWorld.height / 2,\n width: 0,\n height: 0,\n rotation: 0,\n }) as RectPose;\n const theta = at.rotation ?? 0;\n const mirror: Mat3 = axis === 'x' ? [-1, 0, 0, 1, 0, 0] : [1, 0, 0, -1, 0, 0];\n const turned = multiply(multiply(rotate(theta), mirror), rotate(-theta));\n return multiply(translate(at.x, at.y), multiply(turned, translate(-at.x, -at.y)));\n}\n\n/**\n * Flip the current selection in `scene` along `axis`, using the\n * `poseDescriptor` dep. Called from `flipAction.invoker.run`.\n *\n * Builds one `createTransformOp` per selected node (from = pre-flip pose,\n * to = flipped pose) and routes the batch through the consumer commit hook.\n * Mirrors `moveAction`/`nudge`'s `commitOps` helper: when `deps.applyOps` is\n * present the ops route through the consumer's history (one undo entry there);\n * otherwise they apply straight to the scene's own history via\n * `scene.applyBatch` + `defaultCommitAdapter`. Either way the whole flip is a\n * single batch → one undo entry, preserving the old `scene.batch('Flip', …)`\n * semantics exactly.\n *\n * Pivot: `'each'` (per-item own AABB) unless the caller passes `'union'`,\n * which mirrors every pose about the selection's *visual* union AABB — the\n * rotated ink extents — so items swap sides as well as reflect, or a\n * `SpatialReference`, which mirrors about the center of what it names. A rotated\n * member's own mirrored position needs no expansion: `axisAlignedBounds`\n * keeps the stored box's centre, so mirroring either box lands the same place.\n */\nfunction flipSelection(\n selection: SelectionApi,\n scene: Scene<unknown, string, unknown>,\n axis: FlipAxis,\n pivot: FlipPivot,\n applyOps: ((ops: Op[], label: string) => void) | undefined,\n geometryProjection: GeometryProjection | undefined,\n geom: PoseDescriptor<unknown>,\n poseComposition: unknown,\n params: Record<string, unknown> | undefined,\n pointer: PointerContextValue | undefined,\n): void {\n const ids = selection.get();\n if (ids.length === 0) return;\n const frame = scenePoseFrame(scene, poseComposition);\n\n const worlds = new Map<string, unknown>();\n for (const id of ids) {\n if (scene.get(id) !== undefined) worlds.set(id as string, frame.world(id));\n }\n let sharedPivot: ReturnType<typeof unionAABB> = null;\n if (pivot === 'union') {\n sharedPivot = unionAABB([...worlds.values()].map((p) => visualBoundsViaDescriptor(p, geom)));\n } else if (pivot !== 'each') {\n sharedPivot = resolveSpatialReference(pivot, actionReferenceSources(params, pointer, scene, frame, geom));\n if (sharedPivot === null) return;\n }\n\n // Read poses BEFORE building ops so each op's `from` is the pre-flip value\n // (the same value the old direct mutation captured implicitly).\n const ops: Op[] = [];\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const world = worlds.get(id as string)!;\n const flipped = sharedPivot\n ? flipPoseAboutBounds(world, axis, geom, sharedPivot)\n : flipPoseViaDescriptor(world, axis, geom);\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from: node.pose,\n to: frame.local(id, flipped),\n label: 'Flip',\n }));\n // Mirror the data-held geometry about the SAME pivot the pose flip uses,\n // so a wired geometryProjection reflects the contents in lock-step with\n // the frame. A pose-only flip of a bare-AABB pose is identity, so this\n // data op is the only way an asymmetric shape actually mirrors.\n const g = sharedPivot ?? geom.getBounds(world);\n const dataOp = geometryDataOp(\n geometryProjection,\n { id: id as string, data: node.data, pose: node.pose },\n localMirror(axis, frame, id, g),\n 'Flip',\n );\n if (dataOp) ops.push(dataOp);\n }\n if (ops.length === 0) return;\n\n if (applyOps) applyOps(ops, 'Flip');\n else scene.applyBatch(ops, 'Flip', defaultCommitAdapter(scene, selection.adapterMethods));\n}\n\n/**\n * @experimental\n * Static descriptor for the unified `flip` Action.\n *\n * Collapses the old `flip.horizontal` and `flip.vertical` pair into one action\n * with two parametric gesture bindings. The axis (`'x'` | `'y'`) is carried in\n * `opts.params.axis` and forwarded to `invoker.run` by the dispatcher, as is\n * the optional `opts.params.pivot` (a `FlipPivot`, default `'each'`).\n *\n * Requires dep-schema entries: `selection`, `scene`.\n */\nexport const flipAction: Action & { requires: string[] } = {\n id: 'flip',\n label: 'Flip',\n group: 'flip',\n variants: [\n { key: 'x', label: 'Flip Horizontal', icon: createElement(FlipXIcon), params: { axis: 'x' } },\n { key: 'y', label: 'Flip Vertical', icon: createElement(FlipYIcon), params: { axis: 'y' } },\n ],\n defaultBinding: [\n { spec: { kind: 'key', key: ['h', 'H'], mods: { shift: true } }, opts: { params: { axis: 'x' } } },\n { spec: { kind: 'key', key: ['v', 'V'], mods: { shift: true } }, opts: { params: { axis: 'y' } } },\n ],\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'applyOps', 'geometryProjection', 'poseDescriptor', 'poseComposition', 'pointer'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const axis = (params?.axis as FlipAxis | undefined) ?? 'x';\n const pivot = (params?.pivot as FlipPivot | undefined) ?? 'each';\n const selection = deps.selection as SelectionApi | undefined;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n const geometryProjection = deps.geometryProjection as GeometryProjection | undefined;\n if (!selection || !scene) return;\n flipSelection(\n selection, scene, axis, pivot, applyOps, geometryProjection,\n poseDescriptorOf(deps.poseDescriptor), deps.poseComposition,\n params, deps.pointer as PointerContextValue | undefined,\n );\n },\n },\n enabled: requiresSelection,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"flip.js","names":["localMirror","at","frame","id","pivotWorld","theta","mirror","axis","turned","multiply","rotate","translate","flipSelection","ids","selection","scenePoseFrame","scene","poseComposition","worlds","sharedPivot","pivot","unionAABB","visualBoundsViaDescriptor","p","geom","resolveSpatialReference","actionReferenceSources","params","pointer","ops","node","world","flipped","flipPoseAboutBounds","flipPoseViaDescriptor","createTransformOp","g","dataOp","geometryDataOp","geometryProjection","applyOps","defaultCommitAdapter","flipAction","createElement","FlipXIcon","FlipYIcon","deps","poseDescriptorOf","requiresSelection"],"sources":["../../../../src/interactions/actions/defaults/flip.ts"],"sourcesContent":["import type { Scene } from 'core/scene/types';\nimport { createElement } from 'react';\nimport { FlipXIcon, FlipYIcon } from './icons/editIcons';\nimport type { Op } from 'core/ops/types';\nimport { multiply, rotate, translate, type Mat3 } from '@weasel-js/geom';\nimport { createTransformOp } from 'core/ops/transform';\nimport type { PoseDescriptor } from '../resize/geometry';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport {\n flipPoseAboutBounds,\n flipPoseViaDescriptor,\n type FlipAxis,\n type FlipPivot,\n} from '../flip/helpers';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport { visualBoundsViaDescriptor } from '../align/align';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { actionReferenceSources, resolveSpatialReference } from '../spatialReference';\nimport type { PointerContextValue } from 'features/pointer/PointerContext';\nimport type { RectPose } from 'core/scene/types';\nimport type { Action } from '@weasel-js/routing';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport { requiresSelection } from './requiresSelection';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { geometryDataOp, type GeometryProjection } from '../geometryProjection';\n\n/**\n * The world mirror about `axis` through the center of `pivotWorld`, written in\n * the frame `id` stores its data in. `geometryProjection.transform` is given\n * the affine the node's own pose underwent, and data-held geometry is local —\n * so a world mirror has to be conjugated by the frame or an asymmetric shape\n * reflects across the wrong line while its pose lands correctly.\n *\n * The frame's turn comes back as the rotation on a rebased zero-size pose at\n * the pivot point, so no composition strategy has to expose one.\n */\nfunction localMirror(\n axis: FlipAxis,\n frame: PoseFrame<unknown>,\n id: Parameters<PoseFrame<unknown>['local']>[0],\n pivotWorld: { x: number; y: number; width: number; height: number },\n): Mat3 {\n const at = frame.local(id, {\n x: pivotWorld.x + pivotWorld.width / 2,\n y: pivotWorld.y + pivotWorld.height / 2,\n width: 0,\n height: 0,\n rotation: 0,\n }) as RectPose;\n const theta = at.rotation ?? 0;\n const mirror: Mat3 = axis === 'x' ? [-1, 0, 0, 1, 0, 0] : [1, 0, 0, -1, 0, 0];\n const turned = multiply(multiply(rotate(theta), mirror), rotate(-theta));\n return multiply(translate(at.x, at.y), multiply(turned, translate(-at.x, -at.y)));\n}\n\n/**\n * Flip the current selection in `scene` along `axis`, using the\n * `poseDescriptor` dep. Called from `flipAction.invoker.run`.\n *\n * Builds one `createTransformOp` per selected node (from = pre-flip pose,\n * to = flipped pose) and routes the batch through the consumer commit hook.\n * Mirrors `moveAction`/`nudge`'s `commitOps` helper: when `deps.applyOps` is\n * present the ops route through the consumer's history (one undo entry there);\n * otherwise they apply straight to the scene's own history via\n * `scene.applyBatch` + `defaultCommitAdapter`. Either way the whole flip is a\n * single batch → one undo entry, preserving the old `scene.batch('Flip', …)`\n * semantics exactly.\n *\n * Pivot: `'each'` (per-item own AABB) unless the caller passes `'union'`,\n * which mirrors every pose about the selection's *visual* union AABB — the\n * rotated ink extents — so items swap sides as well as reflect, or a\n * `SpatialReference`, which mirrors about the center of what it names. A rotated\n * member's own mirrored position needs no expansion: `axisAlignedBounds`\n * keeps the stored box's centre, so mirroring either box lands the same place.\n */\nfunction flipSelection(\n selection: SelectionApi,\n scene: Scene<unknown, string, unknown>,\n axis: FlipAxis,\n pivot: FlipPivot,\n applyOps: ((ops: Op[], label: string) => void) | undefined,\n geometryProjection: GeometryProjection | undefined,\n geom: PoseDescriptor<unknown>,\n poseComposition: unknown,\n params: Record<string, unknown> | undefined,\n pointer: PointerContextValue | undefined,\n): void {\n const ids = selection.get();\n if (ids.length === 0) return;\n const frame = scenePoseFrame(scene, poseComposition);\n\n const worlds = new Map<string, unknown>();\n for (const id of ids) {\n if (scene.get(id) !== undefined) worlds.set(id as string, frame.world(id));\n }\n let sharedPivot: ReturnType<typeof unionAABB> = null;\n if (pivot === 'union') {\n sharedPivot = unionAABB([...worlds.values()].map((p) => visualBoundsViaDescriptor(p, geom)));\n } else if (pivot !== 'each') {\n sharedPivot = resolveSpatialReference(pivot, actionReferenceSources(params, pointer, scene, frame, geom));\n if (sharedPivot === null) return;\n }\n\n // Read poses BEFORE building ops so each op's `from` is the pre-flip value\n // (the same value the old direct mutation captured implicitly).\n const ops: Op[] = [];\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const world = worlds.get(id as string)!;\n const flipped = sharedPivot\n ? flipPoseAboutBounds(world, axis, geom, sharedPivot)\n : flipPoseViaDescriptor(world, axis, geom);\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from: node.pose,\n to: frame.local(id, flipped),\n label: 'Flip',\n }));\n // Mirror the data-held geometry about the SAME pivot the pose flip uses,\n // so a wired geometryProjection reflects the contents in lock-step with\n // the frame. A pose-only flip of a bare-AABB pose is identity, so this\n // data op is the only way an asymmetric shape actually mirrors.\n const g = sharedPivot ?? geom.getBounds(world);\n const dataOp = geometryDataOp(\n geometryProjection,\n { id: id as string, data: node.data, pose: node.pose },\n localMirror(axis, frame, id, g),\n 'Flip',\n );\n if (dataOp) ops.push(dataOp);\n }\n if (ops.length === 0) return;\n\n if (applyOps) applyOps(ops, 'Flip');\n else scene.applyBatch(ops, 'Flip', defaultCommitAdapter(scene, selection.adapterMethods));\n}\n\n/**\n * @experimental\n * Static descriptor for the unified `flip` Action.\n *\n * Collapses the old `flip.horizontal` and `flip.vertical` pair into one action\n * with two parametric gesture bindings. The axis (`'x'` | `'y'`) is carried in\n * `opts.params.axis` and forwarded to `invoker.run` by the dispatcher, as is\n * the optional `opts.params.pivot` (a `FlipPivot`, default `'each'`).\n *\n * Requires dep-schema entries: `selection`, `scene`.\n */\nexport const flipAction: Action & { requires: string[] } = {\n id: 'flip',\n label: 'Flip',\n group: 'flip',\n variants: [\n { key: 'x', label: 'Flip Horizontal', icon: createElement(FlipXIcon), params: { axis: 'x' } },\n { key: 'y', label: 'Flip Vertical', icon: createElement(FlipYIcon), params: { axis: 'y' } },\n ],\n defaultBinding: [\n { spec: { kind: 'key', key: ['h', 'H'], mods: { shift: true } }, opts: { params: { axis: 'x' } } },\n { spec: { kind: 'key', key: ['v', 'V'], mods: { shift: true } }, opts: { params: { axis: 'y' } } },\n ],\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'applyOps', 'geometryProjection', 'poseDescriptor', 'poseComposition', 'pointer'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const axis = (params?.axis as FlipAxis | undefined) ?? 'x';\n const pivot = (params?.pivot as FlipPivot | undefined) ?? 'each';\n const selection = deps.selection;\n const scene = deps.scene;\n const applyOps = deps.applyOps;\n const geometryProjection = deps.geometryProjection;\n if (!selection || !scene) return;\n flipSelection(\n selection, scene, axis, pivot, applyOps, geometryProjection,\n poseDescriptorOf(deps.poseDescriptor), deps.poseComposition,\n params, deps.pointer,\n );\n },\n },\n enabled: requiresSelection,\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group.js","names":["containerIdCounter","freshContainerId","asNodeId","groupAction","createElement","GroupIcon","selection","deps","scene","applyOps","ids","nodes","id","n","layer","parent","d","poseDescriptorOf","frame","scenePoseFrame","worlds","node","union","unionBounds","visualBoundsViaDescriptor","p","containerWorldPose","pose","absolutePoses","derive","UNION_OF_CHILDREN","unionOfChildren","containerId","ops","createInsertOp","createReparentOp","createTransformOp","defaultCommitAdapter","requiresSelection","ungroupAction","UngroupIcon","containers","freed","container","baseIndex","children","childId","i","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/group.ts"],"sourcesContent":["import type { Node, NodeId, Scene } from 'core/scene/types';\nimport { createElement } from 'react';\nimport { GroupIcon, UngroupIcon } from './icons/editIcons';\nimport { asNodeId } from 'core/scene/types';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type { Op } from 'core/ops/types';\nimport { createInsertOp } from 'core/ops/create';\nimport { createReparentOp } from 'core/ops/reparent';\nimport { unionBounds } from 'core/geometry/unionBounds';\nimport { unionOfChildren, UNION_OF_CHILDREN } from 'core/scene/kitRegistry';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport { visualBoundsViaDescriptor } from '../resize/geometry';\nimport { createTransformOp } from 'core/ops/transform';\nimport { ActionDisabledReason, type Action } from '@weasel-js/routing';\nimport { requiresSelection } from './requiresSelection';\nimport { scenePoseFrame } from '../poseFrame';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\n\n/** Mint a fresh NodeId for a new container. The old `scene.add(spec)` path\n * (no explicit id) let the scene generate a random id, so the produced id\n * was never deterministic/observable — pre-generating one here to feed the\n * insert op preserves behavior. Mirrors the scene's default id scheme\n * (`n{counter}-{random}`), which `core/scene/scene.ts` keeps module-private.\n * Same approach as `cloneAction`. */\nlet containerIdCounter = 0;\nfunction freshContainerId(): NodeId {\n return asNodeId(`n${(containerIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);\n}\n\n/**\n * @experimental\n * Static descriptor for the `group` Action.\n *\n * \"Group\" here means a structural `ContainerNode` — the real group that\n * round-trips to SVG `<g>` — not the legacy membership-list. `run` creates a\n * fresh container and reparents the current selection under it as a single\n * undoable batch.\n *\n * Grouping never moves a member on screen. The container takes the world\n * envelope of its members' ink; each member is then re-expressed in that\n * container's frame, which is a no-op only when the scene composes poses by\n * identity.\n *\n * Under identity the container's pose is also *derived* from its members, so\n * it tracks one that moves later instead of freezing at the moment the group\n * was made. Under any other composition that derivation is circular — the\n * members' poses are expressed in the container's own frame — so the container\n * keeps the authored envelope instead. Making it track its members again needs\n * its own decision; see the group-as-frame design doc.\n */\nexport const groupAction: Action & { requires: string[] } = {\n id: 'group',\n label: 'Group',\n icon: createElement(GroupIcon),\n group: 'structure',\n defaultBinding: { kind: 'key', key: 'g', mods: { mod: true } },\n eligible: { capability: 'edits-page' },\n requires: ['scene', 'selection', 'applyOps', 'poseDescriptor', 'poseComposition'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const selection = deps.selection as SelectionApi | undefined;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n if (!selection || !scene) return;\n\n const ids = selection.get();\n if (ids.length === 0) return;\n\n // Resolve nodes; drop any that have gone missing.\n const nodes = ids\n .map((id) => scene.get(id as NodeId))\n .filter((n): n is NonNullable<typeof n> => n !== undefined);\n if (nodes.length === 0) return;\n\n // Members must share a layer — containers live on a single layer.\n const layers = new Set(nodes.map((n) => n.layer));\n if (layers.size > 1) return;\n const layer = nodes[0]!.layer;\n\n // Nest under the shared parent if the members agree, else at the root.\n const parents = new Set(nodes.map((n) => n.parent));\n const parent = parents.size === 1 ? nodes[0]!.parent : null;\n\n // The container takes the world envelope of its members' ink. The\n // authored pose is only the fallback (an emptied group, a lost registry\n // key); under identity the derived union is what the scene shows.\n const d = poseDescriptorOf(deps.poseDescriptor);\n const frame = scenePoseFrame(scene, deps.poseComposition);\n const worlds = new Map<NodeId, unknown>();\n for (const node of nodes) worlds.set(node.id, frame.world(node.id));\n // Visual, not stored, bounds: a turned member's ink reaches outside its\n // own box, and the container has to cover what is drawn.\n const union = unionBounds([...worlds.values()].map((p) => visualBoundsViaDescriptor(p, d)));\n const containerWorldPose = union === null\n ? nodes[0]!.pose\n : d.fromBounds(union, nodes[0]!.pose);\n const pose = frame.localUnder(parent, containerWorldPose);\n // Under identity the container is an envelope that tracks its members;\n // under a composing strategy it is a frame they are re-expressed in.\n const absolutePoses = frame.pc.closure === 'identity';\n const derive = scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren;\n\n // Build ops mirroring the old direct mutations, in the same order the\n // `scene.batch('Group', …)` body produced them:\n // 1. insert the fresh container (the old `scene.add` minted a random\n // id; we pre-generate one so the insert op carries a full node —\n // the id was never observable, so behavior is preserved);\n // 2. reparent each selected id under the new container, in selection\n // order, capturing each node's PRE-mutation parent as `from`\n // (the op captures the sibling slot itself, on apply).\n // The container must exist before any child reparents into it, so the\n // insert op is emitted first. A member's pose then follows its reparent,\n // re-expressed in the container's frame so the member does not move.\n const containerId = freshContainerId();\n const ops: Op[] = [\n createInsertOp<Node<unknown, string, unknown>>({\n node: {\n id: containerId,\n kind: 'container',\n layer,\n pose: pose as unknown,\n data: {} as unknown,\n parent: parent ?? null,\n ...(absolutePoses ? { dependsOn: 'children', derivePose: derive } : {}),\n } as Node<unknown, string, unknown>,\n label: 'Group',\n }),\n ];\n for (const node of nodes) {\n ops.push(createReparentOp({\n id: node.id as string,\n fromParentId: (node.parent ?? null) as string | null,\n toParentId: containerId as string,\n label: 'Group',\n }));\n if (absolutePoses) continue;\n ops.push(createTransformOp<unknown>({\n id: node.id as string,\n from: node.pose,\n to: frame.pc.decompose(containerWorldPose, worlds.get(node.id)!),\n label: 'Group',\n }));\n }\n\n // Route the batch through the consumer `applyOps` hook when present (so\n // an app with its own history captures the group as a single undo\n // entry); otherwise commit straight to the scene's own history via\n // `scene.applyBatch`. One applyOps / applyBatch call = one undo entry,\n // matching the prior single `scene.batch('Group', …)`.\n if (applyOps) applyOps(ops, 'Group');\n else scene.applyBatch(ops, 'Group', defaultCommitAdapter(scene, selection.adapterMethods));\n\n // Selection update is selection state, not a scene op — set it after the\n // commit regardless of which path ran (the old code set it inside the\n // batch; it was never an undoable scene mutation).\n selection.set([containerId]);\n },\n },\n enabled: requiresSelection,\n};\n\n/**\n * @experimental\n * Static descriptor for the `ungroup` Action.\n *\n * \"Group\" here means a structural `ContainerNode`. `run` dissolves each\n * selected container by reparenting its children up to the container's own\n * parent (preserving z-order) and removing the now-empty container, as a single\n * undoable batch. Selected non-container nodes are ignored. The freed children\n * become the new selection.\n *\n * A child's pose is expressed in the container's frame, so each one is\n * re-expressed in the frame it lands in and nothing moves on screen.\n */\nexport const ungroupAction: Action & { requires: string[] } = {\n id: 'ungroup',\n label: 'Ungroup',\n icon: createElement(UngroupIcon),\n group: 'structure',\n defaultBinding: { kind: 'key', key: 'g', mods: { mod: true, shift: true } },\n eligible: { capability: 'edits-page' },\n requires: ['scene', 'selection', 'poseComposition'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const selection = deps.selection as SelectionApi | undefined;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n if (!selection || !scene) return;\n\n const ids = selection.get();\n if (ids.length === 0) return;\n\n const containers = ids.filter((id) => scene.get(id as NodeId)?.kind === 'container');\n if (containers.length === 0) return;\n\n const frame = scenePoseFrame(scene, deps.poseComposition);\n const absolutePoses = frame.pc.closure === 'identity';\n\n scene.batch('Ungroup', () => {\n const freed: NodeId[] = [];\n for (const containerId of containers) {\n const container = scene.get(containerId as NodeId);\n if (!container || container.kind !== 'container') continue;\n\n const parent = container.parent ?? null;\n const baseIndex =\n parent === null\n ? scene.roots.indexOf(containerId as NodeId)\n : scene.childrenOf(parent).indexOf(containerId as NodeId);\n\n const children = [...scene.childrenOf(containerId as NodeId)];\n // Read every world pose before the first move: the frame resolves\n // through the live scene, and the container is about to leave it.\n const worlds = new Map<NodeId, unknown>();\n if (!absolutePoses) {\n for (const childId of children) worlds.set(childId, frame.world(childId));\n }\n children.forEach((childId, i) => {\n scene.move(childId, parent, baseIndex < 0 ? undefined : baseIndex + i);\n if (!absolutePoses) {\n scene.setPose(childId, frame.localUnder(parent, worlds.get(childId)!) as never);\n }\n freed.push(childId);\n });\n\n scene.remove(containerId as NodeId);\n }\n selection.set(freed);\n });\n },\n },\n enabled: (deps) => {\n const selection = deps?.selection as SelectionApi | undefined;\n const scene = deps?.scene as Scene<unknown, string, unknown> | undefined;\n const any = selection?.get().some((id) => scene?.get(id as NodeId)?.kind === 'container');\n return any ? true : ActionDisabledReason.NotApplicable;\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"group.js","names":["containerIdCounter","freshContainerId","asNodeId","groupAction","createElement","GroupIcon","selection","deps","scene","applyOps","ids","nodes","id","n","layer","parent","d","poseDescriptorOf","frame","scenePoseFrame","worlds","node","union","unionBounds","visualBoundsViaDescriptor","p","containerWorldPose","pose","absolutePoses","derive","UNION_OF_CHILDREN","unionOfChildren","containerId","ops","createInsertOp","createReparentOp","createTransformOp","defaultCommitAdapter","requiresSelection","ungroupAction","UngroupIcon","containers","freed","container","baseIndex","children","childId","i","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/group.ts"],"sourcesContent":["import type { Node, NodeId } from 'core/scene/types';\nimport { createElement } from 'react';\nimport { GroupIcon, UngroupIcon } from './icons/editIcons';\nimport { asNodeId } from 'core/scene/types';\nimport type { Op } from 'core/ops/types';\nimport { createInsertOp } from 'core/ops/create';\nimport { createReparentOp } from 'core/ops/reparent';\nimport { unionBounds } from 'core/geometry/unionBounds';\nimport { unionOfChildren, UNION_OF_CHILDREN } from 'core/scene/kitRegistry';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport { visualBoundsViaDescriptor } from '../resize/geometry';\nimport { createTransformOp } from 'core/ops/transform';\nimport { ActionDisabledReason, type Action } from '@weasel-js/routing';\nimport { requiresSelection } from './requiresSelection';\nimport { scenePoseFrame } from '../poseFrame';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\n\n/** Mint a fresh NodeId for a new container. The old `scene.add(spec)` path\n * (no explicit id) let the scene generate a random id, so the produced id\n * was never deterministic/observable — pre-generating one here to feed the\n * insert op preserves behavior. Mirrors the scene's default id scheme\n * (`n{counter}-{random}`), which `core/scene/scene.ts` keeps module-private.\n * Same approach as `cloneAction`. */\nlet containerIdCounter = 0;\nfunction freshContainerId(): NodeId {\n return asNodeId(`n${(containerIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);\n}\n\n/**\n * @experimental\n * Static descriptor for the `group` Action.\n *\n * \"Group\" here means a structural `ContainerNode` — the real group that\n * round-trips to SVG `<g>` — not the legacy membership-list. `run` creates a\n * fresh container and reparents the current selection under it as a single\n * undoable batch.\n *\n * Grouping never moves a member on screen. The container takes the world\n * envelope of its members' ink; each member is then re-expressed in that\n * container's frame, which is a no-op only when the scene composes poses by\n * identity.\n *\n * Under identity the container's pose is also *derived* from its members, so\n * it tracks one that moves later instead of freezing at the moment the group\n * was made. Under any other composition that derivation is circular — the\n * members' poses are expressed in the container's own frame — so the container\n * keeps the authored envelope instead. Making it track its members again needs\n * its own decision; see the group-as-frame design doc.\n */\nexport const groupAction: Action & { requires: string[] } = {\n id: 'group',\n label: 'Group',\n icon: createElement(GroupIcon),\n group: 'structure',\n defaultBinding: { kind: 'key', key: 'g', mods: { mod: true } },\n eligible: { capability: 'edits-page' },\n requires: ['scene', 'selection', 'applyOps', 'poseDescriptor', 'poseComposition'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const selection = deps.selection;\n const scene = deps.scene;\n const applyOps = deps.applyOps;\n if (!selection || !scene) return;\n\n const ids = selection.get();\n if (ids.length === 0) return;\n\n // Resolve nodes; drop any that have gone missing.\n const nodes = ids\n .map((id) => scene.get(id as NodeId))\n .filter((n): n is NonNullable<typeof n> => n !== undefined);\n if (nodes.length === 0) return;\n\n // Members must share a layer — containers live on a single layer.\n const layers = new Set(nodes.map((n) => n.layer));\n if (layers.size > 1) return;\n const layer = nodes[0]!.layer;\n\n // Nest under the shared parent if the members agree, else at the root.\n const parents = new Set(nodes.map((n) => n.parent));\n const parent = parents.size === 1 ? nodes[0]!.parent : null;\n\n // The container takes the world envelope of its members' ink. The\n // authored pose is only the fallback (an emptied group, a lost registry\n // key); under identity the derived union is what the scene shows.\n const d = poseDescriptorOf(deps.poseDescriptor);\n const frame = scenePoseFrame(scene, deps.poseComposition);\n const worlds = new Map<NodeId, unknown>();\n for (const node of nodes) worlds.set(node.id, frame.world(node.id));\n // Visual, not stored, bounds: a turned member's ink reaches outside its\n // own box, and the container has to cover what is drawn.\n const union = unionBounds([...worlds.values()].map((p) => visualBoundsViaDescriptor(p, d)));\n const containerWorldPose = union === null\n ? nodes[0]!.pose\n : d.fromBounds(union, nodes[0]!.pose);\n const pose = frame.localUnder(parent, containerWorldPose);\n // Under identity the container is an envelope that tracks its members;\n // under a composing strategy it is a frame they are re-expressed in.\n const absolutePoses = frame.pc.closure === 'identity';\n const derive = scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren;\n\n // Build ops mirroring the old direct mutations, in the same order the\n // `scene.batch('Group', …)` body produced them:\n // 1. insert the fresh container (the old `scene.add` minted a random\n // id; we pre-generate one so the insert op carries a full node —\n // the id was never observable, so behavior is preserved);\n // 2. reparent each selected id under the new container, in selection\n // order, capturing each node's PRE-mutation parent as `from`\n // (the op captures the sibling slot itself, on apply).\n // The container must exist before any child reparents into it, so the\n // insert op is emitted first. A member's pose then follows its reparent,\n // re-expressed in the container's frame so the member does not move.\n const containerId = freshContainerId();\n const ops: Op[] = [\n createInsertOp<Node<unknown, string, unknown>>({\n node: {\n id: containerId,\n kind: 'container',\n layer,\n pose: pose as unknown,\n data: {} as unknown,\n parent: parent ?? null,\n ...(absolutePoses ? { dependsOn: 'children', derivePose: derive } : {}),\n } as Node<unknown, string, unknown>,\n label: 'Group',\n }),\n ];\n for (const node of nodes) {\n ops.push(createReparentOp({\n id: node.id as string,\n fromParentId: (node.parent ?? null) as string | null,\n toParentId: containerId as string,\n label: 'Group',\n }));\n if (absolutePoses) continue;\n ops.push(createTransformOp<unknown>({\n id: node.id as string,\n from: node.pose,\n to: frame.pc.decompose(containerWorldPose, worlds.get(node.id)!),\n label: 'Group',\n }));\n }\n\n // Route the batch through the consumer `applyOps` hook when present (so\n // an app with its own history captures the group as a single undo\n // entry); otherwise commit straight to the scene's own history via\n // `scene.applyBatch`. One applyOps / applyBatch call = one undo entry,\n // matching the prior single `scene.batch('Group', …)`.\n if (applyOps) applyOps(ops, 'Group');\n else scene.applyBatch(ops, 'Group', defaultCommitAdapter(scene, selection.adapterMethods));\n\n // Selection update is selection state, not a scene op — set it after the\n // commit regardless of which path ran (the old code set it inside the\n // batch; it was never an undoable scene mutation).\n selection.set([containerId]);\n },\n },\n enabled: requiresSelection,\n};\n\n/**\n * @experimental\n * Static descriptor for the `ungroup` Action.\n *\n * \"Group\" here means a structural `ContainerNode`. `run` dissolves each\n * selected container by reparenting its children up to the container's own\n * parent (preserving z-order) and removing the now-empty container, as a single\n * undoable batch. Selected non-container nodes are ignored. The freed children\n * become the new selection.\n *\n * A child's pose is expressed in the container's frame, so each one is\n * re-expressed in the frame it lands in and nothing moves on screen.\n */\nexport const ungroupAction: Action & { requires: string[] } = {\n id: 'ungroup',\n label: 'Ungroup',\n icon: createElement(UngroupIcon),\n group: 'structure',\n defaultBinding: { kind: 'key', key: 'g', mods: { mod: true, shift: true } },\n eligible: { capability: 'edits-page' },\n requires: ['scene', 'selection', 'poseComposition'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const selection = deps.selection;\n const scene = deps.scene;\n if (!selection || !scene) return;\n\n const ids = selection.get();\n if (ids.length === 0) return;\n\n const containers = ids.filter((id) => scene.get(id as NodeId)?.kind === 'container');\n if (containers.length === 0) return;\n\n const frame = scenePoseFrame(scene, deps.poseComposition);\n const absolutePoses = frame.pc.closure === 'identity';\n\n scene.batch('Ungroup', () => {\n const freed: NodeId[] = [];\n for (const containerId of containers) {\n const container = scene.get(containerId as NodeId);\n if (!container || container.kind !== 'container') continue;\n\n const parent = container.parent ?? null;\n const baseIndex =\n parent === null\n ? scene.roots.indexOf(containerId as NodeId)\n : scene.childrenOf(parent).indexOf(containerId as NodeId);\n\n const children = [...scene.childrenOf(containerId as NodeId)];\n // Read every world pose before the first move: the frame resolves\n // through the live scene, and the container is about to leave it.\n const worlds = new Map<NodeId, unknown>();\n if (!absolutePoses) {\n for (const childId of children) worlds.set(childId, frame.world(childId));\n }\n children.forEach((childId, i) => {\n scene.move(childId, parent, baseIndex < 0 ? undefined : baseIndex + i);\n if (!absolutePoses) {\n scene.setPose(childId, frame.localUnder(parent, worlds.get(childId)!) as never);\n }\n freed.push(childId);\n });\n\n scene.remove(containerId as NodeId);\n }\n selection.set(freed);\n });\n },\n },\n enabled: (deps) => {\n const selection = deps?.selection;\n const scene = deps?.scene;\n const any = selection?.get().some((id) => scene?.get(id as NodeId)?.kind === 'container');\n return any ? true : ActionDisabledReason.NotApplicable;\n },\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest.js","names":["ANY_MODS","ingestAction","items","params","ingestion","deps","insert","scene","selection","applyOps","ctx","ops","label","defaultCommitAdapter","err"],"sources":["../../../../src/interactions/actions/defaults/ingest.ts"],"sourcesContent":["/**\n * @experimental\n * `ingest` — routes externally-arrived content (OS drop, clipboard paste,\n * file picker / `CanvasExtensionApi.ingest`) through the content-handler\n * registry (`features/ingestion/contentHandlers`).\n *\n * Registered ambient with both `drop` and `paste` default bindings, so\n * external content works regardless of the active tool. A tool can still\n * bind `{ kind: 'drop', types: [...] }` itself to intercept (active scope\n * beats ambient).\n *\n * Both bindings declare every modifier `'optional'`: strict matching would\n * otherwise reject Cmd+V paste and macOS Option-drag drops outright.\n *\n * The dispatcher merges event data into `params`: `items` (IngestItem[]),\n * `via` ('drop' | 'paste'), plus `worldX`/`worldY` for drops. Absent\n * coords → `point: null`; handlers pick their own placement policy.\n */\nimport type {
|
|
1
|
+
{"version":3,"file":"ingest.js","names":["ANY_MODS","ingestAction","items","params","ingestion","deps","insert","scene","selection","applyOps","ctx","ops","label","defaultCommitAdapter","err"],"sources":["../../../../src/interactions/actions/defaults/ingest.ts"],"sourcesContent":["/**\n * @experimental\n * `ingest` — routes externally-arrived content (OS drop, clipboard paste,\n * file picker / `CanvasExtensionApi.ingest`) through the content-handler\n * registry (`features/ingestion/contentHandlers`).\n *\n * Registered ambient with both `drop` and `paste` default bindings, so\n * external content works regardless of the active tool. A tool can still\n * bind `{ kind: 'drop', types: [...] }` itself to intercept (active scope\n * beats ambient).\n *\n * Both bindings declare every modifier `'optional'`: strict matching would\n * otherwise reject Cmd+V paste and macOS Option-drag drops outright.\n *\n * The dispatcher merges event data into `params`: `items` (IngestItem[]),\n * `via` ('drop' | 'paste'), plus `worldX`/`worldY` for drops. Absent\n * coords → `point: null`; handlers pick their own placement policy.\n */\nimport type { Action } from '@weasel-js/routing';\nimport { runIngest, type IngestCtx } from 'features/ingestion/contentHandlers';\nimport type { IngestItem } from '@weasel-js/routing';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\n\nconst ANY_MODS = {\n alt: 'optional', ctrl: 'optional', meta: 'optional', shift: 'optional',\n} as const;\n\nexport const ingestAction: Action & { requires: string[] } = {\n id: 'ingest',\n label: 'Insert external content',\n defaultBinding: [\n { kind: 'drop', mods: ANY_MODS },\n { kind: 'paste', mods: ANY_MODS },\n ],\n requires: ['scene', 'selection', 'insert', 'applyOps', 'ingestion'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const items = params?.items as IngestItem[] | undefined;\n if (!items || items.length === 0) return;\n const ingestion = deps.ingestion;\n const insert = deps.insert;\n const scene = deps.scene;\n const selection = deps.selection;\n if (!ingestion || !insert || !scene || !selection) return;\n const applyOps = deps.applyOps;\n\n const point =\n typeof params?.worldX === 'number' && typeof params?.worldY === 'number'\n ? { x: params.worldX as number, y: params.worldY as number }\n : null;\n\n const ctx: IngestCtx = {\n point,\n viewportWorldRect: () => ingestion.viewportWorldRect(),\n insert,\n applyOps: (ops, label) => {\n if (applyOps) applyOps(ops, label ?? 'Ingest');\n else scene.applyBatch(ops, label ?? 'Ingest', defaultCommitAdapter(scene, selection.adapterMethods));\n },\n scene,\n selection,\n // Snapshot per ingest event — intentional. The dep's live getters are\n // fresh at dispatch time, and one consistent resolver per drop/paste\n // beats half a multi-file drop resolving through a swapped-in one.\n resolveSrc: ingestion.resolveSrc,\n svg: ingestion.svg,\n clipboard: ingestion.clipboard,\n deps,\n };\n // Fire-and-forget: handler errors are caught inside runIngest, but a\n // malformed item (non-string mime via untyped JS) can still throw in\n // the matching path — keep the rejection out of the event loop.\n runIngest(items, ctx).catch((err) => console.warn('weasel ingest:', err));\n },\n },\n // No `eligible` rule (always eligible): external-content arrival is\n // mode-agnostic in v1. Revisit if a capability-restricted mode needs to\n // refuse drops/pastes (the listener-level editable-target guard already\n // keeps text-editing pastes away from the scene).\n enabled: () => true,\n};\n"],"mappings":";;;AAuBA,IAAMA,IAAW;CACf,KAAK;CAAY,MAAM;CAAY,MAAM;CAAY,OAAO;AAC9D,GAEaC,IAAgD;CAC3D,IAAI;CACJ,OAAO;CACP,gBAAgB,CACd;EAAE,MAAM;EAAQ,MAAMD;CAAS,GAC/B;EAAE,MAAM;EAAS,MAAMA;CAAS,CAClC;CACA,UAAU;EAAC;EAAS;EAAa;EAAU;EAAY;CAAW;CAClE,SAAS;EACP,QAAQ;EACR,MAAM,GAAM,MAAW;GACrB,IAAME,IAAQC,GAAQ;GACtB,IAAI,CAACD,KAASA,EAAM,WAAW,GAAG;GAClC,IAAME,IAAYC,EAAK,WACjBC,IAASD,EAAK,QACdE,IAAQF,EAAK,OACbG,IAAYH,EAAK;GACvB,IAAI,CAACD,KAAa,CAACE,KAAU,CAACC,KAAS,CAACC,GAAW;GACnD,IAAMC,IAAWJ,EAAK,UAOhBK,IAAiB;IACrB,OALA,OAAOP,GAAQ,UAAW,YAAY,OAAOA,GAAQ,UAAW,WAC5D;KAAE,GAAGA,EAAO;KAAkB,GAAGA,EAAO;IAAiB,IACzD;IAIJ,yBAAyBC,EAAU,kBAAkB;IACrD;IACA,WAAW,GAAK,MAAU;KACxB,AAAIK,IAAUA,EAASE,GAAKC,KAAS,QAAQ,IACxCL,EAAM,WAAWI,GAAKC,KAAS,UAAUC,EAAqBN,GAAOC,EAAU,cAAc,CAAC;IACrG;IACA;IACA;IAIA,YAAYJ,EAAU;IACtB,KAAKA,EAAU;IACf,WAAWA,EAAU;IACrB;GACF;GAIA,EAAUF,GAAOQ,CAAG,CAAC,CAAC,OAAO,MAAQ,QAAQ,KAAK,kBAAkBI,CAAG,CAAC;EAC1E;CACF;CAKA,eAAe;AACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.js","names":["liveInsertScratch","effectiveOriginMode","nominal","paramsOrigin","altHeld","computeBounds","originMode","dx","currentX","startX","dy","currentY","startY","snapTo15Degrees","end","start","len","step","snapped","GESTURE_KEY","shapeEndpoints","resolved","resolveParams","scratch","kind","\"kind\"","gctx","current","modifiers","mode","\"originMode\"","b","bounds","r","resolveEndpoints","buildExtras","params","sides","\"sides\"","dragAngle","extras","\"rotation\"","\"center\"","\"radius\"","pts","\"points\"","ir","\"innerRadiusRatio\"","\"outerRadius\"","\"samples\"","points","insertAction","inPlane","dep","ctx","selection","snapDep","snap","p","behaviors","opts","readView","gestureViewReader","readPlane","gesturePlaneReader","origin","gestureCtx","startPoint","moveCtx","shapeKindInfo","effectiveExtras","applyUserRotation","reason","d","o","endCtx","reduceBehaviorEnd","commitGestureOps","defaultCommitAdapter","readGestureLifecycle","isCommittableExtent","id","insertLayer","userRotation","ROTATION_SENSITIVITY","insertRotateAction","deltaY"],"sources":["../../../../src/interactions/actions/defaults/insert.ts"],"sourcesContent":["/**\n * `insertAction` — ongoing Action descriptor for drag-to-insert.\n *\n * ## Status: REAL\n *\n * Implements the drag-rect insert logic from `useInsert`:\n * - `start`: validates the `insert` dep and records the drag start point.\n * - `onMove`: tracks the live drag bounds (no scene writes).\n * - `onEnd('commit')`: derives final bounds from drag.start + drag.current;\n * calls `deps.insert.commit(bounds, kind)` to materialise the new node.\n * `kind` comes from `opts.params.kind` (set by the active tool's binding).\n * - `onEnd('cancel')`: no-op.\n *\n * ## Dependencies\n *\n * Requires `insert` dep from DepSchema:\n * `{ commit(bounds, kind): NodeId | null }`\n *\n * `<SceneCanvas>` / `<StandardActionsRegistrar>` should source this dep by\n * delegating to the scene's `add()` with a sensible default data payload for\n * the given `kind`. Override per-consumer for custom node factories.\n *\n * ## Modifiers and snapping\n *\n * Grid snapping comes from the optional `snap` dep, applied to the drag's\n * start and current point (never to freehand pencil samples). The `line`\n * kind additionally honors Shift (constrain to 15°) and reads Alt/center as\n * \"mirror the start around the pointer\" rather than \"grow a symmetric AABB\".\n * Behaviors from the binding's `opts.behaviors` (`InsertBehavior[]` — e.g.\n * `@weasel-js/guides`' `snapToGrid` and `alignInsertBehavior`) run over the two\n * points after those, the way `moveAction` runs its own: `onStart` on the\n * start point, `onMove` on each frame's proposal, `onEnd` on release, where\n * the first answer wins (`null` aborts, `Op[]` commits in place of the\n * insert). `shapeEndpoints` folds them each pump event and `resolveEndpoints`\n * reads the result, so the live preview and the committed node can't\n * disagree.\n *\n * ## What this does NOT wire (vs `useInsert`)\n *\n * - `pointInsert` fallback for click / sub-threshold drags — not wired; a\n * sub-threshold drag produces no insert.\n * - `clickOnly` mode — not applicable to the descriptor model.\n *\n * ## Live preview\n *\n * Insert produces a brand-new node that has no scene id until commit, so\n * the preview-ghost layer (`usePreviewGhostLayer`, keyed by node id) can't\n * paint a silhouette for it. Instead, the action exposes its in-flight\n * geometry via the `overlay()` surface with `kind: 'insertPreview'`. The\n * canvas's `useDispatcherOverlayLayer` rebuilds the shape using the same\n * path builders the commit-time factory uses (`rectPath`, `ellipsePath`,\n * `linePath`, `regularPolygonPath`, `starPath`, polyline / `polygonFromPoints`),\n * so the preview is geometry-faithful to the eventual node.\n *\n * Consumer-defined insert kinds (kinds the kit doesn't know how to draw)\n * skip the preview but still commit normally on release.\n *\n * ## Relationship to `useInsert`\n *\n * `useInsert` calls `adapter.commitInsert(bounds)` which returns a node and\n * dispatches a `createInsertOp`. This descriptor delegates to `deps.insert.commit`\n * which encapsulates both factory + op dispatch in one call. This keeps the\n * dep contract thin and avoids importing `createInsertOp` into the descriptor.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle, BindingOpts, OngoingOverlay, DragSample } from '@weasel-js/routing';\nimport { resolveParams } from '@weasel-js/routing';\nimport type { InsertDep, InsertExtras, SnapDep } from '../depSchema';\nimport type { Op } from 'core/ops/types';\nimport type { Scene } from 'core/scene/types';\nimport type { GestureContext, InsertBehavior, InsertPoint } from '../../gestures/types';\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { View } from 'core/viewport/view';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel } from '../gestureLifecycle';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport type { TextEditDep } from '../depSchema';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { shapeKindInfo } from 'core/shapeKinds';\nimport type { KitInsertShape } from 'core/shapeKinds';\nimport { inPlane, insertLayer } from '../planeInput';\nimport type { PlaneMap } from 'core/viewport/parallax';\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface InsertScratch {\n dep: InsertDep;\n /** Text-edit dep captured at gesture start, for the text kind's\n * drop-into-the-caret step. Stashed rather than read at commit because\n * the dispatcher only builds the deps bag once, on `start` — every later\n * pump event carries `deps: {}`. */\n textEdit: TextEditDep | undefined;\n /** World-space point snapper from the `snap` dep, or identity when the\n * dep isn't registered. Applied to the drag's start and current point so\n * the live preview and the committed geometry agree. Freehand pencil\n * samples are deliberately NOT snapped — a grid-quantized freehand trail\n * is a staircase, not a stroke. */\n snap: (p: { x: number; y: number }) => { x: number; y: number };\n /** The active binding's opts — re-resolved at commit time so thunked\n * params see the latest tool state (e.g. polygon `sides` after ArrowUp). */\n opts: BindingOpts | undefined;\n /** RAW (unsnapped) drag endpoints in world space. `shapeEndpoints`\n * constrains the angle first, THEN aligns the endpoint to the grid, so\n * the documented modifier ordering holds for the preview and the commit. */\n startX: number;\n startY: number;\n currentX: number;\n currentY: number;\n /** Pointer trail accumulated by the dispatcher in world space. Same array\n * reference as `ctx.drag.points`; pencil-kind commits read from this. */\n points: ReadonlyArray<DragSample> | null;\n /** Live Alt-key state from the most recent pump event. Alt INVERTS the\n * binding's nominal `originMode` (corner → center and vice versa), so\n * the user can press / release Alt mid-drag and the preview/commit\n * flip without needing the dispatcher to re-route to a different\n * binding. Captured per-onMove from `ctx.modifiers.alt`. */\n altHeld: boolean;\n /** Live Shift-key state from the most recent pump event. Constrains the\n * `line` kind to 15° increments (the modifier the line tool documents).\n * Captured per-onMove from `ctx.modifiers.shift`. */\n shiftHeld: boolean;\n /** Cleared once `onEnd` runs so subsequent `overlay()` calls report no\n * in-flight preview (mirrors the areaSelect/lasso convention). */\n open: boolean;\n /** Behaviors from `opts.behaviors`; empty when none were bound. */\n behaviors: InsertBehavior<unknown>[];\n /** Reused gesture context handed to behaviors. Its `origin` holds the\n * start point under {@link GESTURE_KEY}, after `onStart` shaped it. */\n gestureCtx: GestureContext<unknown>;\n readView: () => View | null;\n readPlane: () => PlaneMap | null;\n /** The endpoints after Shift-constrain, the `snap` dep and the behaviors,\n * recomputed on every pump event so each behavior runs once per frame. */\n shaped: { start: InsertPoint; current: InsertPoint };\n /** Captured at `start` for committing a behavior's `Op[]`. */\n scene: Scene<unknown, string, unknown> | undefined;\n applyOps: ((ops: Op[], label: string) => void) | undefined;\n selection: SelectionApi | undefined;\n /** Accumulated user-driven rotation (radians) layered on top of any\n * drag-direction / params rotation. Driven by `insert.adjustRotation`\n * (Shift+wheel during engaged phase). Only consulted for kinds that\n * carry a `rotation` field on their extras (star / polygon today). */\n userRotation: number;\n}\n\n// Module-scoped pointer to the in-flight insert scratch. There is at most\n// one insert gesture at a time (single primary pointer) so a single ref\n// is sufficient. `insert.adjustRotation` reads this to mutate the live\n// scratch without needing handle-passing plumbing through the dispatcher.\nlet liveInsertScratch: InsertScratch | null = null;\n\n/** Resolve the effective origin mode from the binding's nominal opt\n * and live Alt state. Alt inverts: `corner` ⇄ `center`. Applies to\n * every shape kind — radial shapes (polygon/star) included; the\n * overlay paints an `anchorPoint` dot at the click so corner mode\n * reads as anchored even though no polygon vertex sits there.\n */\nfunction effectiveOriginMode(\n paramsOrigin: unknown,\n altHeld: boolean,\n): 'corner' | 'center' {\n const nominal: 'corner' | 'center' = paramsOrigin === 'center' ? 'center' : 'corner';\n if (!altHeld) return nominal;\n return nominal === 'center' ? 'corner' : 'center';\n}\n\n/** Compute the insert AABB from drag endpoints.\n * - `'corner'` (default): bounds = drag rect (top-left → bottom-right of cursor sweep).\n * - `'center'`: bounds = symmetric AABB anchored on the start point.\n * Used when the tool's Alt-modifier binding passes\n * `originMode: 'center'` (Illustrator/Figma convention). */\nfunction computeBounds(\n startX: number, startY: number,\n currentX: number, currentY: number,\n originMode: 'corner' | 'center',\n): { x: number; y: number; width: number; height: number } {\n if (originMode === 'center') {\n const dx = Math.abs(currentX - startX);\n const dy = Math.abs(currentY - startY);\n return { x: startX - dx, y: startY - dy, width: dx * 2, height: dy * 2 };\n }\n return {\n x: Math.min(startX, currentX),\n y: Math.min(startY, currentY),\n width: Math.abs(currentX - startX),\n height: Math.abs(currentY - startY),\n };\n}\n\n/** Constrain `end` to the nearest 15° increment around `start`, preserving\n * the drag length. The line tool's documented Shift behavior. */\nfunction snapTo15Degrees(\n start: { x: number; y: number },\n end: { x: number; y: number },\n): { x: number; y: number } {\n const dx = end.x - start.x;\n const dy = end.y - start.y;\n const len = Math.hypot(dx, dy);\n if (len === 0) return end;\n const step = Math.PI / 12; // 15°\n const snapped = Math.round(Math.atan2(dy, dx) / step) * step;\n return { x: start.x + len * Math.cos(snapped), y: start.y + len * Math.sin(snapped) };\n}\n\n/** The key an insert's single gesture point rides under in the behaviors'\n * `draggedIds` / `origin` / `current` — insert has no node id yet. */\nconst GESTURE_KEY = 'gesture';\n\n/**\n * Shape the raw drag endpoints, in order: the line tool's Shift-constrain\n * (on raw coords, so the locked angle survives), the `snap` dep, then each\n * behavior's `onMove`, each seeing the points the one before it returned.\n * Stored on `scratch.shaped`; `resolveEndpoints` reads it.\n */\nfunction shapeEndpoints(scratch: InsertScratch, modifiers: GestureContext<unknown>['modifiers']): void {\n const resolved = resolveParams(scratch.opts?.params);\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n const gctx = scratch.gestureCtx;\n let start = gctx.origin.get(GESTURE_KEY) as InsertPoint;\n let current = { x: scratch.currentX, y: scratch.currentY };\n if (kind === 'line' && scratch.shiftHeld) {\n current = snapTo15Degrees({ x: scratch.startX, y: scratch.startY }, current);\n }\n current = scratch.snap(current);\n\n if (scratch.behaviors.length > 0) {\n gctx.modifiers = { ...modifiers };\n gctx.pointer = { worldX: scratch.currentX, worldY: scratch.currentY, clientX: 0, clientY: 0 };\n gctx.view = scratch.readView();\n gctx.plane = scratch.readPlane();\n gctx.current.set(GESTURE_KEY, current);\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n for (const b of scratch.behaviors) {\n const bounds = computeBounds(start.x, start.y, current.x, current.y, mode);\n const r = b.onMove?.(gctx, { start, current, bounds, pose: bounds });\n if (!r) continue;\n if (r.start) start = r.start;\n if (r.current) current = r.current;\n }\n gctx.current.set(GESTURE_KEY, current);\n }\n scratch.shaped = { start, current };\n}\n\n/**\n * The effective endpoints: `scratch.shaped`, plus — for the `line` kind\n * only — the alt/center reading as \"mirror the start around the pointer\"\n * rather than \"grow a symmetric AABB\".\n *\n * Every geometry consumer (bounds, extras, preview) goes through this, so\n * the live overlay and the committed node can't disagree.\n */\nfunction resolveEndpoints(\n scratch: InsertScratch,\n kind: string,\n mode: 'corner' | 'center',\n): { startX: number; startY: number; currentX: number; currentY: number } {\n let { start } = scratch.shaped;\n const { current } = scratch.shaped;\n\n // A line has no area, so \"from center\" means the drag is a half-line:\n // mirror the start to the far side of the pointer.\n if (kind === 'line' && mode === 'center') {\n start = { x: start.x - (current.x - start.x), y: start.y - (current.y - start.y) };\n }\n\n return { startX: start.x, startY: start.y, currentX: current.x, currentY: current.y };\n}\n\n/** Build a typed `InsertExtras` from the static params + gesture context.\n * Kit-built-in kinds (line / polygon / star / pencil) read kind-specific\n * fields; unknown kinds pass the raw params through as `{ kind, ... }`. */\nfunction buildExtras(\n params: Record<string, unknown> | undefined,\n startX: number,\n startY: number,\n currentX: number,\n currentY: number,\n points: ReadonlyArray<DragSample> | null,\n): InsertExtras {\n const kind = ((params?.['kind'] as string | undefined) ?? 'rect');\n switch (kind) {\n case 'rect':\n case 'ellipse':\n return { kind } as InsertExtras;\n case 'line':\n // Use the live drag endpoints (not AABB) so the line preserves the\n // user's drag direction — drag from bottom-left to top-right makes a\n // line that slopes up, not the bounds diagonal.\n return { kind: 'line', a: { x: startX, y: startY }, b: { x: currentX, y: currentY } };\n case 'polygon': {\n const sides = Number(params?.['sides'] ?? 6);\n // When the tool omits rotation, point the first vertex along the\n // drag vector so dragging in a direction orients the shape that\n // way. Sub-pixel drags (no direction yet) fall back to 0.\n const dragAngle = (currentX === startX && currentY === startY)\n ? 0\n : Math.atan2(currentY - startY, currentX - startX);\n const rotation = params?.['rotation'] !== undefined\n ? Number(params['rotation'])\n : dragAngle;\n const extras: InsertExtras = { kind: 'polygon', sides, rotation };\n // Drag-from-center: tool's Alt binding passes `originMode:'center'`.\n // Compute polygon center + radius from the drag vector so the shape\n // anchors on the click point. Without this, the dep falls back to\n // inscribing in the AABB (drag-from-corner default).\n if (params?.['originMode'] === 'center') {\n const dx = currentX - startX;\n const dy = currentY - startY;\n (extras as { center?: unknown }).center = { x: startX, y: startY };\n (extras as { radius?: unknown }).radius = Math.hypot(dx, dy);\n } else {\n if (params?.['center'] !== undefined) (extras as { center?: unknown }).center = params['center'];\n if (params?.['radius'] !== undefined) (extras as { radius?: unknown }).radius = params['radius'];\n }\n return extras;\n }\n case 'star': {\n const pts = Number(params?.['points'] ?? 5);\n const ir = Number(params?.['innerRadiusRatio'] ?? 0.5);\n const dragAngle = (currentX === startX && currentY === startY)\n ? 0\n : Math.atan2(currentY - startY, currentX - startX);\n const rotation = params?.['rotation'] !== undefined\n ? Number(params['rotation'])\n : dragAngle;\n const extras: InsertExtras = { kind: 'star', points: pts, innerRadiusRatio: ir, rotation };\n if (params?.['originMode'] === 'center') {\n const dx = currentX - startX;\n const dy = currentY - startY;\n (extras as { center?: unknown }).center = { x: startX, y: startY };\n (extras as { outerRadius?: unknown }).outerRadius = Math.hypot(dx, dy);\n } else {\n if (params?.['center'] !== undefined) (extras as { center?: unknown }).center = params['center'];\n if (params?.['outerRadius'] !== undefined) (extras as { outerRadius?: unknown }).outerRadius = params['outerRadius'];\n }\n return extras;\n }\n case 'pencil':\n // Prefer tool-supplied samples (param) when present — pencil tools may\n // capture richer per-sample data (pressure/tilt). Fall back to the\n // dispatcher's accumulated pointer trail.\n return {\n kind: 'pencil',\n samples: (params?.['samples'] as ReadonlyArray<DragSample> | undefined)\n ?? points\n ?? [],\n };\n default:\n // Consumer-defined kind: pass everything through so a custom insert\n // dep can read its own fields.\n return { ...(params ?? {}), kind } as InsertExtras;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `insert` Action.\n *\n * Requires dep-schema entry: `insert`.\n * Node `kind` is read from `opts.params.kind`; defaults to `'rect'` when absent.\n *\n * @see useInsert — the React hook this descriptor mirrors for the simple case.\n */\nexport const insertAction: Action & { requires: string[] } = inPlane({\n id: 'insert',\n label: 'Insert',\n group: 'insert',\n eligible: { capability: 'creates-shapes' },\n requires: ['insert', 'selection', 'snap', 'textEdit', 'scene', 'applyOps', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle {\n const dep = ctx.deps.insert as InsertDep | undefined;\n if (!dep) return {};\n\n // Starting a draw retires any prior selection — the lingering halo\n // around an unrelated node while the user is drawing a new shape is\n // just visual noise, and it primes the UX bug where a drag that\n // continues over the selected node would otherwise drive the move\n // action via fall-through. The clear is unconditional: the user has\n // committed to \"I'm making a new thing\" by engaging a creation tool.\n const selection = ctx.deps.selection as SelectionApi | undefined;\n if (selection && selection.get().length > 0) {\n selection.clear();\n }\n\n const snapDep = ctx.deps.snap as SnapDep | undefined;\n const snap = snapDep\n ? (p: { x: number; y: number }) => snapDep.point(p)\n : (p: { x: number; y: number }) => p;\n const behaviors = (opts?.behaviors ?? []) as InsertBehavior<unknown>[];\n const readView = gestureViewReader(ctx.deps);\n const readPlane = gesturePlaneReader(ctx.deps);\n const origin = snap({ x: ctx.world.x, y: ctx.world.y });\n const gestureCtx: GestureContext<unknown> = {\n draggedIds: [GESTURE_KEY],\n origin: new Map<string, unknown>([[GESTURE_KEY, origin]]),\n current: new Map<string, unknown>([[GESTURE_KEY, origin]]),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: ctx.world.x, worldY: ctx.world.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n // No node exists to adapt until commit; insert behaviors read points.\n adapter: undefined as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n };\n for (const b of behaviors) b.onStart?.(gestureCtx);\n const startPoint = gestureCtx.origin.get(GESTURE_KEY) as InsertPoint;\n\n const scratch: InsertScratch = {\n dep,\n textEdit: ctx.deps.textEdit as TextEditDep | undefined,\n snap,\n opts,\n behaviors,\n gestureCtx,\n readView,\n readPlane,\n shaped: { start: startPoint, current: startPoint },\n scene: ctx.deps.scene as Scene<unknown, string, unknown> | undefined,\n applyOps: ctx.deps.applyOps as ((ops: Op[], label: string) => void) | undefined,\n selection,\n startX: ctx.world.x,\n startY: ctx.world.y,\n currentX: ctx.world.x,\n currentY: ctx.world.y,\n points: ctx.drag?.points ?? null,\n altHeld: ctx.modifiers.alt,\n shiftHeld: ctx.modifiers.shift,\n open: true,\n userRotation: 0,\n };\n liveInsertScratch = scratch;\n\n return {\n kind: 'insert',\n onMove(moveCtx: InvocationCtx): void {\n scratch.currentX = moveCtx.world.x;\n scratch.currentY = moveCtx.world.y;\n scratch.shiftHeld = moveCtx.modifiers.shift;\n // Track live Alt state for the corner ⇄ center toggle. Each\n // pointermove carries fresh modifier state from the dispatcher;\n // releasing / pressing Alt mid-drag flips the bounds mode on\n // the next move tick (true modifier-only events don't fire\n // without a cursor change).\n scratch.altHeld = moveCtx.modifiers.alt;\n // The dispatcher mutates its own per-gesture trail in place but\n // attaches the array reference to each InvocationCtx.drag — keep\n // the latest reference in case the dispatcher swaps it.\n if (moveCtx.drag?.points) scratch.points = moveCtx.drag.points;\n shapeEndpoints(scratch, moveCtx.modifiers);\n },\n overlay(): OngoingOverlay | null {\n if (!scratch.open) return null;\n // Resolve params on every overlay() so thunked params (polygon\n // sides ticking via ArrowUp mid-drag) reflect in the live preview.\n const resolved = resolveParams(scratch.opts?.params);\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n // Consumer-defined kinds aren't renderable by the kit overlay —\n // skip the preview rather than emit something half-faithful.\n if (!shapeKindInfo(kind)?.insertPreview) return null;\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n const pts = resolveEndpoints(scratch, kind, mode);\n const bounds = computeBounds(\n pts.startX, pts.startY, pts.currentX, pts.currentY, mode,\n );\n // Derive extras with the *effective* mode so polygon/star\n // center+radius reflect the live Alt toggle, not the binding's\n // nominal originMode.\n const effectiveExtras = buildExtras(\n { ...(resolved ?? {}), originMode: mode },\n pts.startX, pts.startY, pts.currentX, pts.currentY,\n scratch.points,\n );\n applyUserRotation(effectiveExtras, scratch.userRotation);\n return {\n kind: 'insertPreview',\n shape: effectiveExtras.kind as KitInsertShape,\n bounds,\n extras: effectiveExtras,\n // Anchor dot at the click point — chrome that sells \"this\n // is where the drag started.\" Useful for radial shapes\n // (no vertex at click) and for center mode (dot marks the\n // growth axis).\n anchorPoint: { x: pts.startX, y: pts.startY },\n };\n },\n onEnd(endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n scratch.open = false;\n if (liveInsertScratch === scratch) liveInsertScratch = null;\n if (reason === 'cancel') {\n runBehaviorCancel(scratch.behaviors, scratch.gestureCtx);\n return;\n }\n\n const { dep: d, opts: o } = scratch;\n const points = endCtx.drag?.points ?? scratch.points;\n\n // Resolve params at commit time so thunked params (polygon\n // `sides` adjusted mid-drag, etc.) see the latest tool state.\n const resolved = resolveParams(o?.params);\n\n if (scratch.behaviors.length > 0) {\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx);\n if (r === null) return;\n if (r !== undefined) {\n if (scratch.scene) {\n commitGestureOps(\n {\n scene: scratch.scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scratch.scene, scratch.selection?.adapterMethods),\n },\n readGestureLifecycle(resolved, 'Insert', scratch.behaviors),\n r,\n );\n }\n return;\n }\n }\n\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n const { startX, startY, currentX, currentY } = resolveEndpoints(scratch, kind, mode);\n const bounds = computeBounds(startX, startY, currentX, currentY, mode);\n const extras = buildExtras(\n { ...(resolved ?? {}), originMode: mode },\n startX, startY, currentX, currentY, points,\n );\n applyUserRotation(extras, scratch.userRotation);\n\n // Sub-threshold drag — no insert. The test is kind-aware because\n // \"zero size\" isn't one shape:\n // - pencil: a sample trail can be meaningful even when start ≈ end\n // (a closed loop), so no guard at all.\n // - line: has no area by construction. An axis-aligned line has a\n // zero-height (or zero-width) AABB and is perfectly valid — only\n // a zero-LENGTH drag is degenerate. (Shift-constrain makes\n // axis-aligned the common case, which is how this surfaced.)\n // - everything else: a zero extent in either axis is degenerate.\n if (!isCommittableExtent(bounds, extras.kind)) return;\n\n const id = d.commit(bounds, extras);\n\n // A freshly dragged text box is empty, so it draws nothing and the\n // tool's own `click on selected body → enterTextEdit` binding has\n // nothing visible to click. Drop straight into the caret instead —\n // which is also what every other editor does with a text tool.\n // `textEdit` is optional: without it the box is still committed,\n // exactly as before.\n //\n // Read from the scratch, not `endCtx.deps`: the dispatcher builds\n // the deps bag once, at `start`, and passes `deps: {}` on every\n // later pump event. Same reason the insert dep itself is stashed.\n if (id !== null && extras.kind === 'text') {\n scratch.textEdit?.startEdit(String(id), { caret: 0 });\n }\n },\n };\n },\n },\n /**\n * Insert is always available — no selection required. Returns `true` so the\n * dispatcher allows the action through on every drag-on-empty gesture.\n *\n * Fixed from the stale `ActionDisabledReason.SelectionRequired`\n * placeholder that was silently blocking all dispatcher-routed inserts.\n */\n enabled: () => true as const,\n}, insertLayer);\n\n/** Whether a drag produced enough extent to be worth committing, per kind.\n * See the call site in `onEnd` for why this isn't one uniform test. */\nfunction isCommittableExtent(\n bounds: { width: number; height: number },\n kind: string,\n): boolean {\n if (kind === 'pencil') return true;\n if (kind === 'line') return bounds.width !== 0 || bounds.height !== 0;\n return bounds.width !== 0 && bounds.height !== 0;\n}\n\n/** Add a user-driven rotation offset onto kinds that carry rotation. Rect\n * / ellipse / line / pencil don't expose a rotation field on their\n * extras today, so they ignore the offset. */\nfunction applyUserRotation(extras: InsertExtras, userRotation: number): void {\n if (userRotation === 0) return;\n if (extras.kind === 'star' || extras.kind === 'polygon') {\n (extras as { rotation: number }).rotation += userRotation;\n }\n}\n\n/** Radians per unit of wheel deltaY. Tuned so a typical mouse-wheel click\n * (deltaY ≈ 100) yields ~25°, while a trackpad event (deltaY ≈ 1–10)\n * yields a smooth 0.25–2.5° increment. */\nconst ROTATION_SENSITIVITY = Math.PI / 720;\n\n/**\n * @experimental\n * Companion to `insertAction` — adjusts the live insert gesture's\n * `userRotation` based on wheel deltaY. Intended to be bound to\n * Shift+wheel during the insert tool's engaged phase. No-op when no\n * insert is in flight.\n */\nexport const insertRotateAction: Action = {\n id: 'insert.adjustRotation',\n label: 'Insert — rotate',\n group: 'insert',\n eligible: { capability: 'creates-shapes' },\n invoker: {\n timing: 'immediate' as const,\n run: (_deps, params) => {\n const scratch = liveInsertScratch;\n if (!scratch || !scratch.open) return;\n const deltaY = (params as { deltaY?: number } | undefined)?.deltaY ?? 0;\n if (deltaY === 0) return;\n scratch.userRotation += deltaY * ROTATION_SENSITIVITY;\n },\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"insert.js","names":["liveInsertScratch","effectiveOriginMode","nominal","paramsOrigin","altHeld","computeBounds","originMode","dx","currentX","startX","dy","currentY","startY","snapTo15Degrees","end","start","len","step","snapped","GESTURE_KEY","shapeEndpoints","resolved","resolveParams","scratch","kind","\"kind\"","gctx","current","modifiers","mode","\"originMode\"","b","bounds","r","resolveEndpoints","buildExtras","params","sides","\"sides\"","dragAngle","extras","\"rotation\"","\"center\"","\"radius\"","pts","\"points\"","ir","\"innerRadiusRatio\"","\"outerRadius\"","\"samples\"","points","insertAction","inPlane","dep","ctx","selection","snapDep","snap","p","behaviors","opts","readView","gestureViewReader","readPlane","gesturePlaneReader","origin","gestureCtx","startPoint","moveCtx","shapeKindInfo","effectiveExtras","applyUserRotation","reason","d","o","endCtx","reduceBehaviorEnd","commitGestureOps","defaultCommitAdapter","readGestureLifecycle","isCommittableExtent","id","insertLayer","userRotation","ROTATION_SENSITIVITY","insertRotateAction","deltaY"],"sources":["../../../../src/interactions/actions/defaults/insert.ts"],"sourcesContent":["/**\n * `insertAction` — ongoing Action descriptor for drag-to-insert.\n *\n * ## Status: REAL\n *\n * Implements the drag-rect insert logic from `useInsert`:\n * - `start`: validates the `insert` dep and records the drag start point.\n * - `onMove`: tracks the live drag bounds (no scene writes).\n * - `onEnd('commit')`: derives final bounds from drag.start + drag.current;\n * calls `deps.insert.commit(bounds, kind)` to materialise the new node.\n * `kind` comes from `opts.params.kind` (set by the active tool's binding).\n * - `onEnd('cancel')`: no-op.\n *\n * ## Dependencies\n *\n * Requires `insert` dep from DepSchema:\n * `{ commit(bounds, kind): NodeId | null }`\n *\n * `<SceneCanvas>` / `<StandardActionsRegistrar>` should source this dep by\n * delegating to the scene's `add()` with a sensible default data payload for\n * the given `kind`. Override per-consumer for custom node factories.\n *\n * ## Modifiers and snapping\n *\n * Grid snapping comes from the optional `snap` dep, applied to the drag's\n * start and current point (never to freehand pencil samples). The `line`\n * kind additionally honors Shift (constrain to 15°) and reads Alt/center as\n * \"mirror the start around the pointer\" rather than \"grow a symmetric AABB\".\n * Behaviors from the binding's `opts.behaviors` (`InsertBehavior[]` — e.g.\n * `@weasel-js/guides`' `snapToGrid` and `alignInsertBehavior`) run over the two\n * points after those, the way `moveAction` runs its own: `onStart` on the\n * start point, `onMove` on each frame's proposal, `onEnd` on release, where\n * the first answer wins (`null` aborts, `Op[]` commits in place of the\n * insert). `shapeEndpoints` folds them each pump event and `resolveEndpoints`\n * reads the result, so the live preview and the committed node can't\n * disagree.\n *\n * ## What this does NOT wire (vs `useInsert`)\n *\n * - `pointInsert` fallback for click / sub-threshold drags — not wired; a\n * sub-threshold drag produces no insert.\n * - `clickOnly` mode — not applicable to the descriptor model.\n *\n * ## Live preview\n *\n * Insert produces a brand-new node that has no scene id until commit, so\n * the preview-ghost layer (`usePreviewGhostLayer`, keyed by node id) can't\n * paint a silhouette for it. Instead, the action exposes its in-flight\n * geometry via the `overlay()` surface with `kind: 'insertPreview'`. The\n * canvas's `useDispatcherOverlayLayer` rebuilds the shape using the same\n * path builders the commit-time factory uses (`rectPath`, `ellipsePath`,\n * `linePath`, `regularPolygonPath`, `starPath`, polyline / `polygonFromPoints`),\n * so the preview is geometry-faithful to the eventual node.\n *\n * Consumer-defined insert kinds (kinds the kit doesn't know how to draw)\n * skip the preview but still commit normally on release.\n *\n * ## Relationship to `useInsert`\n *\n * `useInsert` calls `adapter.commitInsert(bounds)` which returns a node and\n * dispatches a `createInsertOp`. This descriptor delegates to `deps.insert.commit`\n * which encapsulates both factory + op dispatch in one call. This keeps the\n * dep contract thin and avoids importing `createInsertOp` into the descriptor.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle, BindingOpts, OngoingOverlay, DragSample } from '@weasel-js/routing';\nimport { resolveParams } from '@weasel-js/routing';\nimport type { InsertDep, InsertExtras } from '../depSchema';\nimport type { Op } from 'core/ops/types';\nimport type { Scene } from 'core/scene/types';\nimport type { GestureContext, InsertBehavior, InsertPoint } from '../../gestures/types';\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { View } from 'core/viewport/view';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel } from '../gestureLifecycle';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport type { TextEditDep } from '../depSchema';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { shapeKindInfo } from 'core/shapeKinds';\nimport type { KitInsertShape } from 'core/shapeKinds';\nimport { inPlane, insertLayer } from '../planeInput';\nimport type { PlaneMap } from 'core/viewport/parallax';\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface InsertScratch {\n dep: InsertDep;\n /** Text-edit dep captured at gesture start, for the text kind's\n * drop-into-the-caret step. Stashed rather than read at commit because\n * the dispatcher only builds the deps bag once, on `start` — every later\n * pump event carries `deps: {}`. */\n textEdit: TextEditDep | undefined;\n /** World-space point snapper from the `snap` dep, or identity when the\n * dep isn't registered. Applied to the drag's start and current point so\n * the live preview and the committed geometry agree. Freehand pencil\n * samples are deliberately NOT snapped — a grid-quantized freehand trail\n * is a staircase, not a stroke. */\n snap: (p: { x: number; y: number }) => { x: number; y: number };\n /** The active binding's opts — re-resolved at commit time so thunked\n * params see the latest tool state (e.g. polygon `sides` after ArrowUp). */\n opts: BindingOpts | undefined;\n /** RAW (unsnapped) drag endpoints in world space. `shapeEndpoints`\n * constrains the angle first, THEN aligns the endpoint to the grid, so\n * the documented modifier ordering holds for the preview and the commit. */\n startX: number;\n startY: number;\n currentX: number;\n currentY: number;\n /** Pointer trail accumulated by the dispatcher in world space. Same array\n * reference as `ctx.drag.points`; pencil-kind commits read from this. */\n points: ReadonlyArray<DragSample> | null;\n /** Live Alt-key state from the most recent pump event. Alt INVERTS the\n * binding's nominal `originMode` (corner → center and vice versa), so\n * the user can press / release Alt mid-drag and the preview/commit\n * flip without needing the dispatcher to re-route to a different\n * binding. Captured per-onMove from `ctx.modifiers.alt`. */\n altHeld: boolean;\n /** Live Shift-key state from the most recent pump event. Constrains the\n * `line` kind to 15° increments (the modifier the line tool documents).\n * Captured per-onMove from `ctx.modifiers.shift`. */\n shiftHeld: boolean;\n /** Cleared once `onEnd` runs so subsequent `overlay()` calls report no\n * in-flight preview (mirrors the areaSelect/lasso convention). */\n open: boolean;\n /** Behaviors from `opts.behaviors`; empty when none were bound. */\n behaviors: InsertBehavior<unknown>[];\n /** Reused gesture context handed to behaviors. Its `origin` holds the\n * start point under {@link GESTURE_KEY}, after `onStart` shaped it. */\n gestureCtx: GestureContext<unknown>;\n readView: () => View | null;\n readPlane: () => PlaneMap | null;\n /** The endpoints after Shift-constrain, the `snap` dep and the behaviors,\n * recomputed on every pump event so each behavior runs once per frame. */\n shaped: { start: InsertPoint; current: InsertPoint };\n /** Captured at `start` for committing a behavior's `Op[]`. */\n scene: Scene<unknown, string, unknown> | undefined;\n applyOps: ((ops: Op[], label: string) => void) | undefined;\n selection: SelectionApi | undefined;\n /** Accumulated user-driven rotation (radians) layered on top of any\n * drag-direction / params rotation. Driven by `insert.adjustRotation`\n * (Shift+wheel during engaged phase). Only consulted for kinds that\n * carry a `rotation` field on their extras (star / polygon today). */\n userRotation: number;\n}\n\n// Module-scoped pointer to the in-flight insert scratch. There is at most\n// one insert gesture at a time (single primary pointer) so a single ref\n// is sufficient. `insert.adjustRotation` reads this to mutate the live\n// scratch without needing handle-passing plumbing through the dispatcher.\nlet liveInsertScratch: InsertScratch | null = null;\n\n/** Resolve the effective origin mode from the binding's nominal opt\n * and live Alt state. Alt inverts: `corner` ⇄ `center`. Applies to\n * every shape kind — radial shapes (polygon/star) included; the\n * overlay paints an `anchorPoint` dot at the click so corner mode\n * reads as anchored even though no polygon vertex sits there.\n */\nfunction effectiveOriginMode(\n paramsOrigin: unknown,\n altHeld: boolean,\n): 'corner' | 'center' {\n const nominal: 'corner' | 'center' = paramsOrigin === 'center' ? 'center' : 'corner';\n if (!altHeld) return nominal;\n return nominal === 'center' ? 'corner' : 'center';\n}\n\n/** Compute the insert AABB from drag endpoints.\n * - `'corner'` (default): bounds = drag rect (top-left → bottom-right of cursor sweep).\n * - `'center'`: bounds = symmetric AABB anchored on the start point.\n * Used when the tool's Alt-modifier binding passes\n * `originMode: 'center'` (Illustrator/Figma convention). */\nfunction computeBounds(\n startX: number, startY: number,\n currentX: number, currentY: number,\n originMode: 'corner' | 'center',\n): { x: number; y: number; width: number; height: number } {\n if (originMode === 'center') {\n const dx = Math.abs(currentX - startX);\n const dy = Math.abs(currentY - startY);\n return { x: startX - dx, y: startY - dy, width: dx * 2, height: dy * 2 };\n }\n return {\n x: Math.min(startX, currentX),\n y: Math.min(startY, currentY),\n width: Math.abs(currentX - startX),\n height: Math.abs(currentY - startY),\n };\n}\n\n/** Constrain `end` to the nearest 15° increment around `start`, preserving\n * the drag length. The line tool's documented Shift behavior. */\nfunction snapTo15Degrees(\n start: { x: number; y: number },\n end: { x: number; y: number },\n): { x: number; y: number } {\n const dx = end.x - start.x;\n const dy = end.y - start.y;\n const len = Math.hypot(dx, dy);\n if (len === 0) return end;\n const step = Math.PI / 12; // 15°\n const snapped = Math.round(Math.atan2(dy, dx) / step) * step;\n return { x: start.x + len * Math.cos(snapped), y: start.y + len * Math.sin(snapped) };\n}\n\n/** The key an insert's single gesture point rides under in the behaviors'\n * `draggedIds` / `origin` / `current` — insert has no node id yet. */\nconst GESTURE_KEY = 'gesture';\n\n/**\n * Shape the raw drag endpoints, in order: the line tool's Shift-constrain\n * (on raw coords, so the locked angle survives), the `snap` dep, then each\n * behavior's `onMove`, each seeing the points the one before it returned.\n * Stored on `scratch.shaped`; `resolveEndpoints` reads it.\n */\nfunction shapeEndpoints(scratch: InsertScratch, modifiers: GestureContext<unknown>['modifiers']): void {\n const resolved = resolveParams(scratch.opts?.params);\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n const gctx = scratch.gestureCtx;\n let start = gctx.origin.get(GESTURE_KEY) as InsertPoint;\n let current = { x: scratch.currentX, y: scratch.currentY };\n if (kind === 'line' && scratch.shiftHeld) {\n current = snapTo15Degrees({ x: scratch.startX, y: scratch.startY }, current);\n }\n current = scratch.snap(current);\n\n if (scratch.behaviors.length > 0) {\n gctx.modifiers = { ...modifiers };\n gctx.pointer = { worldX: scratch.currentX, worldY: scratch.currentY, clientX: 0, clientY: 0 };\n gctx.view = scratch.readView();\n gctx.plane = scratch.readPlane();\n gctx.current.set(GESTURE_KEY, current);\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n for (const b of scratch.behaviors) {\n const bounds = computeBounds(start.x, start.y, current.x, current.y, mode);\n const r = b.onMove?.(gctx, { start, current, bounds, pose: bounds });\n if (!r) continue;\n if (r.start) start = r.start;\n if (r.current) current = r.current;\n }\n gctx.current.set(GESTURE_KEY, current);\n }\n scratch.shaped = { start, current };\n}\n\n/**\n * The effective endpoints: `scratch.shaped`, plus — for the `line` kind\n * only — the alt/center reading as \"mirror the start around the pointer\"\n * rather than \"grow a symmetric AABB\".\n *\n * Every geometry consumer (bounds, extras, preview) goes through this, so\n * the live overlay and the committed node can't disagree.\n */\nfunction resolveEndpoints(\n scratch: InsertScratch,\n kind: string,\n mode: 'corner' | 'center',\n): { startX: number; startY: number; currentX: number; currentY: number } {\n let { start } = scratch.shaped;\n const { current } = scratch.shaped;\n\n // A line has no area, so \"from center\" means the drag is a half-line:\n // mirror the start to the far side of the pointer.\n if (kind === 'line' && mode === 'center') {\n start = { x: start.x - (current.x - start.x), y: start.y - (current.y - start.y) };\n }\n\n return { startX: start.x, startY: start.y, currentX: current.x, currentY: current.y };\n}\n\n/** Build a typed `InsertExtras` from the static params + gesture context.\n * Kit-built-in kinds (line / polygon / star / pencil) read kind-specific\n * fields; unknown kinds pass the raw params through as `{ kind, ... }`. */\nfunction buildExtras(\n params: Record<string, unknown> | undefined,\n startX: number,\n startY: number,\n currentX: number,\n currentY: number,\n points: ReadonlyArray<DragSample> | null,\n): InsertExtras {\n const kind = ((params?.['kind'] as string | undefined) ?? 'rect');\n switch (kind) {\n case 'rect':\n case 'ellipse':\n return { kind } as InsertExtras;\n case 'line':\n // Use the live drag endpoints (not AABB) so the line preserves the\n // user's drag direction — drag from bottom-left to top-right makes a\n // line that slopes up, not the bounds diagonal.\n return { kind: 'line', a: { x: startX, y: startY }, b: { x: currentX, y: currentY } };\n case 'polygon': {\n const sides = Number(params?.['sides'] ?? 6);\n // When the tool omits rotation, point the first vertex along the\n // drag vector so dragging in a direction orients the shape that\n // way. Sub-pixel drags (no direction yet) fall back to 0.\n const dragAngle = (currentX === startX && currentY === startY)\n ? 0\n : Math.atan2(currentY - startY, currentX - startX);\n const rotation = params?.['rotation'] !== undefined\n ? Number(params['rotation'])\n : dragAngle;\n const extras: InsertExtras = { kind: 'polygon', sides, rotation };\n // Drag-from-center: tool's Alt binding passes `originMode:'center'`.\n // Compute polygon center + radius from the drag vector so the shape\n // anchors on the click point. Without this, the dep falls back to\n // inscribing in the AABB (drag-from-corner default).\n if (params?.['originMode'] === 'center') {\n const dx = currentX - startX;\n const dy = currentY - startY;\n (extras as { center?: unknown }).center = { x: startX, y: startY };\n (extras as { radius?: unknown }).radius = Math.hypot(dx, dy);\n } else {\n if (params?.['center'] !== undefined) (extras as { center?: unknown }).center = params['center'];\n if (params?.['radius'] !== undefined) (extras as { radius?: unknown }).radius = params['radius'];\n }\n return extras;\n }\n case 'star': {\n const pts = Number(params?.['points'] ?? 5);\n const ir = Number(params?.['innerRadiusRatio'] ?? 0.5);\n const dragAngle = (currentX === startX && currentY === startY)\n ? 0\n : Math.atan2(currentY - startY, currentX - startX);\n const rotation = params?.['rotation'] !== undefined\n ? Number(params['rotation'])\n : dragAngle;\n const extras: InsertExtras = { kind: 'star', points: pts, innerRadiusRatio: ir, rotation };\n if (params?.['originMode'] === 'center') {\n const dx = currentX - startX;\n const dy = currentY - startY;\n (extras as { center?: unknown }).center = { x: startX, y: startY };\n (extras as { outerRadius?: unknown }).outerRadius = Math.hypot(dx, dy);\n } else {\n if (params?.['center'] !== undefined) (extras as { center?: unknown }).center = params['center'];\n if (params?.['outerRadius'] !== undefined) (extras as { outerRadius?: unknown }).outerRadius = params['outerRadius'];\n }\n return extras;\n }\n case 'pencil':\n // Prefer tool-supplied samples (param) when present — pencil tools may\n // capture richer per-sample data (pressure/tilt). Fall back to the\n // dispatcher's accumulated pointer trail.\n return {\n kind: 'pencil',\n samples: (params?.['samples'] as ReadonlyArray<DragSample> | undefined)\n ?? points\n ?? [],\n };\n default:\n // Consumer-defined kind: pass everything through so a custom insert\n // dep can read its own fields.\n return { ...(params ?? {}), kind } as InsertExtras;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `insert` Action.\n *\n * Requires dep-schema entry: `insert`.\n * Node `kind` is read from `opts.params.kind`; defaults to `'rect'` when absent.\n *\n * @see useInsert — the React hook this descriptor mirrors for the simple case.\n */\nexport const insertAction: Action & { requires: string[] } = inPlane({\n id: 'insert',\n label: 'Insert',\n group: 'insert',\n eligible: { capability: 'creates-shapes' },\n requires: ['insert', 'selection', 'snap', 'textEdit', 'scene', 'applyOps', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle {\n const dep = ctx.deps.insert;\n if (!dep) return {};\n\n // Starting a draw retires any prior selection — the lingering halo\n // around an unrelated node while the user is drawing a new shape is\n // just visual noise, and it primes the UX bug where a drag that\n // continues over the selected node would otherwise drive the move\n // action via fall-through. The clear is unconditional: the user has\n // committed to \"I'm making a new thing\" by engaging a creation tool.\n const selection = ctx.deps.selection;\n if (selection && selection.get().length > 0) {\n selection.clear();\n }\n\n const snapDep = ctx.deps.snap;\n const snap = snapDep\n ? (p: { x: number; y: number }) => snapDep.point(p)\n : (p: { x: number; y: number }) => p;\n const behaviors = (opts?.behaviors ?? []) as InsertBehavior<unknown>[];\n const readView = gestureViewReader(ctx.deps);\n const readPlane = gesturePlaneReader(ctx.deps);\n const origin = snap({ x: ctx.world.x, y: ctx.world.y });\n const gestureCtx: GestureContext<unknown> = {\n draggedIds: [GESTURE_KEY],\n origin: new Map<string, unknown>([[GESTURE_KEY, origin]]),\n current: new Map<string, unknown>([[GESTURE_KEY, origin]]),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: ctx.world.x, worldY: ctx.world.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n // No node exists to adapt until commit; insert behaviors read points.\n adapter: undefined as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n };\n for (const b of behaviors) b.onStart?.(gestureCtx);\n const startPoint = gestureCtx.origin.get(GESTURE_KEY) as InsertPoint;\n\n const scratch: InsertScratch = {\n dep,\n textEdit: ctx.deps.textEdit,\n snap,\n opts,\n behaviors,\n gestureCtx,\n readView,\n readPlane,\n shaped: { start: startPoint, current: startPoint },\n scene: ctx.deps.scene,\n applyOps: ctx.deps.applyOps,\n selection,\n startX: ctx.world.x,\n startY: ctx.world.y,\n currentX: ctx.world.x,\n currentY: ctx.world.y,\n points: ctx.drag?.points ?? null,\n altHeld: ctx.modifiers.alt,\n shiftHeld: ctx.modifiers.shift,\n open: true,\n userRotation: 0,\n };\n liveInsertScratch = scratch;\n\n return {\n kind: 'insert',\n onMove(moveCtx: InvocationCtx): void {\n scratch.currentX = moveCtx.world.x;\n scratch.currentY = moveCtx.world.y;\n scratch.shiftHeld = moveCtx.modifiers.shift;\n // Track live Alt state for the corner ⇄ center toggle. Each\n // pointermove carries fresh modifier state from the dispatcher;\n // releasing / pressing Alt mid-drag flips the bounds mode on\n // the next move tick (true modifier-only events don't fire\n // without a cursor change).\n scratch.altHeld = moveCtx.modifiers.alt;\n // The dispatcher mutates its own per-gesture trail in place but\n // attaches the array reference to each InvocationCtx.drag — keep\n // the latest reference in case the dispatcher swaps it.\n if (moveCtx.drag?.points) scratch.points = moveCtx.drag.points;\n shapeEndpoints(scratch, moveCtx.modifiers);\n },\n overlay(): OngoingOverlay | null {\n if (!scratch.open) return null;\n // Resolve params on every overlay() so thunked params (polygon\n // sides ticking via ArrowUp mid-drag) reflect in the live preview.\n const resolved = resolveParams(scratch.opts?.params);\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n // Consumer-defined kinds aren't renderable by the kit overlay —\n // skip the preview rather than emit something half-faithful.\n if (!shapeKindInfo(kind)?.insertPreview) return null;\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n const pts = resolveEndpoints(scratch, kind, mode);\n const bounds = computeBounds(\n pts.startX, pts.startY, pts.currentX, pts.currentY, mode,\n );\n // Derive extras with the *effective* mode so polygon/star\n // center+radius reflect the live Alt toggle, not the binding's\n // nominal originMode.\n const effectiveExtras = buildExtras(\n { ...(resolved ?? {}), originMode: mode },\n pts.startX, pts.startY, pts.currentX, pts.currentY,\n scratch.points,\n );\n applyUserRotation(effectiveExtras, scratch.userRotation);\n return {\n kind: 'insertPreview',\n shape: effectiveExtras.kind as KitInsertShape,\n bounds,\n extras: effectiveExtras,\n // Anchor dot at the click point — chrome that sells \"this\n // is where the drag started.\" Useful for radial shapes\n // (no vertex at click) and for center mode (dot marks the\n // growth axis).\n anchorPoint: { x: pts.startX, y: pts.startY },\n };\n },\n onEnd(endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n scratch.open = false;\n if (liveInsertScratch === scratch) liveInsertScratch = null;\n if (reason === 'cancel') {\n runBehaviorCancel(scratch.behaviors, scratch.gestureCtx);\n return;\n }\n\n const { dep: d, opts: o } = scratch;\n const points = endCtx.drag?.points ?? scratch.points;\n\n // Resolve params at commit time so thunked params (polygon\n // `sides` adjusted mid-drag, etc.) see the latest tool state.\n const resolved = resolveParams(o?.params);\n\n if (scratch.behaviors.length > 0) {\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx);\n if (r === null) return;\n if (r !== undefined) {\n if (scratch.scene) {\n commitGestureOps(\n {\n scene: scratch.scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scratch.scene, scratch.selection?.adapterMethods),\n },\n readGestureLifecycle(resolved, 'Insert', scratch.behaviors),\n r,\n );\n }\n return;\n }\n }\n\n const kind = (resolved?.['kind'] as string | undefined) ?? 'rect';\n const mode = effectiveOriginMode(resolved?.['originMode'], scratch.altHeld);\n const { startX, startY, currentX, currentY } = resolveEndpoints(scratch, kind, mode);\n const bounds = computeBounds(startX, startY, currentX, currentY, mode);\n const extras = buildExtras(\n { ...(resolved ?? {}), originMode: mode },\n startX, startY, currentX, currentY, points,\n );\n applyUserRotation(extras, scratch.userRotation);\n\n // Sub-threshold drag — no insert. The test is kind-aware because\n // \"zero size\" isn't one shape:\n // - pencil: a sample trail can be meaningful even when start ≈ end\n // (a closed loop), so no guard at all.\n // - line: has no area by construction. An axis-aligned line has a\n // zero-height (or zero-width) AABB and is perfectly valid — only\n // a zero-LENGTH drag is degenerate. (Shift-constrain makes\n // axis-aligned the common case, which is how this surfaced.)\n // - everything else: a zero extent in either axis is degenerate.\n if (!isCommittableExtent(bounds, extras.kind)) return;\n\n const id = d.commit(bounds, extras);\n\n // A freshly dragged text box is empty, so it draws nothing and the\n // tool's own `click on selected body → enterTextEdit` binding has\n // nothing visible to click. Drop straight into the caret instead —\n // which is also what every other editor does with a text tool.\n // `textEdit` is optional: without it the box is still committed,\n // exactly as before.\n //\n // Read from the scratch, not `endCtx.deps`: the dispatcher builds\n // the deps bag once, at `start`, and passes `deps: {}` on every\n // later pump event. Same reason the insert dep itself is stashed.\n if (id !== null && extras.kind === 'text') {\n scratch.textEdit?.startEdit(String(id), { caret: 0 });\n }\n },\n };\n },\n },\n /**\n * Insert is always available — no selection required. Returns `true` so the\n * dispatcher allows the action through on every drag-on-empty gesture.\n *\n * Fixed from the stale `ActionDisabledReason.SelectionRequired`\n * placeholder that was silently blocking all dispatcher-routed inserts.\n */\n enabled: () => true as const,\n}, insertLayer);\n\n/** Whether a drag produced enough extent to be worth committing, per kind.\n * See the call site in `onEnd` for why this isn't one uniform test. */\nfunction isCommittableExtent(\n bounds: { width: number; height: number },\n kind: string,\n): boolean {\n if (kind === 'pencil') return true;\n if (kind === 'line') return bounds.width !== 0 || bounds.height !== 0;\n return bounds.width !== 0 && bounds.height !== 0;\n}\n\n/** Add a user-driven rotation offset onto kinds that carry rotation. Rect\n * / ellipse / line / pencil don't expose a rotation field on their\n * extras today, so they ignore the offset. */\nfunction applyUserRotation(extras: InsertExtras, userRotation: number): void {\n if (userRotation === 0) return;\n if (extras.kind === 'star' || extras.kind === 'polygon') {\n (extras as { rotation: number }).rotation += userRotation;\n }\n}\n\n/** Radians per unit of wheel deltaY. Tuned so a typical mouse-wheel click\n * (deltaY ≈ 100) yields ~25°, while a trackpad event (deltaY ≈ 1–10)\n * yields a smooth 0.25–2.5° increment. */\nconst ROTATION_SENSITIVITY = Math.PI / 720;\n\n/**\n * @experimental\n * Companion to `insertAction` — adjusts the live insert gesture's\n * `userRotation` based on wheel deltaY. Intended to be bound to\n * Shift+wheel during the insert tool's engaged phase. No-op when no\n * insert is in flight.\n */\nexport const insertRotateAction: Action = {\n id: 'insert.adjustRotation',\n label: 'Insert — rotate',\n group: 'insert',\n eligible: { capability: 'creates-shapes' },\n invoker: {\n timing: 'immediate' as const,\n run: (_deps, params) => {\n const scratch = liveInsertScratch;\n if (!scratch || !scratch.open) return;\n const deltaY = (params as { deltaY?: number } | undefined)?.deltaY ?? 0;\n if (deltaY === 0) return;\n scratch.userRotation += deltaY * ROTATION_SENSITIVITY;\n },\n },\n};\n"],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ function l(e, t, r) {
|
|
|
12
12
|
if (!i?.editingId) return null;
|
|
13
13
|
let o = i.getEditablePath(i.editingId);
|
|
14
14
|
if (!o || o.kind !== "polygon") return null;
|
|
15
|
-
let c =
|
|
15
|
+
let c = e?.view?.get().scale, l = a(n(o), t, r, {
|
|
16
16
|
tolerancePx: s,
|
|
17
17
|
...c ? { scale: c } : {}
|
|
18
18
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insertPathAnchor.js","names":["SEGMENT_HIT_PX","INSERT_CURSOR","segmentUnder","dep","deps","path","scale","hit","segmentAt","pathToAnchors","x","y","insertPathAnchorAction","inPlane","wx","params","wy","under","inserted","next","editAnchorSet","insertAnchorOnSegment","set","at","ActionDisabledReason","editingLayer"],"sources":["../../../../src/interactions/actions/defaults/insertPathAnchor.ts"],"sourcesContent":["/**\n * `insertPathAnchorAction` — Alt+click on a segment of the path being\n * edited inserts a new anchor there, without changing the path's shape.\n *\n * Alt+click because plain click already belongs to `selectAnchorAction`\n * (select an anchor, or clear the anchor selection on a miss), and\n * Alt+Shift+click to `cutPathAtAnchorAction`.\n *\n * The segment test lives in `enabled`, which the dispatcher hands the\n * event's world point: a click off every segment (or on an anchor) reports\n * not-applicable, so the dispatcher falls through to whatever else the\n * click could mean, and the hover pump shows `cursor` only where a click\n * would insert.\n */\n\nimport type { Action, ActionDeps, ImmediateInvoker } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { CursorSpec } from '@weasel-js/cursor';\nimport type { EditAnchorsDep
|
|
1
|
+
{"version":3,"file":"insertPathAnchor.js","names":["SEGMENT_HIT_PX","INSERT_CURSOR","segmentUnder","dep","deps","path","scale","hit","segmentAt","pathToAnchors","x","y","insertPathAnchorAction","inPlane","wx","params","wy","under","inserted","next","editAnchorSet","insertAnchorOnSegment","set","at","ActionDisabledReason","editingLayer"],"sources":["../../../../src/interactions/actions/defaults/insertPathAnchor.ts"],"sourcesContent":["/**\n * `insertPathAnchorAction` — Alt+click on a segment of the path being\n * edited inserts a new anchor there, without changing the path's shape.\n *\n * Alt+click because plain click already belongs to `selectAnchorAction`\n * (select an anchor, or clear the anchor selection on a miss), and\n * Alt+Shift+click to `cutPathAtAnchorAction`.\n *\n * The segment test lives in `enabled`, which the dispatcher hands the\n * event's world point: a click off every segment (or on an anchor) reports\n * not-applicable, so the dispatcher falls through to whatever else the\n * click could mean, and the hover pump shows `cursor` only where a click\n * would insert.\n */\n\nimport type { Action, ActionDeps, ImmediateInvoker } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { CursorSpec } from '@weasel-js/cursor';\nimport type { EditAnchorsDep } from '../depSchema';\nimport type { PolygonPath } from '@weasel-js/geom';\nimport { pathToAnchors } from 'features/paths/anchors';\nimport {\n editAnchorSet,\n insertAnchorOnSegment,\n segmentAt,\n type AnchorSet,\n type SegmentHit,\n} from 'features/paths/anchorEdits';\nimport { inPlane, editingLayer } from '../planeInput';\n\n/** Screen-px reach of a segment, and the radius around each anchor that\n * stays the anchor's. Matches the path-anchor affordance's default hit\n * radius, so the two regions meet without a gap. */\nconst SEGMENT_HIT_PX = 8;\n\nconst INSERT_CURSOR: CursorSpec = { glyph: 'penPlus', fallback: 'crosshair' };\n\nfunction segmentUnder(\n deps: ActionDeps | undefined,\n x: number,\n y: number,\n): { dep: EditAnchorsDep; hit: SegmentHit } | null {\n const dep = deps?.editAnchors;\n if (!dep?.editingId) return null;\n const path = dep.getEditablePath(dep.editingId) as PolygonPath | null | undefined;\n if (!path || path.kind !== 'polygon') return null;\n const scale = deps?.view?.get().scale;\n const hit = segmentAt(pathToAnchors(path) as AnchorSet, x, y, {\n tolerancePx: SEGMENT_HIT_PX,\n ...(scale ? { scale } : {}),\n });\n return hit ? { dep, hit } : null;\n}\n\nexport const insertPathAnchorAction: Action & { requires: string[] } = inPlane({\n id: 'insertPathAnchor',\n label: 'Insert anchor',\n defaultBinding: { kind: 'click', mods: { alt: true } },\n cursor: INSERT_CURSOR,\n eligible: { capability: 'edits-anchors' },\n requires: ['editAnchors', 'view'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const wx = params?.worldX as number | undefined;\n const wy = params?.worldY as number | undefined;\n if (typeof wx !== 'number' || typeof wy !== 'number') return;\n const under = segmentUnder(deps, wx, wy);\n if (!under) return;\n const { dep, hit } = under;\n const path = dep.getEditablePath(dep.editingId) as PolygonPath;\n let inserted = -1;\n const next = editAnchorSet(path, (set) => {\n inserted = insertAnchorOnSegment(set, hit);\n return inserted !== -1;\n });\n if (!next) return;\n dep.applyEdit(dep.editingId, next, 'Insert anchor');\n dep.setSelectedAnchors([inserted]);\n },\n } as ImmediateInvoker,\n enabled: (deps, at) => {\n const dep = deps?.editAnchors;\n if (!dep?.editingId) return ActionDisabledReason.NotApplicable;\n if (at && !segmentUnder(deps, at.x, at.y)) return ActionDisabledReason.NotApplicable;\n return true;\n },\n}, editingLayer);\n"],"mappings":";;;;;AAiCA,IAAMA,IAAiB,GAEjBC,IAA4B;CAAE,OAAO;CAAW,UAAU;AAAY;AAE5E,SAASC,EACP,GACA,GACA,GACiD;CACjD,IAAMC,IAAMC,GAAM;CAClB,IAAI,CAACD,GAAK,WAAW,OAAO;CAC5B,IAAME,IAAOF,EAAI,gBAAgBA,EAAI,SAAS;CAC9C,IAAI,CAACE,KAAQA,EAAK,SAAS,WAAW,OAAO;CAC7C,IAAMC,IAAQF,GAAM,MAAM,IAAI,CAAC,CAAC,OAC1BG,IAAMC,EAAUC,EAAcJ,CAAI,GAAgBK,GAAGC,GAAG;EAC5D,aAAaX;EACb,GAAIM,IAAQ,EAAE,SAAM,IAAI,CAAC;CAC3B,CAAC;CACD,OAAOC,IAAM;EAAE;EAAK;CAAI,IAAI;AAC9B;AAEA,IAAaK,IAA0DC,EAAQ;CAC7E,IAAI;CACJ,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAS,MAAM,EAAE,KAAK,GAAK;CAAE;CACrD,QAAQZ;CACR,UAAU,EAAE,YAAY,gBAAgB;CACxC,UAAU,CAAC,eAAe,MAAM;CAChC,SAAS;EACP,QAAQ;EACR,IAAI,GAAM,GAAQ;GAChB,IAAMa,IAAKC,GAAQ,QACbC,IAAKD,GAAQ;GACnB,IAAI,OAAOD,KAAO,YAAY,OAAOE,KAAO,UAAU;GACtD,IAAMC,IAAQf,EAAaE,GAAMU,GAAIE,CAAE;GACvC,IAAI,CAACC,GAAO;GACZ,IAAM,EAAE,QAAK,WAAQA,GACfZ,IAAOF,EAAI,gBAAgBA,EAAI,SAAS,GAC1Ce,IAAW,IACTC,IAAOC,EAAcf,IAAO,OAChC,IAAWgB,EAAsBC,GAAKf,CAAG,GAClCW,MAAa,GACrB;GACD,AAAKC,MACLhB,EAAI,UAAUA,EAAI,WAAWgB,GAAM,eAAe,GAClDhB,EAAI,mBAAmB,CAACe,CAAQ,CAAC;EACnC;CACF;CACA,UAAU,GAAM,MAEV,CADQd,GAAM,aACR,aACNmB,KAAM,CAACrB,EAAaE,GAAMmB,EAAG,GAAGA,EAAG,CAAC,IAAUC,EAAqB,gBAChE;AAEX,GAAGC,CAAY"}
|