@weasel-js/core 1.14.1 → 1.15.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 +218 -0
- package/README.md +1 -1
- package/dist/affordances/pathAnchors.js +0 -1
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/paths.js +1 -1
- package/dist/barrel/renderer.js +2 -2
- package/dist/barrel/routing.js +6 -4
- package/dist/barrel/text.js +5 -3
- package/dist/canvas/Canvas.js +5 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +5 -1
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
- package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
- package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
- package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +5 -4
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +5 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/affordanceAt.js +0 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/deps/chromeRules.js +37 -0
- package/dist/canvas/deps/chromeRules.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +0 -6
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +3 -1
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/modeDefaultTool.js +23 -0
- package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
- package/dist/canvas/deps/pathEditCommits.js +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +23 -0
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
- package/dist/canvas/deps/snap.js +1 -0
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/pickReach.js +12 -8
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +7 -5
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/scenePickSource.js +2 -2
- package/dist/canvas/scenePickSource.js.map +1 -1
- package/dist/canvas/useCanvasSurface.js +21 -3
- package/dist/canvas/useCanvasSurface.js.map +1 -1
- package/dist/canvas/useFrameLoop.js +6 -7
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSurfaceSize.js +4 -3
- package/dist/canvas/useSurfaceSize.js.map +1 -1
- package/dist/core/geometry/regionHit.js +35 -16
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +9 -10
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/editMode.js +4 -1
- package/dist/core/scene/editMode.js.map +1 -1
- package/dist/core/scene/historyFacade.js +7 -50
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/lockGuard.js +1 -0
- package/dist/core/scene/lockGuard.js.map +1 -1
- package/dist/core/scene/nodeGates.js +43 -23
- package/dist/core/scene/nodeGates.js.map +1 -1
- package/dist/core/scene/opTable.js +8 -8
- package/dist/core/scene/opTable.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -2
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/poseRemovals.js +1 -0
- package/dist/core/scene/poseRemovals.js.map +1 -1
- package/dist/core/scene/poseWrites.js +16 -15
- package/dist/core/scene/poseWrites.js.map +1 -1
- package/dist/core/scene/sceneJournals.js +78 -0
- package/dist/core/scene/sceneJournals.js.map +1 -0
- package/dist/core/scene/sceneRecording.js +3 -2
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/scene/statedFields.js +10 -8
- package/dist/core/scene/statedFields.js.map +1 -1
- package/dist/core/scene/treeEdits.js +4 -9
- package/dist/core/scene/treeEdits.js.map +1 -1
- package/dist/core/viewport/useCanvasSize.js +47 -26
- package/dist/core/viewport/useCanvasSize.js.map +1 -1
- package/dist/{depSchema-C3BwxitB.d.ts → depSchema-ChQPzEkW.d.ts} +56 -25
- package/dist/domains/chrome-caps/resolve.js +1 -0
- package/dist/domains/chrome-caps/resolve.js.map +1 -1
- package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
- package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
- package/dist/domains/paths/pathEditState.js +0 -24
- package/dist/domains/paths/pathEditState.js.map +1 -1
- package/dist/domains/snap/probes2d.js +5 -0
- package/dist/domains/snap/probes2d.js.map +1 -1
- package/dist/domains/snap/resizeBox.js +43 -22
- package/dist/domains/snap/resizeBox.js.map +1 -1
- package/dist/domains/snap/sceneReads.js +11 -1
- package/dist/domains/snap/sceneReads.js.map +1 -1
- package/dist/domains/snap/sceneSession2d.js +4 -2
- package/dist/domains/snap/sceneSession2d.js.map +1 -1
- package/dist/domains/snap/session2d.js +14 -7
- package/dist/domains/snap/session2d.js.map +1 -1
- package/dist/domains/text/index.js +2 -2
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +5 -0
- package/dist/index.d.ts +114 -62
- package/dist/index.js +12 -8
- package/dist/interactions/actions/booleans/action.js +4 -2
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -2
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +21 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/params.js +43 -0
- package/dist/interactions/actions/insert/params.js.map +1 -0
- package/dist/interactions/actions/notices.js +17 -0
- package/dist/interactions/actions/notices.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +8 -6
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
- package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
- package/dist/interactions/actions/testUtils.js +0 -7
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/useNotices.js +13 -0
- package/dist/interactions/actions/useNotices.js.map +1 -0
- package/dist/test-seams.d.ts +5 -12
- package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
- package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
- package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
- package/dist/tools/builtin/star/useStarTool.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/toolOptions.js +47 -0
- package/dist/tools/toolOptions.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
- package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SceneCanvas.js","names":[],"sources":["../../src/canvas/SceneCanvas.tsx"],"sourcesContent":["/**\n * `<SceneCanvas>` — `<Canvas>` wired to a `Scene` primitive. Bare, it only renders: behavior\n * comes from the `features` presets (`canvas/SceneCanvas/features.ts`), each of which turns on\n * its own actions, tools, ambient bindings, and chrome.\n *\n * A consumer needing custom tools can pass `tools={useTools(...)}` and SceneCanvas forwards it\n * as-is — the internal tool registry is ignored in that case.\n */\nimport { getActiveModeFor } from '@weasel-js/modes';\nimport { useLatest } from '@weasel-js/react';\nimport { InputScope, useActionsRegistry } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { useCanvasCamera } from './SceneCanvas/useCanvasCamera';\nimport { useHeldModifiers } from './SceneCanvas/useHeldModifiers';\nimport { useCanvasIngestion } from 'domains/ingestion/useCanvasIngestion';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type React from 'react';\nimport { forwardRef, useEffect, useMemo, useRef } from 'react';\nimport { DeviceProfileProvider, useDeviceProfile } from '../core/device/useDeviceProfile';\nimport { Canvas } from './Canvas';\nimport { canvasDebug, DevHuds } from './SceneCanvas/DevHuds';\nimport { GestureReason } from './GestureReason';\nimport { CanvasKitProviders } from './SceneCanvas/CanvasKitProviders';\nimport { canvasPassThrough } from './SceneCanvas/canvasPassThrough';\nimport { SceneCanvasInput, type SceneCanvasInputOptions } from './SceneCanvas/SceneCanvasInput';\nimport type { PresetSource } from './SceneCanvas/features';\nimport { CORE_PRESET_SOURCE } from './SceneCanvas/namedPresets';\nimport type { BridgeAdapter, SceneCanvasProps } from './SceneCanvas/props';\nimport { useBakedScene } from './SceneCanvas/useBakedScene';\nimport { useCanvasDispatcher } from './SceneCanvas/useCanvasDispatcher';\nimport { publishPreviews } from './publishPreviews';\nimport { useCanvasSelection } from './SceneCanvas/useCanvasSelection';\nimport { useEnabledFeatures } from './SceneCanvas/useCanvasTools';\nimport { contributionEntries } from './SceneCanvas/useContributionRoles';\nimport { SurfaceDeclarations } from './SceneCanvas/SurfaceDeclarations';\nimport { useAddedViews, useSceneCanvasHandle } from './SceneCanvas/useSceneCanvasHandle';\nimport { useSceneCanvasLayers } from './SceneCanvas/useSceneCanvasLayers';\nimport { useSceneCanvasPathEdit } from './SceneCanvas/useSceneCanvasPathEdit';\nimport { useSceneCanvasTools, type SceneCanvasToolProps } from './SceneCanvas/useSceneCanvasTools';\nimport { useSceneCanvasChrome } from './SceneCanvas/useSceneChrome';\nimport { useDevtoolsOverride } from '../devtools/useDevtoolsRoot';\nimport { useSceneRedrawSources } from './SceneCanvas/useSceneRedrawSources';\nimport { useScenePicking } from './SceneCanvas/useScenePicking';\nimport { useGroupEntryContribution } from 'tools/builtin/select/groupEntry';\nimport { useNodeKinds } from './SceneCanvas/useNodeKinds';\nimport { DefaultTextEditDep, useCanvasTextEditor } from './SceneCanvas/defaultTextEditor';\nimport { useSurfaceViewInputs } from './SceneCanvas/useSurfaceViewInputs';\nimport type { CanvasExtensionApi, SceneCanvasApi } from './canvasExtension';\nimport { ViewRegistryProvider } from './viewRegistry';\nimport { useChromeColor } from './accentColor';\nimport { ENTERED_FRAME_COLOR, ENTERED_FRAME_TOKEN } from 'domains/selection/enteredGroupFrame';\nexport { SCENE_CANVAS_FEATURES, type Feature } from './SceneCanvas/features';\nexport type { BuiltinToolOptions } from './SceneCanvas/useBuiltinShapeTools';\nexport { defaultDrawOne } from './defaultDrawOne';\nexport { rotateAroundAABBCenter } from './poseRotation';\n\nfunction SceneCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneCanvasProps<TData, TLayer, TPose> & { presetSource: PresetSource },\n ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n const { selectTool: selectToolOpts, selectable = true, viewport, modes } = props;\n const devtools = useDevtoolsOverride(props.debug), { debug } = devtools;\n const descriptor = (props.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const deviceProfile = useDeviceProfile();\n const scene = useBakedScene(props.scene);\n const getActiveMode = useMemo(() => (modes ? getActiveModeFor(modes) : undefined), [modes]);\n const heldModifiers = useHeldModifiers();\n\n // Our own refs, so input listeners attach even when the consumer forwards its own ref.\n const internalCanvasRef = useRef<HTMLElement | null>(null);\n const canvasApiRef = useRef<CanvasExtensionApi | null>(null);\n const declaredSize = useLatest({ width: props.width ?? 0, height: props.height ?? 0 });\n const { viewState, viewAnimation, camera } = useCanvasCamera({\n view: props.view, defaultView: props.defaultView, onViewChange: props.onViewChange, viewBounds: props.viewBounds,\n dims: () => canvasApiRef.current?.getSurfaceRect() ?? declaredSize.current,\n requestRedraw: () => { canvasApiRef.current?.requestRedraw(); },\n });\n const { viewRef } = viewState;\n const kinds = useNodeKinds(scene, props.routing);\n const selection = useCanvasSelection({ scene, props, selectable, kindOf: kinds.kindOf });\n const enabled = useEnabledFeatures(\n props.features, props.presetSource.named, viewport !== undefined, selectToolOpts?.rotate !== false,\n );\n const { pathEditState, selectToolWithDefaults } = useSceneCanvasPathEdit({\n host: props.pathEdit, opening: props.defaultPathEdit, getActiveMode, scene, selection,\n extensions: enabled.pathEdit, selectTool: selectToolOpts,\n });\n const picking = useScenePicking({ scene, selection, descriptor, props, viewRef, selectTool: selectToolWithDefaults, kinds });\n const { adapter, boundsOf, planeOfNode } = picking;\n const getNodeAtPointRef = useLatest(picking.getNodeAtPoint);\n const groupEntry = useGroupEntryContribution({ pickHit: picking.pickBest, tree: adapter, boxOf: boundsOf });\n const tools = useSceneCanvasTools({\n props: props as SceneCanvasToolProps, select: picking.selectTool, move: picking.moveOptions,\n enabled, presetSource: props.presetSource, groupEntry,\n });\n\n // The wrapper's `<InputScope>` provides it.\n const actionsRegistry = useActionsRegistry();\n const { dispatcher, gestureSource, previewIdsExtra, previewPoseExtra } = useCanvasDispatcher(actionsRegistry);\n useEffect(() => publishPreviews(scene, dispatcher), [scene, dispatcher]);\n\n const frameColor = useChromeColor(internalCanvasRef, ENTERED_FRAME_TOKEN, ENTERED_FRAME_COLOR);\n const { editor: textEditor, layers } = useCanvasTextEditor(scene as Scene<TData, string, unknown>, internalCanvasRef, props);\n const layered = useSceneCanvasLayers({\n layers, presetLayers: enabled.layers, targetScale: deviceProfile.targetScale, scene, adapter, poseDescriptor: descriptor,\n enteredGroup: picking.enteredGroup, frameColor, alphaFor: props.alphaFor, colorOverrides: props.animator?.colorOverrides, boundsOf, planeOfNode,\n poseTransition: props.poseTransition, previewIds: previewIdsExtra, dispatcher, snapReadout: props.snap?.readout !== false, snapReadoutStyle: props.snap?.readoutStyle, pathEditState,\n slops: debug !== undefined && debug !== false && debug.slops === true,\n boundsIncludeStroke: props.geometry?.boundsIncludeStroke ?? true, selectTool: selectToolOpts,\n });\n const redrawOn = useSceneRedrawSources({ props, scene, heldModifiers, departures: layered.departures });\n\n const chrome = useSceneCanvasChrome({\n source: {\n getMode: getActiveMode, getFocused: props.getFocused, getModifiers: heldModifiers.get,\n resizable: selectToolOpts?.resize?.resizable,\n editingAnchors: () => pathEditState.getEditingId() !== '',\n device: deviceProfile,\n },\n rules: { enabled, selectable, chromeVisibility: props.chromeVisibility },\n hover: { canvasApiRef, nodeAtPointRef: getNodeAtPointRef, ...(props.onHover ? { onChange: props.onHover } : {}) },\n selection: selection.current as readonly NodeId[], viewRef, dispatcher,\n });\n\n const { ingest, clipboard: ingestionClipboard, handlers: ingestionHandlers } = useCanvasIngestion(adapter, () => actionsRegistry, props.ingestion);\n const { views: addedViews, addView, viewAt, aimOf, setAim } = useAddedViews();\n // The consumer's handle extends the primitive's with `ingest`, which needs the action stack.\n const { handleRef, ready: canvasReady } = useSceneCanvasHandle({\n ref, elementRef: internalCanvasRef, apiRef: canvasApiRef, onElement: chrome.trackHoverOn,\n ingest, viewAnimation, camera, addView, viewAt, aimOf, setAim,\n });\n const viewInputs = useSurfaceViewInputs({ picking, selection, resizeHandles: layered.resizeHandles,\n descriptor: descriptor as PoseDescriptor<unknown>, chromeCaps: chrome.chromeCaps, rotationBadge: layered.rotationBadge,\n });\n\n return (\n <CanvasKitProviders viewInputs={viewInputs}>\n <Canvas<TPose>\n ref={handleRef} adapter={adapter} selection={selection} poseDescriptor={descriptor}\n tools={tools.tools} layers={layered.layers}\n // The chrome's bounds are the picker's: a node on a parallax plane is\n // boxed where the plane draws it, and a `geometry.boundsOf` reaches both.\n boundsOf={boundsOf} boundsOfPose={picking.boundsOfPose}\n getIsVisible={chrome.getIsVisible}\n // The gesture surface behind `helpersRef.getGestureBounds()` /\n // `subscribeGestures()` — Canvas has no dispatcher of its own.\n gestureSource={gestureSource}\n previewPoseExtra={previewPoseExtra}\n {...(debug !== undefined ? { debug: canvasDebug(debug) } : {})}\n contentVersion={scene.getVersion} layerVisibility={props.layerVisibility} layerOrder={props.layerOrder}\n viewState={viewState} redrawOn={redrawOn}\n {...canvasPassThrough(props)}\n />\n <DevHuds\n cursorCoords={props.cursorCoordsHud} pick={props.pickHud} modality={props.modalityHud}\n canvasRef={internalCanvasRef} canvasApiRef={canvasApiRef}\n detachedInput={props.paintInto ? props.inputElement ?? null : undefined}\n viewRef={viewRef} pickEvery={picking.pickEvery} pickBest={picking.pickBest}\n />\n <GestureReason dispatcher={dispatcher} canvasRef={internalCanvasRef} viewRef={viewRef} />\n <SceneCanvasInput\n props={props as SceneCanvasInputOptions} scene={scene as Scene<unknown, string, unknown>}\n selection={selection} descriptor={descriptor as PoseDescriptor<unknown>}\n picking={{ ...picking, adapter: adapter as unknown as BridgeAdapter }} nodeAtPointRef={getNodeAtPointRef}\n tools={tools} viewOn={enabled.has('view')} camera={{ viewState, viewAnimation }}\n dispatcher={dispatcher} pathEdit={{ state: pathEditState }}\n chrome={chrome} overlay={layered} ingestionClipboard={ingestionClipboard} ingestionHandlers={ingestionHandlers} devtools={devtools}\n canvas={{ elementRef: internalCanvasRef, apiRef: canvasApiRef, ready: canvasReady }} snapSources={enabled.snapSources} snapStore={layered.snapStore}\n />\n <SurfaceDeclarations views={props.views} entries={contributionEntries(tools.tools)} added={addedViews} />\n <DefaultTextEditDep editor={textEditor} onDoubleClick={enabled.has('edit')} />\n {props.children}\n </CanvasKitProviders>\n );\n}\n\nconst SceneCanvasInnerForwardRef = forwardRef(SceneCanvasInner);\n\n// The canvas's input scope sits above `SceneCanvasInner`, whose own hooks\n// (`useTools` among them) must already read this canvas's tool and registries.\n// Its device profile too, so the canvas and everything under it share one.\n/** A scene canvas resolving its `features` through `source`. */\nexport function renderSceneCanvas<TData, TLayer extends string, TPose>(\n source: PresetSource,\n { device, ...props }: SceneCanvasProps<TData, TLayer, TPose>,\n ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n return (\n <InputScope yoke={props.yoke}>\n <DeviceProfileProvider value={device}>\n <ViewRegistryProvider>\n <SceneCanvasInnerForwardRef {...(props as SceneCanvasProps<unknown, string, unknown>)} presetSource={source} ref={ref} />\n </ViewRegistryProvider>\n </DeviceProfileProvider>\n </InputScope>\n );\n}\n\nfunction SceneCanvasWrapper<TData, TLayer extends string, TPose>(\n props: SceneCanvasProps<TData, TLayer, TPose>, ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n return renderSceneCanvas(CORE_PRESET_SOURCE, props, ref);\n}\n\n// Marked pure so a bundle using only `LeanSceneCanvas` drops it, and core's named presets with it.\n/**\n * The canvas component: renders a `Scene` and wires the interaction stack\n * around it.\n *\n * Mounted bare it renders the scene, keeps a selection no input sets, and\n * runs a gesture dispatcher for whatever bindings the consumer adds. The\n * `features` presets turn the kit's behavior on — `features={['draw']}` is the\n * whole editor: tools, standard actions and their keys, selection chrome,\n * undo. This is the intended entry point; the lower-level primitives it\n * composes are not part of the public surface.\n *\n * Its ref exposes a `SceneCanvasApi` for the imperative operations that do not\n * fit a prop (view control, hit queries, redraw requests).\n */\nexport const SceneCanvas = /* @__PURE__ */ forwardRef(SceneCanvasWrapper) as <\n TData, TLayer extends string, TPose,\n>(\n props: SceneCanvasProps<TData, TLayer, TPose> & { ref?: React.Ref<SceneCanvasApi> },\n) => ReturnType<typeof SceneCanvasInner>;\n\nexport { DEFAULT_HANDLE_SIZE, BUILTIN_TOOL_IDS } from './SceneCanvas/props';\nexport {\n BUILTIN_TOOLS, BUILTIN_TOOL_GROUPS, TOOL_GROUP_ORDER,\n SELECT_TOOLS, VIEW_TOOLS, SHAPE_TOOLS, DRAW_TOOLS, TYPE_TOOLS, PAGE_TOOLS,\n} from './SceneCanvas/builtinToolSets';\nexport { mergeLayersWithDefaults } from './SceneCanvas/mergeLayers';\nexport type {\n SceneCanvasProps, SceneCanvasHit, SceneCanvasLayers, SceneCanvasLongPress, BuiltinToolId,\n} from './SceneCanvas/props';\nexport type { CoordTraceEntry } from './SceneCanvas/coordTrace';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SceneCanvas.js","names":[],"sources":["../../src/canvas/SceneCanvas.tsx"],"sourcesContent":["/**\n * `<SceneCanvas>` — `<Canvas>` wired to a `Scene` primitive. Bare, it only renders: behavior\n * comes from the `features` presets (`canvas/SceneCanvas/features.ts`), each of which turns on\n * its own actions, tools, ambient bindings, and chrome.\n *\n * A consumer needing custom tools can pass `tools={useTools(...)}` and SceneCanvas forwards it\n * as-is — the internal tool registry is ignored in that case.\n */\nimport { getActiveModeFor } from '@weasel-js/modes';\nimport { useLatest } from '@weasel-js/react';\nimport { InputScope, useActionsRegistry } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { useCanvasCamera } from './SceneCanvas/useCanvasCamera';\nimport { useHeldModifiers } from './SceneCanvas/useHeldModifiers';\nimport { useCanvasIngestion } from 'domains/ingestion/useCanvasIngestion';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type React from 'react';\nimport { forwardRef, useEffect, useMemo, useRef } from 'react';\nimport { DeviceProfileProvider, useDeviceProfile } from '../core/device/useDeviceProfile';\nimport { Canvas } from './Canvas';\nimport { canvasDebug, DevHuds } from './SceneCanvas/DevHuds';\nimport { GestureReason } from './GestureReason';\nimport { CanvasKitProviders } from './SceneCanvas/CanvasKitProviders';\nimport { canvasPassThrough } from './SceneCanvas/canvasPassThrough';\nimport { SceneCanvasInput, type SceneCanvasInputOptions } from './SceneCanvas/SceneCanvasInput';\nimport type { PresetSource } from './SceneCanvas/features';\nimport { CORE_PRESET_SOURCE } from './SceneCanvas/namedPresets';\nimport type { BridgeAdapter, SceneCanvasProps } from './SceneCanvas/props';\nimport { useBakedScene } from './SceneCanvas/useBakedScene';\nimport { useCanvasDispatcher } from './SceneCanvas/useCanvasDispatcher';\nimport { publishPreviews } from './publishPreviews';\nimport { useCanvasSelection } from './SceneCanvas/useCanvasSelection';\nimport { useEnabledFeatures } from './SceneCanvas/useCanvasTools';\nimport { contributionEntries } from './SceneCanvas/useContributionRoles';\nimport { SurfaceDeclarations } from './SceneCanvas/SurfaceDeclarations';\nimport { useAddedViews, useSceneCanvasHandle } from './SceneCanvas/useSceneCanvasHandle';\nimport { useSceneCanvasLayers } from './SceneCanvas/useSceneCanvasLayers';\nimport { useSceneCanvasPathEdit } from './SceneCanvas/useSceneCanvasPathEdit';\nimport { useSceneCanvasTools, type SceneCanvasToolProps } from './SceneCanvas/useSceneCanvasTools';\nimport { hiddenChromeOf, useSceneCanvasChrome } from './SceneCanvas/useSceneChrome';\nimport { useDevtoolsOverride } from '../devtools/useDevtoolsRoot';\nimport { useSceneRedrawSources } from './SceneCanvas/useSceneRedrawSources';\nimport { useScenePicking } from './SceneCanvas/useScenePicking';\nimport { useGroupEntryContribution } from 'tools/builtin/select/groupEntry';\nimport { useNodeKinds } from './SceneCanvas/useNodeKinds';\nimport { DefaultTextEditDep, useCanvasTextEditor } from './SceneCanvas/defaultTextEditor';\nimport { useSurfaceViewInputs } from './SceneCanvas/useSurfaceViewInputs';\nimport type { CanvasExtensionApi, SceneCanvasApi } from './canvasExtension';\nimport { ViewRegistryProvider } from './viewRegistry';\nimport { useChromeColor } from './accentColor';\nimport { ENTERED_FRAME_COLOR, ENTERED_FRAME_TOKEN } from 'domains/selection/enteredGroupFrame';\nexport { SCENE_CANVAS_FEATURES, type Feature } from './SceneCanvas/features';\nexport type { BuiltinToolOptions } from './SceneCanvas/useBuiltinShapeTools';\nexport { defaultDrawOne } from './defaultDrawOne';\nexport { rotateAroundAABBCenter } from './poseRotation';\n\nfunction SceneCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneCanvasProps<TData, TLayer, TPose> & { presetSource: PresetSource },\n ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n const { selectTool: selectToolOpts, selectable = true, viewport, modes } = props;\n const devtools = useDevtoolsOverride(props.debug), { debug } = devtools;\n const descriptor = (props.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const deviceProfile = useDeviceProfile();\n const scene = useBakedScene(props.scene);\n const getActiveMode = useMemo(() => (modes ? getActiveModeFor(modes) : undefined), [modes]);\n const heldModifiers = useHeldModifiers();\n\n // Our own refs, so input listeners attach even when the consumer forwards its own ref.\n const internalCanvasRef = useRef<HTMLElement | null>(null);\n const canvasApiRef = useRef<CanvasExtensionApi | null>(null);\n const declaredSize = useLatest({ width: props.width ?? 0, height: props.height ?? 0 });\n const { viewState, viewAnimation, camera } = useCanvasCamera({\n view: props.view, defaultView: props.defaultView, onViewChange: props.onViewChange, viewBounds: props.viewBounds,\n dims: () => canvasApiRef.current?.getSurfaceRect() ?? declaredSize.current,\n requestRedraw: () => { canvasApiRef.current?.requestRedraw(); },\n });\n const { viewRef } = viewState;\n const kinds = useNodeKinds(scene, props.routing);\n const selection = useCanvasSelection({ scene, props, selectable, kindOf: kinds.kindOf });\n const enabled = useEnabledFeatures(\n props.features, props.presetSource.named, viewport !== undefined, selectToolOpts?.rotate !== false,\n );\n const { pathEditState, selectToolWithDefaults } = useSceneCanvasPathEdit({\n host: props.pathEdit, opening: props.defaultPathEdit, getActiveMode, scene, selection,\n extensions: enabled.pathEdit, selectTool: selectToolOpts,\n });\n const picking = useScenePicking({ scene, selection, descriptor, props, viewRef, selectTool: selectToolWithDefaults, kinds });\n const { adapter, boundsOf, planeOfNode } = picking;\n const getNodeAtPointRef = useLatest(picking.getNodeAtPoint);\n const groupEntry = useGroupEntryContribution({ pickHit: picking.pickBest, tree: adapter, boxOf: boundsOf });\n const tools = useSceneCanvasTools({\n props: props as SceneCanvasToolProps, select: picking.selectTool, move: picking.moveOptions,\n enabled, presetSource: props.presetSource, groupEntry,\n });\n\n // The wrapper's `<InputScope>` provides it.\n const actionsRegistry = useActionsRegistry();\n const { dispatcher, gestureSource, previewIdsExtra, previewPoseExtra } = useCanvasDispatcher(actionsRegistry);\n useEffect(() => publishPreviews(scene, dispatcher), [scene, dispatcher]);\n\n const frameColor = useChromeColor(internalCanvasRef, ENTERED_FRAME_TOKEN, ENTERED_FRAME_COLOR);\n const { editor: textEditor, layers } = useCanvasTextEditor(scene as Scene<TData, string, unknown>, internalCanvasRef, props);\n const layered = useSceneCanvasLayers({\n layers, presetLayers: enabled.layers, targetScale: deviceProfile.targetScale, scene, adapter, poseDescriptor: descriptor,\n enteredGroup: picking.enteredGroup, frameColor, alphaFor: props.alphaFor, colorOverrides: props.animator?.colorOverrides, boundsOf, planeOfNode,\n poseTransition: props.poseTransition, previewIds: previewIdsExtra, dispatcher, snapReadout: props.snap?.readout !== false, snapReadoutStyle: props.snap?.readoutStyle, pathEditState,\n slops: debug !== undefined && debug !== false && debug.slops === true,\n boundsIncludeStroke: props.geometry?.boundsIncludeStroke ?? true, selectTool: selectToolOpts,\n });\n const redrawOn = useSceneRedrawSources({ props, scene, heldModifiers, departures: layered.departures });\n\n const chrome = useSceneCanvasChrome({\n source: {\n getMode: getActiveMode, getFocused: props.getFocused, getModifiers: heldModifiers.get,\n resizable: selectToolOpts?.resize?.resizable,\n editingAnchors: () => pathEditState.getEditingId() !== '', device: deviceProfile,\n getHiddenChrome: () => hiddenChromeOf(tools.tools),\n },\n rules: { enabled, selectable, chromeVisibility: props.chromeVisibility },\n hover: { canvasApiRef, nodeAtPointRef: getNodeAtPointRef, ...(props.onHover ? { onChange: props.onHover } : {}) },\n selection: selection.current as readonly NodeId[], viewRef, dispatcher,\n });\n\n const { ingest, clipboard: ingestionClipboard, handlers: ingestionHandlers } = useCanvasIngestion(adapter, () => actionsRegistry, props.ingestion);\n const { views: addedViews, addView, viewAt, aimOf, setAim } = useAddedViews();\n // The consumer's handle extends the primitive's with `ingest`, which needs the action stack.\n const { handleRef, ready: canvasReady } = useSceneCanvasHandle({\n ref, elementRef: internalCanvasRef, apiRef: canvasApiRef, onElement: chrome.trackHoverOn,\n ingest, viewAnimation, camera, addView, viewAt, aimOf, setAim,\n });\n const viewInputs = useSurfaceViewInputs({ picking, selection, resizeHandles: layered.resizeHandles,\n descriptor: descriptor as PoseDescriptor<unknown>, chromeCaps: chrome.chromeCaps, rotationBadge: layered.rotationBadge,\n });\n\n return (\n <CanvasKitProviders viewInputs={viewInputs}>\n <Canvas<TPose>\n ref={handleRef} adapter={adapter} selection={selection} poseDescriptor={descriptor}\n tools={tools.tools} layers={layered.layers}\n // The chrome's bounds are the picker's: a node on a parallax plane is\n // boxed where the plane draws it, and a `geometry.boundsOf` reaches both.\n boundsOf={boundsOf} boundsOfPose={picking.boundsOfPose}\n getIsVisible={chrome.getIsVisible}\n // The gesture surface behind `helpersRef.getGestureBounds()` /\n // `subscribeGestures()` — Canvas has no dispatcher of its own.\n gestureSource={gestureSource}\n previewPoseExtra={previewPoseExtra}\n {...(debug !== undefined ? { debug: canvasDebug(debug) } : {})}\n contentVersion={scene.getVersion} layerVisibility={props.layerVisibility} layerOrder={props.layerOrder}\n viewState={viewState} redrawOn={redrawOn}\n {...canvasPassThrough(props)}\n />\n <DevHuds\n cursorCoords={props.cursorCoordsHud} pick={props.pickHud} modality={props.modalityHud}\n canvasRef={internalCanvasRef} canvasApiRef={canvasApiRef}\n detachedInput={props.paintInto ? props.inputElement ?? null : undefined}\n viewRef={viewRef} pickEvery={picking.pickEvery} pickBest={picking.pickBest}\n />\n <GestureReason dispatcher={dispatcher} canvasRef={internalCanvasRef} viewRef={viewRef} />\n <SceneCanvasInput\n props={props as SceneCanvasInputOptions} scene={scene as Scene<unknown, string, unknown>}\n selection={selection} descriptor={descriptor as PoseDescriptor<unknown>}\n picking={{ ...picking, adapter: adapter as unknown as BridgeAdapter }} nodeAtPointRef={getNodeAtPointRef}\n tools={tools} viewOn={enabled.has('view')} camera={{ viewState, viewAnimation }}\n dispatcher={dispatcher} pathEdit={{ state: pathEditState }}\n chrome={chrome} overlay={layered} ingestionClipboard={ingestionClipboard} ingestionHandlers={ingestionHandlers} devtools={devtools}\n canvas={{ elementRef: internalCanvasRef, apiRef: canvasApiRef, ready: canvasReady }} snapSources={enabled.snapSources} snapStore={layered.snapStore}\n />\n <SurfaceDeclarations views={props.views} entries={contributionEntries(tools.tools)} added={addedViews} />\n <DefaultTextEditDep editor={textEditor} onDoubleClick={enabled.has('edit')} />\n {props.children}\n </CanvasKitProviders>\n );\n}\n\nconst SceneCanvasInnerForwardRef = forwardRef(SceneCanvasInner);\n\n// The canvas's input scope sits above `SceneCanvasInner`, whose own hooks\n// (`useTools` among them) must already read this canvas's tool and registries.\n// Its device profile too, so the canvas and everything under it share one.\n/** A scene canvas resolving its `features` through `source`. */\nexport function renderSceneCanvas<TData, TLayer extends string, TPose>(\n source: PresetSource,\n { device, ...props }: SceneCanvasProps<TData, TLayer, TPose>,\n ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n return (\n <InputScope yoke={props.yoke}>\n <DeviceProfileProvider value={device}>\n <ViewRegistryProvider>\n <SceneCanvasInnerForwardRef {...(props as SceneCanvasProps<unknown, string, unknown>)} presetSource={source} ref={ref} />\n </ViewRegistryProvider>\n </DeviceProfileProvider>\n </InputScope>\n );\n}\n\nfunction SceneCanvasWrapper<TData, TLayer extends string, TPose>(\n props: SceneCanvasProps<TData, TLayer, TPose>, ref: React.ForwardedRef<SceneCanvasApi>,\n) {\n return renderSceneCanvas(CORE_PRESET_SOURCE, props, ref);\n}\n\n// Marked pure so a bundle using only `LeanSceneCanvas` drops it, and core's named presets with it.\n/**\n * The canvas component: renders a `Scene` and wires the interaction stack\n * around it.\n *\n * Mounted bare it renders the scene, keeps a selection no input sets, and\n * runs a gesture dispatcher for whatever bindings the consumer adds. The\n * `features` presets turn the kit's behavior on — `features={['draw']}` is the\n * whole editor: tools, standard actions and their keys, selection chrome,\n * undo. This is the intended entry point; the lower-level primitives it\n * composes are not part of the public surface.\n *\n * Its ref exposes a `SceneCanvasApi` for the imperative operations that do not\n * fit a prop (view control, hit queries, redraw requests).\n */\nexport const SceneCanvas = /* @__PURE__ */ forwardRef(SceneCanvasWrapper) as <\n TData, TLayer extends string, TPose,\n>(\n props: SceneCanvasProps<TData, TLayer, TPose> & { ref?: React.Ref<SceneCanvasApi> },\n) => ReturnType<typeof SceneCanvasInner>;\n\nexport { DEFAULT_HANDLE_SIZE, BUILTIN_TOOL_IDS } from './SceneCanvas/props';\nexport {\n BUILTIN_TOOLS, BUILTIN_TOOL_GROUPS, TOOL_GROUP_ORDER,\n SELECT_TOOLS, ANCHOR_TOOLS, VIEW_TOOLS, SHAPE_TOOLS, DRAW_TOOLS, TYPE_TOOLS, PAGE_TOOLS,\n} from './SceneCanvas/builtinToolSets';\nexport { mergeLayersWithDefaults } from './SceneCanvas/mergeLayers';\nexport type {\n SceneCanvasProps, SceneCanvasHit, SceneCanvasLayers, SceneCanvasLongPress, BuiltinToolId,\n} from './SceneCanvas/props';\nexport type { CoordTraceEntry } from './SceneCanvas/coordTrace';\n"],"mappings":"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"}
|
|
@@ -51,7 +51,11 @@ function SceneViewCanvasInner(props) {
|
|
|
51
51
|
};
|
|
52
52
|
const { requestRedraw } = useCanvasSurface(paint, {
|
|
53
53
|
target: localRef,
|
|
54
|
-
onCommit: "paint"
|
|
54
|
+
onCommit: "paint",
|
|
55
|
+
size: {
|
|
56
|
+
width,
|
|
57
|
+
height
|
|
58
|
+
}
|
|
55
59
|
});
|
|
56
60
|
useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);
|
|
57
61
|
useEffect(() => scene.scroll?.subscribe(requestRedraw), [scene, requestRedraw]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SceneViewCanvas.js","names":[],"sources":["../../src/canvas/SceneViewCanvas.tsx"],"sourcesContent":["/**\n * `<SceneViewCanvas>` — pointer-inert, read-only render of a `Scene` at a\n * given `View` into its own `<canvas>` DOM element.\n *\n * It paints through `useFrameLoop` rather than from React: a render and an\n * override commit both just mark the surface dirty. The mount paint is\n * synchronous, so the first frame the user sees is the scene, and nothing\n * paints while the canvas is off screen or the tab is hidden.\n */\nimport {\n useCallback,\n useEffect,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { Ref } from 'react';\nimport { assignRef } from '@weasel-js/react';\nimport { useCanvasSurface } from './useCanvasSurface';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport { SurfaceCanvas } from './SurfaceCanvas';\nimport { useSurfaceSize, type SurfaceSizeProps } from './useSurfaceSize';\nimport { sceneViewFrame } from './sceneViewRender';\nimport type { RenderSceneToCanvasArgs, SceneViewDrawOne, SceneViewLayers } from './sceneViewRender';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { normalizeView, type View } from '../core/viewport/view';\nimport type { Scene } from '../core/scene/types';\nimport type { Animator } from '@weasel-js/animator';\n\n/** Props for `<SceneViewCanvas>`. */\nexport interface SceneViewCanvasProps<TData, TLayer extends string, TPose> extends SurfaceSizeProps {\n /** Scene to render. The component subscribes to it and repaints when it\n * changes. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera. Applied to scene + `extraCommands` via the renderer's group\n * transform. */\n view: View;\n /** Per-node draw callback. See `SceneViewDrawOne`. The consumer is\n * responsible for what gets painted — there is no AABB-fill default. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional extra commands appended after scene commands, inside the same\n * view transform — a world-space annotation over the scene. */\n extraCommands?: ReadonlyArray<DrawCommand>;\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Hide scene layers in this view only, keyed `scene:<layerId>` as on\n * `<SceneCanvas>` — pass the main canvas's map to keep the two in step. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Paint order for this view, keyed as `layerVisibility`. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Skip nodes and commands that cannot reach this view — the scene slot's\n * `cull`. Off by default; worth it when the view shows a small part of a\n * large scene. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach — the scene slot's\n * `paintBounds`. Defaults to `defaultPaintBounds` when `drawOne` is\n * `defaultDrawOne`. */\n paintBounds?: RenderSceneToCanvasArgs<TData, TLayer, TPose>['paintBounds'];\n /** Optional animator, as on `<SceneCanvas>`: the view repaints on its ticks\n * and paints its `colorOverrides`. Pass the main canvas's animator so both\n * show the same colors. */\n animator?: Animator;\n /** Optional CSS class for the `<canvas>`. Its size is `width` × `height`\n * unless a rule on this class sizes it. */\n className?: string;\n /** Forwarded ref to the underlying `<canvas>`. Consumers attach their own\n * pointer listeners or call `toBlob` for snapshots through this. */\n canvasRef?: Ref<HTMLCanvasElement>;\n}\n\nfunction SceneViewCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneViewCanvasProps<TData, TLayer, TPose>,\n) {\n const {\n scene, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n animator, className, canvasRef, cull, paintBounds,\n } = props;\n const view = normalizeView(props.view);\n\n // Subscribe to scene version. The snapshot value isn't used directly —\n // we only need React to re-render the component when the scene mutates.\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const localRef = useRef<HTMLCanvasElement | null>(null);\n\n const setCanvasRef = useCallback((el: HTMLCanvasElement | null): void => {\n localRef.current = el;\n assignRef(canvasRef, el);\n }, [canvasRef]);\n const { width, height } = useSurfaceSize(props, localRef);\n\n const paint = (lease: CanvasRendererLease): boolean => {\n const canvas = localRef.current;\n if (!canvas || width === 0 || height === 0) return false;\n const commands = sceneViewFrame({\n scene,\n view,\n width,\n height,\n drawOne,\n extraCommands: extraCommands as DrawCommand[] | undefined,\n alphaFor,\n layerVisibility,\n layerOrder,\n colorOverrides: animator?.colorOverrides,\n cull,\n paintBounds,\n });\n return lease.paint(canvas, commands, view, { width, height });\n };\n const { requestRedraw } = useCanvasSurface(paint, { target: localRef, onCommit: 'paint' });\n\n // Override writes never bump the scene version, so the version subscription\n // above cannot see them.\n useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => scene.scroll?.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => animator?.onTick(requestRedraw), [animator, requestRedraw]);\n\n return (\n <SurfaceCanvas\n ref={setCanvasRef}\n width={props.width}\n height={props.height}\n className={className}\n />\n );\n}\n\n/**\n * Pointer-inert read-only scene view. It attaches no event listeners;\n * consumers attach their own through `canvasRef`, a plain prop because\n * `forwardRef` erases the component's generics. `width` and `height` are CSS\n * pixels and size the drawing buffer; a `className` rule that sizes the\n * element stretches that buffer to fit. Given neither, it fills its parent.\n */\nexport const SceneViewCanvas = SceneViewCanvasInner;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuEA,SAAS,qBACP,OACA;CACA,MAAM,EACJ,OAAO,SAAS,eAAe,UAAU,iBAAiB,YAC1D,UAAU,WAAW,WAAW,MAAM,gBACpC;CACJ,MAAM,OAAO,cAAc,MAAM,IAAI;CAIrC,qBAAqB,MAAM,WAAW,MAAM,YAAY,MAAM,UAAU;CAExE,MAAM,WAAW,OAAiC,IAAI;CAEtD,MAAM,eAAe,aAAa,OAAuC;EACvE,SAAS,UAAU;EACnB,UAAU,WAAW,EAAE;CACzB,GAAG,CAAC,SAAS,CAAC;CACd,MAAM,EAAE,OAAO,WAAW,eAAe,OAAO,QAAQ;CAExD,MAAM,SAAS,UAAwC;EACrD,MAAM,SAAS,SAAS;EACxB,IAAI,CAAC,UAAU,UAAU,KAAK,WAAW,GAAG,OAAO;EACnD,MAAM,WAAW,eAAe;GAC9B;GACA;GACA;GACA;GACA;GACe;GACf;GACA;GACA;GACA,gBAAgB,UAAU;GAC1B;GACA;EACF,CAAC;EACD,OAAO,MAAM,MAAM,QAAQ,UAAU,MAAM;GAAE;GAAO;EAAO,CAAC;CAC9D;CACA,MAAM,EAAE,kBAAkB,iBAAiB,OAAO;EAAE,QAAQ;EAAU,UAAU;
|
|
1
|
+
{"version":3,"file":"SceneViewCanvas.js","names":[],"sources":["../../src/canvas/SceneViewCanvas.tsx"],"sourcesContent":["/**\n * `<SceneViewCanvas>` — pointer-inert, read-only render of a `Scene` at a\n * given `View` into its own `<canvas>` DOM element.\n *\n * It paints through `useFrameLoop` rather than from React: a render and an\n * override commit both just mark the surface dirty. The mount paint is\n * synchronous, so the first frame the user sees is the scene, and nothing\n * paints while the canvas is off screen or the tab is hidden.\n */\nimport {\n useCallback,\n useEffect,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { Ref } from 'react';\nimport { assignRef } from '@weasel-js/react';\nimport { useCanvasSurface } from './useCanvasSurface';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport { SurfaceCanvas } from './SurfaceCanvas';\nimport { useSurfaceSize, type SurfaceSizeProps } from './useSurfaceSize';\nimport { sceneViewFrame } from './sceneViewRender';\nimport type { RenderSceneToCanvasArgs, SceneViewDrawOne, SceneViewLayers } from './sceneViewRender';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { normalizeView, type View } from '../core/viewport/view';\nimport type { Scene } from '../core/scene/types';\nimport type { Animator } from '@weasel-js/animator';\n\n/** Props for `<SceneViewCanvas>`. */\nexport interface SceneViewCanvasProps<TData, TLayer extends string, TPose> extends SurfaceSizeProps {\n /** Scene to render. The component subscribes to it and repaints when it\n * changes. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera. Applied to scene + `extraCommands` via the renderer's group\n * transform. */\n view: View;\n /** Per-node draw callback. See `SceneViewDrawOne`. The consumer is\n * responsible for what gets painted — there is no AABB-fill default. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional extra commands appended after scene commands, inside the same\n * view transform — a world-space annotation over the scene. */\n extraCommands?: ReadonlyArray<DrawCommand>;\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Hide scene layers in this view only, keyed `scene:<layerId>` as on\n * `<SceneCanvas>` — pass the main canvas's map to keep the two in step. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Paint order for this view, keyed as `layerVisibility`. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Skip nodes and commands that cannot reach this view — the scene slot's\n * `cull`. Off by default; worth it when the view shows a small part of a\n * large scene. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach — the scene slot's\n * `paintBounds`. Defaults to `defaultPaintBounds` when `drawOne` is\n * `defaultDrawOne`. */\n paintBounds?: RenderSceneToCanvasArgs<TData, TLayer, TPose>['paintBounds'];\n /** Optional animator, as on `<SceneCanvas>`: the view repaints on its ticks\n * and paints its `colorOverrides`. Pass the main canvas's animator so both\n * show the same colors. */\n animator?: Animator;\n /** Optional CSS class for the `<canvas>`. Its size is `width` × `height`\n * unless a rule on this class sizes it. */\n className?: string;\n /** Forwarded ref to the underlying `<canvas>`. Consumers attach their own\n * pointer listeners or call `toBlob` for snapshots through this. */\n canvasRef?: Ref<HTMLCanvasElement>;\n}\n\nfunction SceneViewCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneViewCanvasProps<TData, TLayer, TPose>,\n) {\n const {\n scene, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n animator, className, canvasRef, cull, paintBounds,\n } = props;\n const view = normalizeView(props.view);\n\n // Subscribe to scene version. The snapshot value isn't used directly —\n // we only need React to re-render the component when the scene mutates.\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const localRef = useRef<HTMLCanvasElement | null>(null);\n\n const setCanvasRef = useCallback((el: HTMLCanvasElement | null): void => {\n localRef.current = el;\n assignRef(canvasRef, el);\n }, [canvasRef]);\n const { width, height } = useSurfaceSize(props, localRef);\n\n const paint = (lease: CanvasRendererLease): boolean => {\n const canvas = localRef.current;\n if (!canvas || width === 0 || height === 0) return false;\n const commands = sceneViewFrame({\n scene,\n view,\n width,\n height,\n drawOne,\n extraCommands: extraCommands as DrawCommand[] | undefined,\n alphaFor,\n layerVisibility,\n layerOrder,\n colorOverrides: animator?.colorOverrides,\n cull,\n paintBounds,\n });\n return lease.paint(canvas, commands, view, { width, height });\n };\n const { requestRedraw } = useCanvasSurface(paint, { target: localRef, onCommit: 'paint', size: { width, height } });\n\n // Override writes never bump the scene version, so the version subscription\n // above cannot see them.\n useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => scene.scroll?.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => animator?.onTick(requestRedraw), [animator, requestRedraw]);\n\n return (\n <SurfaceCanvas\n ref={setCanvasRef}\n width={props.width}\n height={props.height}\n className={className}\n />\n );\n}\n\n/**\n * Pointer-inert read-only scene view. It attaches no event listeners;\n * consumers attach their own through `canvasRef`, a plain prop because\n * `forwardRef` erases the component's generics. `width` and `height` are CSS\n * pixels and size the drawing buffer; a `className` rule that sizes the\n * element stretches that buffer to fit. Given neither, it fills its parent.\n */\nexport const SceneViewCanvas = SceneViewCanvasInner;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuEA,SAAS,qBACP,OACA;CACA,MAAM,EACJ,OAAO,SAAS,eAAe,UAAU,iBAAiB,YAC1D,UAAU,WAAW,WAAW,MAAM,gBACpC;CACJ,MAAM,OAAO,cAAc,MAAM,IAAI;CAIrC,qBAAqB,MAAM,WAAW,MAAM,YAAY,MAAM,UAAU;CAExE,MAAM,WAAW,OAAiC,IAAI;CAEtD,MAAM,eAAe,aAAa,OAAuC;EACvE,SAAS,UAAU;EACnB,UAAU,WAAW,EAAE;CACzB,GAAG,CAAC,SAAS,CAAC;CACd,MAAM,EAAE,OAAO,WAAW,eAAe,OAAO,QAAQ;CAExD,MAAM,SAAS,UAAwC;EACrD,MAAM,SAAS,SAAS;EACxB,IAAI,CAAC,UAAU,UAAU,KAAK,WAAW,GAAG,OAAO;EACnD,MAAM,WAAW,eAAe;GAC9B;GACA;GACA;GACA;GACA;GACe;GACf;GACA;GACA;GACA,gBAAgB,UAAU;GAC1B;GACA;EACF,CAAC;EACD,OAAO,MAAM,MAAM,QAAQ,UAAU,MAAM;GAAE;GAAO;EAAO,CAAC;CAC9D;CACA,MAAM,EAAE,kBAAkB,iBAAiB,OAAO;EAAE,QAAQ;EAAU,UAAU;EAAS,MAAM;GAAE;GAAO;EAAO;CAAE,CAAC;CAIlH,gBAAgB,MAAM,UAAU,UAAU,aAAa,GAAG,CAAC,OAAO,aAAa,CAAC;CAChF,gBAAgB,MAAM,QAAQ,UAAU,aAAa,GAAG,CAAC,OAAO,aAAa,CAAC;CAC9E,gBAAgB,UAAU,OAAO,aAAa,GAAG,CAAC,UAAU,aAAa,CAAC;CAE1E,OACE,oBAAC,eAAD;EACE,KAAK;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACH;CACZ,CAAA;AAEL;;;;;;;;AASA,IAAa,kBAAkB"}
|
|
@@ -40,7 +40,6 @@ function anchorStateFrom(getRegistry, toCamera) {
|
|
|
40
40
|
if (!dep) return null;
|
|
41
41
|
return {
|
|
42
42
|
editingId: dep.editingId ?? null,
|
|
43
|
-
tool: dep.tool ?? null,
|
|
44
43
|
getPose: (id) => toCamera ? toCamera(id, dep.getEditablePath(id)) : dep.getEditablePath(id),
|
|
45
44
|
getNode: (id) => dep.getNodeShape(id),
|
|
46
45
|
...dep.extensions ? { extensions: dep.extensions } : {},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"affordanceAt.js","names":[],"sources":["../../src/canvas/affordanceAt.ts"],"sourcesContent":["/**\n * The dispatcher-side affordance classifier: answers \"what piece of selection\n * chrome is under this world point?\" by assembling the kit's affordances and\n * running the one shared walk (`hitAffordanceRegions`).\n */\n\nimport type { BodyClassification } from '@weasel-js/gestures';\nimport type { ChromeState } from 'core/selection/chromeState';\nimport type { EditAnchorsDep } from 'interactions/actions/depSchema';\nimport type { View } from 'core/viewport/view';\nimport type { AffordanceHit, WorldPoint } from '@weasel-js/routing';\nimport type { Affordance, CommonAffordanceScratch, LayerHit } from 'affordances/types';\nimport { hitAffordanceRegions, type AffordanceRegionHit } from 'affordances/hitAffordanceRegions';\nimport { createCornerResizeAffordance } from 'affordances/cornerResize';\nimport { createRotationAffordance } from 'affordances/rotationHandle';\nimport { createPathAnchorAffordances, type AnchorState } from 'affordances/pathAnchors';\nimport { targetSizesPx } from 'core/device/targets';\n\nexport type { AnchorState };\n\n/** What decides the kit's chrome affordances — their sizes and which ones\n * exist. Shared by the hit-test and the slops overlay, which draws them. */\nexport interface ChromeAffordanceOptions {\n /** Pointer-size multiplier from the live `DeviceProfile`. Every default\n * below is resolved through {@link targetSizesPx} at this scale, so the\n * grab zone tracks the painted chrome on a coarse pointer. Default 1. */\n targetScale?: number;\n /** Corner-handle hit radius in screen px. Overrides the device-scaled default. */\n handleHitRadius?: number;\n /** Anchor / control hit radius in screen px. Overrides the device-scaled default. */\n anchorHitRadius?: number;\n /** Minimum rotate-band thickness outside the selection AABB, in screen px.\n * Overrides the device-scaled default. */\n rotateBandPx?: number;\n /** The rotate badge the selection overlay paints (`rotationBadgeOf`), so\n * it can be grabbed where it is drawn. Omitted or null, only the rotate\n * ring is hit-tested. */\n rotationBadge?: { distancePx: number; sizePx: number } | null;\n /** Resize handles at the edge midpoints as well as the corners. Default true. */\n edgeHandles?: boolean;\n /** Anchor-editing state. When omitted, anchors aren't hit-tested. */\n getAnchorState?: () => AnchorState | null;\n}\n\nexport interface BuildAffordanceAtOptions extends ChromeAffordanceOptions {\n /** Live ChromeState at call time — current selection plus effective bounds,\n * including in-flight move/resize ghost poses. */\n getChromeState: () => ChromeState;\n /** Live view. Needed because region hit radii and the rotate band are\n * declared in screen pixels and resolved against the current scale. */\n getView: () => View;\n /** The affordances to walk, when the caller built them itself to share\n * them (`chromeAffordances`). Otherwise built from these options. */\n affordances?: readonly Affordance[];\n /** Chrome-caps resolver. Keeps the hit-test and the renderer agreeing on\n * which chrome is live (resize handles gated by\n * `'selection.resize-handles'`, rotation by `'selection.rotation-handle'`,\n * anchors by `'path-edit.anchors'`). Omitting defaults to always-visible. */\n getIsVisible?: () => (id: string) => boolean;\n}\n\n/**\n * The kit's selection chrome as affordances, bottom → top: rotate ring (and\n * badge), resize handles, then anchors and their controls — a corner handle\n * beats the rotate band it sits inside, and a control handle beats everything.\n */\nexport function chromeAffordances(opts: ChromeAffordanceOptions = {}): Affordance[] {\n const sizes = targetSizesPx(opts.targetScale);\n const {\n handleHitRadius = sizes.handle,\n anchorHitRadius = sizes.anchor,\n rotateBandPx = sizes.rotationDistance,\n getAnchorState,\n rotationBadge,\n edgeHandles = true,\n } = opts;\n return [\n createRotationAffordance({\n bandPx: rotateBandPx,\n paint: null,\n ...(rotationBadge\n ? { handle: { distancePx: rotationBadge.distancePx, hitRadiusPx: rotationBadge.sizePx } }\n : {}),\n }),\n createCornerResizeAffordance({ handleHitRadius, edges: edgeHandles }),\n ...(getAnchorState\n ? createPathAnchorAffordances(getAnchorState, { hitRadius: anchorHitRadius })\n : []),\n ];\n}\n\n/**\n * Read anchor-editing state off a dep registry, for\n * {@link ChromeAffordanceOptions.getAnchorState}. Returns `null` while no\n * `editAnchors` dep is registered, so anchors are not hit-tested.\n */\nexport function anchorStateFrom(\n getRegistry: () => { get(name: 'editAnchors'): EditAnchorsDep | undefined } | null,\n /** The editable path in the world the hit-test is asked in. A node on a\n * parallax plane stores its path in the plane's world; the surface carries\n * it into the asking camera's. Default: unchanged. */\n toCamera?: (id: string, path: unknown) => unknown,\n): () => AnchorState | null {\n return () => {\n const dep = getRegistry()?.get('editAnchors');\n if (!dep) return null;\n return {\n editingId: dep.editingId ?? null,\n tool: dep.tool ?? null,\n // The hit-test reads world-coord anchor positions, so route through\n // `getEditablePath`: both pose-as-polygon and `data.path` consumers\n // resolve correctly.\n getPose: (id) => (toCamera ? toCamera(id, dep.getEditablePath(id)) : dep.getEditablePath(id)),\n getNode: (id) => dep.getNodeShape(id),\n ...(dep.extensions ? { extensions: dep.extensions } : {}),\n getOutline: (id) => {\n const path = dep.getOutlinePath?.(id) ?? dep.getEditablePath(id);\n return toCamera ? toCamera(id, path) : path;\n },\n };\n };\n}\n\n/**\n * Build the `affordanceAt` thunk for `useSceneGestureDispatcher`. The point is\n * **world-space**; callers convert from client coords first. Priority follows\n * the order of {@link chromeAffordances}, topmost first.\n */\nexport function buildAffordanceAt(\n opts: BuildAffordanceAtOptions,\n): (worldPoint: WorldPoint) => AffordanceHit | null {\n const { getChromeState, getView, getIsVisible } = opts;\n const affordances = opts.affordances ?? chromeAffordances(opts);\n\n return function affordanceAt({ x: wx, y: wy }) {\n const hit = hitAffordanceRegions(\n affordances,\n wx,\n wy,\n getChromeState(),\n getView(),\n getIsVisible?.(),\n );\n return hit ? toAffordanceHit(hit) : null;\n };\n}\n\n/**\n * Region hit → `AffordanceHit`. The scratch fields named by\n * {@link CommonAffordanceScratch} are lifted onto the hit because the actions\n * that consume them read them there; the whole scratch rides along as `payload`.\n */\nfunction toAffordanceHit(hit: AffordanceRegionHit): AffordanceHit {\n const scratch = (hit.binding.initialScratch ?? {}) as CommonAffordanceScratch;\n return {\n kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,\n owner: hit.affordanceId,\n strength: 'shared',\n ...(scratch.targetId !== undefined ? { targetIds: [scratch.targetId] } : {}),\n ...(scratch.anchor !== undefined ? { anchor: scratch.anchor } : {}),\n ...(scratch.fixedPoint !== undefined ? { fixedPoint: scratch.fixedPoint } : {}),\n ...(hit.cursor !== undefined ? { cursor: hit.cursor } : {}),\n ...(hit.binding.initialScratch !== undefined ? { payload: hit.binding.initialScratch } : {}),\n } as AffordanceHit;\n}\n\n/**\n * Classify a world-space point against the current selection: `'empty'`,\n * `'selected-body'`, or `'unselected-body'`.\n *\n * @param kindOfNode - Resolver from a hit node id to its semantic kind.\n * Omitting it leaves `kind` undefined, which makes every `kind:` TargetSpec\n * form no-match.\n * @param doubleClickNames - The node a double-click on a hit names\n * (`doubleClickTarget`). Where that is another node, it rides along as\n * `doubleClick`, classified the same way.\n */\nexport function buildClassifyTarget(\n getSelection: () => readonly string[],\n pickBest: (world: WorldPoint) => string | null,\n kindOfNode?: (id: string) => string | undefined,\n doubleClickNames?: (hitId: string) => string,\n): (worldPoint: WorldPoint) => BodyClassification {\n return function classifyTarget(world) {\n const hitId = pickBest(world);\n if (!hitId) return { body: 'empty' };\n const sel = getSelection();\n const classify = (id: string) => ({\n body: sel.includes(id) ? 'selected-body' as const : 'unselected-body' as const,\n kind: kindOfNode?.(id),\n id,\n });\n const named = doubleClickNames?.(hitId);\n return named !== undefined && named !== hitId\n ? { ...classify(hitId), doubleClick: classify(named) }\n : classify(hitId);\n };\n}\n\n/** A registered layer's claim on a point, as the affordance the dispatcher\n * routes: its kind names the layer, and it carries what the layer's hit-test\n * resolved. */\nexport function layerAffordance(layerId: string, claim: LayerHit): AffordanceHit {\n return {\n kind: `layer:${layerId}`,\n owner: layerId,\n strength: claim.strength ?? 'shared',\n ...(claim.claimedKinds !== undefined ? { claimedKinds: claim.claimedKinds } : {}),\n ...(claim.cursor !== undefined ? { cursor: claim.cursor } : {}),\n ...(claim.initialScratch !== undefined ? { payload: claim.initialScratch } : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"affordanceAt.js","names":[],"sources":["../../src/canvas/affordanceAt.ts"],"sourcesContent":["/**\n * The dispatcher-side affordance classifier: answers \"what piece of selection\n * chrome is under this world point?\" by assembling the kit's affordances and\n * running the one shared walk (`hitAffordanceRegions`).\n */\n\nimport type { BodyClassification } from '@weasel-js/gestures';\nimport type { ChromeState } from 'core/selection/chromeState';\nimport type { EditAnchorsDep } from 'interactions/actions/depSchema';\nimport type { View } from 'core/viewport/view';\nimport type { AffordanceHit, WorldPoint } from '@weasel-js/routing';\nimport type { Affordance, CommonAffordanceScratch, LayerHit } from 'affordances/types';\nimport { hitAffordanceRegions, type AffordanceRegionHit } from 'affordances/hitAffordanceRegions';\nimport { createCornerResizeAffordance } from 'affordances/cornerResize';\nimport { createRotationAffordance } from 'affordances/rotationHandle';\nimport { createPathAnchorAffordances, type AnchorState } from 'affordances/pathAnchors';\nimport { targetSizesPx } from 'core/device/targets';\n\nexport type { AnchorState };\n\n/** What decides the kit's chrome affordances — their sizes and which ones\n * exist. Shared by the hit-test and the slops overlay, which draws them. */\nexport interface ChromeAffordanceOptions {\n /** Pointer-size multiplier from the live `DeviceProfile`. Every default\n * below is resolved through {@link targetSizesPx} at this scale, so the\n * grab zone tracks the painted chrome on a coarse pointer. Default 1. */\n targetScale?: number;\n /** Corner-handle hit radius in screen px. Overrides the device-scaled default. */\n handleHitRadius?: number;\n /** Anchor / control hit radius in screen px. Overrides the device-scaled default. */\n anchorHitRadius?: number;\n /** Minimum rotate-band thickness outside the selection AABB, in screen px.\n * Overrides the device-scaled default. */\n rotateBandPx?: number;\n /** The rotate badge the selection overlay paints (`rotationBadgeOf`), so\n * it can be grabbed where it is drawn. Omitted or null, only the rotate\n * ring is hit-tested. */\n rotationBadge?: { distancePx: number; sizePx: number } | null;\n /** Resize handles at the edge midpoints as well as the corners. Default true. */\n edgeHandles?: boolean;\n /** Anchor-editing state. When omitted, anchors aren't hit-tested. */\n getAnchorState?: () => AnchorState | null;\n}\n\nexport interface BuildAffordanceAtOptions extends ChromeAffordanceOptions {\n /** Live ChromeState at call time — current selection plus effective bounds,\n * including in-flight move/resize ghost poses. */\n getChromeState: () => ChromeState;\n /** Live view. Needed because region hit radii and the rotate band are\n * declared in screen pixels and resolved against the current scale. */\n getView: () => View;\n /** The affordances to walk, when the caller built them itself to share\n * them (`chromeAffordances`). Otherwise built from these options. */\n affordances?: readonly Affordance[];\n /** Chrome-caps resolver. Keeps the hit-test and the renderer agreeing on\n * which chrome is live (resize handles gated by\n * `'selection.resize-handles'`, rotation by `'selection.rotation-handle'`,\n * anchors by `'path-edit.anchors'`). Omitting defaults to always-visible. */\n getIsVisible?: () => (id: string) => boolean;\n}\n\n/**\n * The kit's selection chrome as affordances, bottom → top: rotate ring (and\n * badge), resize handles, then anchors and their controls — a corner handle\n * beats the rotate band it sits inside, and a control handle beats everything.\n */\nexport function chromeAffordances(opts: ChromeAffordanceOptions = {}): Affordance[] {\n const sizes = targetSizesPx(opts.targetScale);\n const {\n handleHitRadius = sizes.handle,\n anchorHitRadius = sizes.anchor,\n rotateBandPx = sizes.rotationDistance,\n getAnchorState,\n rotationBadge,\n edgeHandles = true,\n } = opts;\n return [\n createRotationAffordance({\n bandPx: rotateBandPx,\n paint: null,\n ...(rotationBadge\n ? { handle: { distancePx: rotationBadge.distancePx, hitRadiusPx: rotationBadge.sizePx } }\n : {}),\n }),\n createCornerResizeAffordance({ handleHitRadius, edges: edgeHandles }),\n ...(getAnchorState\n ? createPathAnchorAffordances(getAnchorState, { hitRadius: anchorHitRadius })\n : []),\n ];\n}\n\n/**\n * Read anchor-editing state off a dep registry, for\n * {@link ChromeAffordanceOptions.getAnchorState}. Returns `null` while no\n * `editAnchors` dep is registered, so anchors are not hit-tested.\n */\nexport function anchorStateFrom(\n getRegistry: () => { get(name: 'editAnchors'): EditAnchorsDep | undefined } | null,\n /** The editable path in the world the hit-test is asked in. A node on a\n * parallax plane stores its path in the plane's world; the surface carries\n * it into the asking camera's. Default: unchanged. */\n toCamera?: (id: string, path: unknown) => unknown,\n): () => AnchorState | null {\n return () => {\n const dep = getRegistry()?.get('editAnchors');\n if (!dep) return null;\n return {\n editingId: dep.editingId ?? null,\n // The hit-test reads world-coord anchor positions, so route through\n // `getEditablePath`: both pose-as-polygon and `data.path` consumers\n // resolve correctly.\n getPose: (id) => (toCamera ? toCamera(id, dep.getEditablePath(id)) : dep.getEditablePath(id)),\n getNode: (id) => dep.getNodeShape(id),\n ...(dep.extensions ? { extensions: dep.extensions } : {}),\n getOutline: (id) => {\n const path = dep.getOutlinePath?.(id) ?? dep.getEditablePath(id);\n return toCamera ? toCamera(id, path) : path;\n },\n };\n };\n}\n\n/**\n * Build the `affordanceAt` thunk for `useSceneGestureDispatcher`. The point is\n * **world-space**; callers convert from client coords first. Priority follows\n * the order of {@link chromeAffordances}, topmost first.\n */\nexport function buildAffordanceAt(\n opts: BuildAffordanceAtOptions,\n): (worldPoint: WorldPoint) => AffordanceHit | null {\n const { getChromeState, getView, getIsVisible } = opts;\n const affordances = opts.affordances ?? chromeAffordances(opts);\n\n return function affordanceAt({ x: wx, y: wy }) {\n const hit = hitAffordanceRegions(\n affordances,\n wx,\n wy,\n getChromeState(),\n getView(),\n getIsVisible?.(),\n );\n return hit ? toAffordanceHit(hit) : null;\n };\n}\n\n/**\n * Region hit → `AffordanceHit`. The scratch fields named by\n * {@link CommonAffordanceScratch} are lifted onto the hit because the actions\n * that consume them read them there; the whole scratch rides along as `payload`.\n */\nfunction toAffordanceHit(hit: AffordanceRegionHit): AffordanceHit {\n const scratch = (hit.binding.initialScratch ?? {}) as CommonAffordanceScratch;\n return {\n kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,\n owner: hit.affordanceId,\n strength: 'shared',\n ...(scratch.targetId !== undefined ? { targetIds: [scratch.targetId] } : {}),\n ...(scratch.anchor !== undefined ? { anchor: scratch.anchor } : {}),\n ...(scratch.fixedPoint !== undefined ? { fixedPoint: scratch.fixedPoint } : {}),\n ...(hit.cursor !== undefined ? { cursor: hit.cursor } : {}),\n ...(hit.binding.initialScratch !== undefined ? { payload: hit.binding.initialScratch } : {}),\n } as AffordanceHit;\n}\n\n/**\n * Classify a world-space point against the current selection: `'empty'`,\n * `'selected-body'`, or `'unselected-body'`.\n *\n * @param kindOfNode - Resolver from a hit node id to its semantic kind.\n * Omitting it leaves `kind` undefined, which makes every `kind:` TargetSpec\n * form no-match.\n * @param doubleClickNames - The node a double-click on a hit names\n * (`doubleClickTarget`). Where that is another node, it rides along as\n * `doubleClick`, classified the same way.\n */\nexport function buildClassifyTarget(\n getSelection: () => readonly string[],\n pickBest: (world: WorldPoint) => string | null,\n kindOfNode?: (id: string) => string | undefined,\n doubleClickNames?: (hitId: string) => string,\n): (worldPoint: WorldPoint) => BodyClassification {\n return function classifyTarget(world) {\n const hitId = pickBest(world);\n if (!hitId) return { body: 'empty' };\n const sel = getSelection();\n const classify = (id: string) => ({\n body: sel.includes(id) ? 'selected-body' as const : 'unselected-body' as const,\n kind: kindOfNode?.(id),\n id,\n });\n const named = doubleClickNames?.(hitId);\n return named !== undefined && named !== hitId\n ? { ...classify(hitId), doubleClick: classify(named) }\n : classify(hitId);\n };\n}\n\n/** A registered layer's claim on a point, as the affordance the dispatcher\n * routes: its kind names the layer, and it carries what the layer's hit-test\n * resolved. */\nexport function layerAffordance(layerId: string, claim: LayerHit): AffordanceHit {\n return {\n kind: `layer:${layerId}`,\n owner: layerId,\n strength: claim.strength ?? 'shared',\n ...(claim.claimedKinds !== undefined ? { claimedKinds: claim.claimedKinds } : {}),\n ...(claim.cursor !== undefined ? { cursor: claim.cursor } : {}),\n ...(claim.initialScratch !== undefined ? { payload: claim.initialScratch } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useLatest } from "@weasel-js/react";
|
|
3
|
+
|
|
4
|
+
//#region src/canvas/deps/chromeRules.ts
|
|
5
|
+
/**
|
|
6
|
+
* The `chromeRules` dep: the canvas's chrome-visibility rules, read and set
|
|
7
|
+
* at run time, for an action that shows or hides a piece of chrome.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The rules set through the dep over `rules`, and the dep that reads and sets
|
|
11
|
+
* them. Its identity is stable.
|
|
12
|
+
*/
|
|
13
|
+
function useChromeRulesDep(rules) {
|
|
14
|
+
const [overrides, setOverrides] = useState({});
|
|
15
|
+
const merged = useMemo(() => ({
|
|
16
|
+
...rules,
|
|
17
|
+
...overrides
|
|
18
|
+
}), [rules, overrides]);
|
|
19
|
+
const mergedRef = useLatest(merged);
|
|
20
|
+
const set = useCallback((id, rule) => {
|
|
21
|
+
setOverrides((prev) => prev[id] === rule ? prev : {
|
|
22
|
+
...prev,
|
|
23
|
+
[id]: rule
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
return {
|
|
27
|
+
rules: merged,
|
|
28
|
+
dep: useMemo(() => ({
|
|
29
|
+
get: (id) => mergedRef.current[id],
|
|
30
|
+
set
|
|
31
|
+
}), [mergedRef, set])
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { useChromeRulesDep };
|
|
37
|
+
//# sourceMappingURL=chromeRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chromeRules.js","names":[],"sources":["../../../src/canvas/deps/chromeRules.ts"],"sourcesContent":["/**\n * The `chromeRules` dep: the canvas's chrome-visibility rules, read and set\n * at run time, for an action that shows or hides a piece of chrome.\n */\nimport { useCallback, useMemo, useState } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { VisibilityRules } from 'domains/chrome-caps';\n\n/** One chrome id's rule, as `VisibilityRules` holds it. */\nexport type VisibilityRule = NonNullable<VisibilityRules[string]>;\n\n/** Read and set the rule in force for a chrome id. */\nexport interface ChromeRulesDep {\n /** The rule in force for `id`, or undefined where none is set, which shows it. */\n get(id: string): VisibilityRule | undefined;\n /** Put `rule` in force for `id`, over the presets' rule and `chromeVisibility`'s. */\n set(id: string, rule: VisibilityRule): void;\n}\n\ndeclare module '@weasel-js/routing' {\n interface DepSchema {\n /** The canvas's chrome-visibility rules. Sourced by `<SceneCanvas>`, whose\n * `set` lays a rule over its presets' and its `chromeVisibility` prop's\n * until it unmounts; an app keeping these as preferences sources its own. */\n chromeRules?: ChromeRulesDep;\n }\n}\n\n/**\n * The rules set through the dep over `rules`, and the dep that reads and sets\n * them. Its identity is stable.\n */\nexport function useChromeRulesDep(rules: VisibilityRules): { rules: VisibilityRules; dep: ChromeRulesDep } {\n const [overrides, setOverrides] = useState<VisibilityRules>({});\n const merged = useMemo(() => ({ ...rules, ...overrides }), [rules, overrides]);\n const mergedRef = useLatest(merged);\n const set = useCallback((id: string, rule: VisibilityRule) => {\n setOverrides((prev) => (prev[id] === rule ? prev : { ...prev, [id]: rule }));\n }, []);\n const dep = useMemo<ChromeRulesDep>(() => ({ get: (id) => mergedRef.current[id], set }), [mergedRef, set]);\n return { rules: merged, dep };\n}\n"],"mappings":";;;;;;;;;;;;AAgCA,SAAgB,kBAAkB,OAAyE;CACzG,MAAM,CAAC,WAAW,gBAAgB,SAA0B,CAAC,CAAC;CAC9D,MAAM,SAAS,eAAe;EAAE,GAAG;EAAO,GAAG;CAAU,IAAI,CAAC,OAAO,SAAS,CAAC;CAC7E,MAAM,YAAY,UAAU,MAAM;CAClC,MAAM,MAAM,aAAa,IAAY,SAAyB;EAC5D,cAAc,SAAU,KAAK,QAAQ,OAAO,OAAO;GAAE,GAAG;IAAO,KAAK;EAAK,CAAE;CAC7E,GAAG,CAAC,CAAC;CAEL,OAAO;EAAE,OAAO;EAAQ,KADZ,eAA+B;GAAE,MAAM,OAAO,UAAU,QAAQ;GAAK;EAAI,IAAI,CAAC,WAAW,GAAG,CAChF;CAAI;AAC9B"}
|
|
@@ -47,7 +47,6 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, tune)
|
|
|
47
47
|
const effectiveId = state.getEditingId();
|
|
48
48
|
const selectedAnchors = effectiveId ? state.getSelectedAnchors() : EMPTY_ANCHOR_SELECTION;
|
|
49
49
|
const marquee = effectiveId ? state.getMarquee() : null;
|
|
50
|
-
const tool = effectiveId ? state.getTool() : null;
|
|
51
50
|
return {
|
|
52
51
|
editingId: effectiveId,
|
|
53
52
|
setEditingId(id) {
|
|
@@ -84,11 +83,6 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, tune)
|
|
|
84
83
|
state.setHandleLocks(next);
|
|
85
84
|
registry.changed?.();
|
|
86
85
|
},
|
|
87
|
-
tool,
|
|
88
|
-
setTool(id) {
|
|
89
|
-
if (!effectiveId) return;
|
|
90
|
-
state.setTool(id);
|
|
91
|
-
},
|
|
92
86
|
...pathAccess(sc, ad, state.getExtensions())
|
|
93
87
|
};
|
|
94
88
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editAnchors.js","names":[],"sources":["../../../src/canvas/deps/editAnchors.ts"],"sourcesContent":["/**\n * `useEditAnchorsDepSource` — wires the `editAnchors` dep.\n *\n * A node's editable polygon lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * `getEditablePath` returns it in **world coords** either way, and `applyEdit` commits in the\n * correct storage shape as one undoable batch, preserving the render invariant\n * `pathInWorld(stored, pose) === world`. What the node keeps per anchor — a `data.skin`, vertex\n * colors — is renumbered in that batch when the caller says where the points came from, and each path-edit extension finishes\n * the edit first (`PathEditExtension.finishEdit`). Live previews ride the standard\n * `OngoingHandle.previewIds/Pose/Data` triple — this dep doesn't own preview state.\n */\nimport { useState } from 'react';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport { useLatest } from '@weasel-js/react';\nimport type { EditAnchorsDep } from 'interactions/actions/depSchema';\nimport { locksOf, type EditAnchorsParams } from 'interactions/actions/edit-anchors/params';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type { PointOrigin, PolygonPath } from '@weasel-js/geom';\nimport { anchorDataPatch } from 'domains/paths/anchorData';\nimport { classifyPathStorage, pathEditPathOf, resolveEditablePathOf } from 'domains/paths/editablePath';\nimport { representationOf } from 'domains/paths/pathRepresentation';\nimport type { PathEditExtension } from 'domains/paths/pathEditExtension';\nimport {\n finishCtx, finishEdit, otherOps, pathEditFollower, reshapeOps, type EditableNode,\n} from './pathEditCommits';\nimport { useTransformFollower } from './transformFollowers';\nimport { createPathEditState, isSelectedNode, type PathEditState } from 'domains/paths/pathEditState';\nimport { resolveParams, type ActionTunings } from '@weasel-js/routing';\nimport type { Op } from 'core/ops/types';\n\ninterface OpsApplier {\n applyOps(ops: Op[], label?: string): void;\n}\n\n/** The part of a `PathEditState` the dep reads and writes. */\nexport type EditAnchorsStateRef = Omit<PathEditState, 'subscribe' | 'getEditingIds' | 'setExtensions'>;\n/** The same, with the change notice chrome reading the dep waits on. */\nexport type EditAnchorsStateSource = EditAnchorsStateRef & Partial<Pick<PathEditState, 'subscribe'>>;\n\nexport { resolveEditablePathOf };\n\nfunction tunedHandleLock(tune: ActionTunings | undefined): EditAnchorsParams['handleLock'] {\n const params = resolveParams(tune?.editAnchors?.params as Parameters<typeof resolveParams>[0]);\n return (params as EditAnchorsParams | undefined)?.handleLock;\n}\n\nconst EMPTY_ANCHOR_SELECTION: ReadonlySet<number> = new Set<number>();\n\n/**\n * Publish the `editAnchors` dep for `scene`: its path access and commits\n * through `adapter`, and the edit state of `externalState` (or one of its\n * own), with the extensions that state holds finishing every edit.\n */\nexport function useEditAnchorsDepSource(\n scene: Scene<unknown, string, unknown>,\n selection: SelectionApi,\n adapter: OpsApplier,\n externalState?: EditAnchorsStateRef,\n /** `<SceneCanvas tune>`: the handle locks start from its `editAnchors`\n * `handleLock` param until a toggle sets them. */\n tune?: ActionTunings,\n): void {\n const registry = useDepRegistry();\n const tuneRef = useLatest(tune);\n const sceneRef = useLatest(scene);\n const selectionRef = useLatest(selection);\n const adapterRef = useLatest(adapter);\n\n // A caller that shares the state with nothing else gets one of its own.\n const [ownState] = useState(() => createPathEditState({\n isSelectedNode: (id) => isSelectedNode(sceneRef.current, selectionRef.current, id),\n }));\n const stateRef = useLatest(externalState ?? ownState);\n\n useDepSource('editAnchors', (): EditAnchorsDep => {\n const sc = sceneRef.current;\n const ad = adapterRef.current;\n const state = stateRef.current;\n const effectiveId = state.getEditingId();\n // Anchor selection and the marquee belong to one specific edited node, so never\n // hand them out alongside an empty id.\n const selectedAnchors = effectiveId ? state.getSelectedAnchors() : EMPTY_ANCHOR_SELECTION;\n const marquee = effectiveId ? state.getMarquee() : null;\n const tool = effectiveId ? state.getTool() : null;\n return {\n editingId: effectiveId,\n setEditingId(id: string | null) {\n const next = id ?? '';\n if (effectiveId !== next) {\n registry.get('dispatchTrace')?.recordModeSwitch(\n 'editAnchors.editingId',\n effectiveId || null,\n next || null,\n next ? 'enter' : 'exit',\n );\n // Anchor indices only mean something relative to one path.\n state.setSelectedAnchors(EMPTY_ANCHOR_SELECTION);\n state.setSelectedMark(null);\n state.setMarquee(null);\n }\n stateRef.current.setEditingId(id);\n },\n selectedAnchors,\n setSelectedAnchors(next: Iterable<number>) {\n if (!effectiveId) return;\n const set = new Set(next);\n if (set.size > 0) state.setSelectedMark(null);\n state.setSelectedAnchors(set);\n },\n selectedMark: effectiveId ? state.getSelectedMark() : null,\n setSelectedMark(mark) {\n if (!effectiveId) return;\n if (mark !== null) state.setSelectedAnchors(EMPTY_ANCHOR_SELECTION);\n state.setSelectedMark(mark);\n },\n extensions: state.getExtensions(),\n marquee,\n setMarquee(rect) {\n if (!effectiveId) return;\n state.setMarquee(rect);\n },\n handleLocks: state.getHandleLocks() ?? locksOf(tunedHandleLock(tuneRef.current)),\n setHandleLocks(next) {\n state.setHandleLocks(next);\n registry.changed?.();\n },\n tool,\n setTool(id) {\n if (!effectiveId) return;\n state.setTool(id);\n },\n ...pathAccess(sc, ad, state.getExtensions()),\n };\n });\n useTransformFollower(() => pathEditFollower(sceneRef.current, stateRef.current.getExtensions()));\n}\n\n/** The members of the dep that read the scene and commit to it. */\nfunction pathAccess(\n sc: Scene<unknown, string, unknown>,\n ad: OpsApplier,\n extensions: readonly PathEditExtension[],\n): Pick<\n EditAnchorsDep,\n | 'getEditablePath' | 'getOutlinePath' | 'getStorageKind' | 'getNodeShape' | 'applyEdit' | 'editOps' | 'finishEdit'\n | 'store'\n> {\n const editOps = (id: string, worldPath: unknown, label: string, origins?: readonly (PointOrigin | null)[]): Op[] => {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n if (!node) return [];\n const done = finishEdit(extensions, node, worldPath as PolygonPath, origins, finishCtx(sc, id, extensions));\n if (!done) return [];\n // A form's origins number the anchors of its edit path, not data.path's.\n const before = origins && !representationOf(extensions, node.data) ? resolveEditablePathOf(node) : null;\n const patch = { ...(before && origins ? anchorDataPatch(node.data, before, done.path, origins) : {}), ...done.data };\n return [\n ...reshapeOps(id, node, done.path, label, Object.keys(patch).length > 0 ? patch : null, extensions),\n ...otherOps(sc, done.others, label, extensions),\n ];\n };\n return {\n getEditablePath(id: string): PolygonPath | null {\n const node = sc.get(id as NodeId);\n return pathEditPathOf(node as { pose: unknown; data: unknown }, extensions);\n },\n getOutlinePath(id: string): PolygonPath | null {\n return resolveEditablePathOf(sc.get(id as NodeId) as { pose: unknown; data: unknown });\n },\n getStorageKind(id: string): 'pose' | 'data' | null {\n const node = sc.get(id as NodeId);\n const storage = classifyPathStorage(node as { pose: unknown; data: unknown });\n return storage?.kind ?? null;\n },\n getNodeShape(id: string): { pose: unknown; data: unknown } | null {\n const node = sc.get(id as NodeId);\n if (!node) return null;\n return { pose: node.pose, data: node.data };\n },\n applyEdit(id: string, worldPath: unknown, label: string, origins?: readonly (PointOrigin | null)[]) {\n const ops = editOps(id, worldPath, label, origins);\n if (ops.length > 0) ad.applyOps(ops, label);\n },\n editOps,\n finishEdit(id: string, worldPath: unknown, origins?: readonly (PointOrigin | null)[]) {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n if (!node) return { path: worldPath, others: [] };\n const done = finishEdit(extensions, node, worldPath as PolygonPath, origins, finishCtx(sc, id, extensions));\n if (!done) return null;\n const others = [...done.others.values()].flatMap((o) => (o.path && !o.remove ? [{ id: o.id, path: o.path }] : []));\n return { path: done.path, others };\n },\n store(id, worldPath, label, data) {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n const ops = node ? reshapeOps(id, node, worldPath, label, data ?? null, extensions) : [];\n if (ops.length > 0) ad.applyOps(ops, label);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"editAnchors.js","names":[],"sources":["../../../src/canvas/deps/editAnchors.ts"],"sourcesContent":["/**\n * `useEditAnchorsDepSource` — wires the `editAnchors` dep.\n *\n * A node's editable polygon lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * `getEditablePath` returns it in **world coords** either way, and `applyEdit` commits in the\n * correct storage shape as one undoable batch, preserving the render invariant\n * `pathInWorld(stored, pose) === world`. What the node keeps per anchor — a `data.skin`, vertex\n * colors — is renumbered in that batch when the caller says where the points came from, and each path-edit extension finishes\n * the edit first (`PathEditExtension.finishEdit`). Live previews ride the standard\n * `OngoingHandle.previewIds/Pose/Data` triple — this dep doesn't own preview state.\n */\nimport { useState } from 'react';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport { useLatest } from '@weasel-js/react';\nimport type { EditAnchorsDep } from 'interactions/actions/depSchema';\nimport { locksOf, type EditAnchorsParams } from 'interactions/actions/edit-anchors/params';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type { PointOrigin, PolygonPath } from '@weasel-js/geom';\nimport { anchorDataPatch } from 'domains/paths/anchorData';\nimport { classifyPathStorage, pathEditPathOf, resolveEditablePathOf } from 'domains/paths/editablePath';\nimport { representationOf } from 'domains/paths/pathRepresentation';\nimport type { PathEditExtension } from 'domains/paths/pathEditExtension';\nimport {\n finishCtx, finishEdit, otherOps, pathEditFollower, reshapeOps, type EditableNode,\n} from './pathEditCommits';\nimport { useTransformFollower } from './transformFollowers';\nimport { createPathEditState, isSelectedNode, type PathEditState } from 'domains/paths/pathEditState';\nimport { resolveParams, type ActionTunings } from '@weasel-js/routing';\nimport type { Op } from 'core/ops/types';\n\ninterface OpsApplier {\n applyOps(ops: Op[], label?: string): void;\n}\n\n/** The part of a `PathEditState` the dep reads and writes. */\nexport type EditAnchorsStateRef = Omit<PathEditState, 'subscribe' | 'getEditingIds' | 'setExtensions'>;\n/** The same, with the change notice chrome reading the dep waits on. */\nexport type EditAnchorsStateSource = EditAnchorsStateRef & Partial<Pick<PathEditState, 'subscribe'>>;\n\nexport { resolveEditablePathOf };\n\nfunction tunedHandleLock(tune: ActionTunings | undefined): EditAnchorsParams['handleLock'] {\n const params = resolveParams(tune?.editAnchors?.params as Parameters<typeof resolveParams>[0]);\n return (params as EditAnchorsParams | undefined)?.handleLock;\n}\n\nconst EMPTY_ANCHOR_SELECTION: ReadonlySet<number> = new Set<number>();\n\n/**\n * Publish the `editAnchors` dep for `scene`: its path access and commits\n * through `adapter`, and the edit state of `externalState` (or one of its\n * own), with the extensions that state holds finishing every edit.\n */\nexport function useEditAnchorsDepSource(\n scene: Scene<unknown, string, unknown>,\n selection: SelectionApi,\n adapter: OpsApplier,\n externalState?: EditAnchorsStateRef,\n /** `<SceneCanvas tune>`: the handle locks start from its `editAnchors`\n * `handleLock` param until a toggle sets them. */\n tune?: ActionTunings,\n): void {\n const registry = useDepRegistry();\n const tuneRef = useLatest(tune);\n const sceneRef = useLatest(scene);\n const selectionRef = useLatest(selection);\n const adapterRef = useLatest(adapter);\n\n // A caller that shares the state with nothing else gets one of its own.\n const [ownState] = useState(() => createPathEditState({\n isSelectedNode: (id) => isSelectedNode(sceneRef.current, selectionRef.current, id),\n }));\n const stateRef = useLatest(externalState ?? ownState);\n\n useDepSource('editAnchors', (): EditAnchorsDep => {\n const sc = sceneRef.current;\n const ad = adapterRef.current;\n const state = stateRef.current;\n const effectiveId = state.getEditingId();\n // Anchor selection and the marquee belong to one specific edited node, so never\n // hand them out alongside an empty id.\n const selectedAnchors = effectiveId ? state.getSelectedAnchors() : EMPTY_ANCHOR_SELECTION;\n const marquee = effectiveId ? state.getMarquee() : null;\n return {\n editingId: effectiveId,\n setEditingId(id: string | null) {\n const next = id ?? '';\n if (effectiveId !== next) {\n registry.get('dispatchTrace')?.recordModeSwitch(\n 'editAnchors.editingId',\n effectiveId || null,\n next || null,\n next ? 'enter' : 'exit',\n );\n // Anchor indices only mean something relative to one path.\n state.setSelectedAnchors(EMPTY_ANCHOR_SELECTION);\n state.setSelectedMark(null);\n state.setMarquee(null);\n }\n stateRef.current.setEditingId(id);\n },\n selectedAnchors,\n setSelectedAnchors(next: Iterable<number>) {\n if (!effectiveId) return;\n const set = new Set(next);\n if (set.size > 0) state.setSelectedMark(null);\n state.setSelectedAnchors(set);\n },\n selectedMark: effectiveId ? state.getSelectedMark() : null,\n setSelectedMark(mark) {\n if (!effectiveId) return;\n if (mark !== null) state.setSelectedAnchors(EMPTY_ANCHOR_SELECTION);\n state.setSelectedMark(mark);\n },\n extensions: state.getExtensions(),\n marquee,\n setMarquee(rect) {\n if (!effectiveId) return;\n state.setMarquee(rect);\n },\n handleLocks: state.getHandleLocks() ?? locksOf(tunedHandleLock(tuneRef.current)),\n setHandleLocks(next) {\n state.setHandleLocks(next);\n registry.changed?.();\n },\n ...pathAccess(sc, ad, state.getExtensions()),\n };\n });\n useTransformFollower(() => pathEditFollower(sceneRef.current, stateRef.current.getExtensions()));\n}\n\n/** The members of the dep that read the scene and commit to it. */\nfunction pathAccess(\n sc: Scene<unknown, string, unknown>,\n ad: OpsApplier,\n extensions: readonly PathEditExtension[],\n): Pick<\n EditAnchorsDep,\n | 'getEditablePath' | 'getOutlinePath' | 'getStorageKind' | 'getNodeShape' | 'applyEdit' | 'editOps' | 'finishEdit'\n | 'store'\n> {\n const editOps = (id: string, worldPath: unknown, label: string, origins?: readonly (PointOrigin | null)[]): Op[] => {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n if (!node) return [];\n const done = finishEdit(extensions, node, worldPath as PolygonPath, origins, finishCtx(sc, id, extensions));\n if (!done) return [];\n // A form's origins number the anchors of its edit path, not data.path's.\n const before = origins && !representationOf(extensions, node.data) ? resolveEditablePathOf(node) : null;\n const patch = { ...(before && origins ? anchorDataPatch(node.data, before, done.path, origins) : {}), ...done.data };\n return [\n ...reshapeOps(id, node, done.path, label, Object.keys(patch).length > 0 ? patch : null, extensions),\n ...otherOps(sc, done.others, label, extensions),\n ];\n };\n return {\n getEditablePath(id: string): PolygonPath | null {\n const node = sc.get(id as NodeId);\n return pathEditPathOf(node as { pose: unknown; data: unknown }, extensions);\n },\n getOutlinePath(id: string): PolygonPath | null {\n return resolveEditablePathOf(sc.get(id as NodeId) as { pose: unknown; data: unknown });\n },\n getStorageKind(id: string): 'pose' | 'data' | null {\n const node = sc.get(id as NodeId);\n const storage = classifyPathStorage(node as { pose: unknown; data: unknown });\n return storage?.kind ?? null;\n },\n getNodeShape(id: string): { pose: unknown; data: unknown } | null {\n const node = sc.get(id as NodeId);\n if (!node) return null;\n return { pose: node.pose, data: node.data };\n },\n applyEdit(id: string, worldPath: unknown, label: string, origins?: readonly (PointOrigin | null)[]) {\n const ops = editOps(id, worldPath, label, origins);\n if (ops.length > 0) ad.applyOps(ops, label);\n },\n editOps,\n finishEdit(id: string, worldPath: unknown, origins?: readonly (PointOrigin | null)[]) {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n if (!node) return { path: worldPath, others: [] };\n const done = finishEdit(extensions, node, worldPath as PolygonPath, origins, finishCtx(sc, id, extensions));\n if (!done) return null;\n const others = [...done.others.values()].flatMap((o) => (o.path && !o.remove ? [{ id: o.id, path: o.path }] : []));\n return { path: done.path, others };\n },\n store(id, worldPath, label, data) {\n const node = sc.get(id as NodeId) as EditableNode | undefined;\n const ops = node ? reshapeOps(id, node, worldPath, label, data ?? null, extensions) : [];\n if (ops.length > 0) ad.applyOps(ops, label);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { rectToPlane, toPlane } from "../../core/viewport/parallax.js";
|
|
|
2
2
|
import { poseDescriptorForNode, visualBoundsViaDescriptor } from "../../core/geometry/poseDescriptor.js";
|
|
3
3
|
import { AUTO_POSE_DESCRIPTOR } from "../../core/geometry/autoPoseDescriptor.js";
|
|
4
4
|
import { findShapeInk, findShapeSilhouette } from "../NodeShape.js";
|
|
5
|
-
import { polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes } from "../../core/geometry/regionHit.js";
|
|
5
|
+
import { polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes, regionTakesByBounds } from "../../core/geometry/regionHit.js";
|
|
6
6
|
import { scenePickSource } from "../scenePickSource.js";
|
|
7
7
|
import { pickWalk } from "../pickWalk.js";
|
|
8
8
|
import { applyToPoint, boundsOfPath, pathIntersectsRect } from "@weasel-js/geom";
|
|
@@ -60,6 +60,8 @@ function regionPickQuery(r, mode, descriptor, query) {
|
|
|
60
60
|
hits: (node, pose, derived, inherited) => {
|
|
61
61
|
const g = poseDescriptorForNode(descriptor, node);
|
|
62
62
|
const b = derived ? boundsOfPath(derived) : regionBoundsOf(node, pose, g);
|
|
63
|
+
const decided = regionTakesByBounds(r, mode, b);
|
|
64
|
+
if (decided !== void 0) return decided;
|
|
63
65
|
let ink;
|
|
64
66
|
const filled = () => ink ??= findShapeInk(node, pose, { inherited })?.filled !== false;
|
|
65
67
|
return regionTakes(r, mode, asInked(filled, pose), b, () => asInked(filled, findShapeSilhouette(node, pose, { derivedPath: derived }) ?? derived ?? poseOutline(pose, b)), filled);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hitTestArea.js","names":[],"sources":["../../../src/canvas/deps/hitTestArea.ts"],"sourcesContent":["/**\n * Silhouette-aware area hit-test for marquee and lasso selection.\n *\n * The per-node decision is `regionTakes` (`core/geometry/regionHit`). This\n * module supplies the scene walk and the node's drawn outline: the painter's\n * world-frame silhouette (`findShapeSilhouette`), or with no painter the pose\n * rect, rotated if the pose is.\n */\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { applyToPoint, boundsOfPath, pathIntersectsRect, type Affine } from '@weasel-js/geom';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport {\n poseDescriptorForNode,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport {\n pickWalk,\n scenePickSource,\n type PickQuery,\n type ScenePickSourceOptions,\n} from 'canvas/pickWalk';\nimport { rectToPlane, toPlane, type PlaneMap } from 'core/viewport/parallax';\n\nimport type { HitTestView } from 'interactions/actions/depSchema';\nimport { findShapeInk, findShapeSilhouette } from 'canvas/NodeShape';\nimport {\n polygonRegion,\n poseOutline,\n rectRegion,\n regionBoundsOf,\n regionOf,\n regionTakes,\n type SelectRegion,\n} from 'core/geometry/regionHit';\n\nexport interface AABBBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** What a region query takes back beyond leaves. */\nexport interface RegionQueryOptions {\n /** Default `false`: a region that takes a group and its members\n * selects the same ink twice. A bare-adapter consumer, which has nothing\n * to fold members back into their group, asks for them. */\n includeGroups?: boolean;\n}\n\n\n/** The pick options a region dep hands the shared walk: the asking view's\n * layer gate and camera, and the surface's alpha. */\nexport function regionPickOptions(\n view: HitTestView | undefined,\n alphaOf: ((id: string) => number) | undefined,\n): ScenePickSourceOptions<unknown> {\n const camera = view?.get?.();\n return {\n ...(alphaOf ? { alphaOf } : {}),\n ...(view?.layerIsPainted ? { layerIsPainted: (layer: string) => view.layerIsPainted!(layer) } : {}),\n ...(camera ? { camera } : {}),\n };\n}\n\n/** Marquee entry point: every node the rect `bounds` meets. */\nexport function hitTestArea(\n scene: Scene<unknown, string, unknown>,\n bounds: AABBBounds,\n opts?: ScenePickSourceOptions<unknown>,\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n return walkArea(scene, rectRegion(bounds), 'intersect', opts, descriptor, query);\n}\n\n/**\n * Every node an arbitrary closed area polygon meets. `area` is interleaved\n * `[x0,y0,x1,y1,…]`; `areaBounds` is its AABB, derived when omitted.\n */\nexport function hitTestAreaPolygon(\n scene: Scene<unknown, string, unknown>,\n area: ArrayLike<number>,\n areaBounds?: AABBBounds,\n /** True when `area` IS its own bounding rect, so `areaBounds` containment\n * implies containment in the area proper. Never pass this for a lasso\n * polygon — a node inside the hull's box can miss the hull. */\n areaIsRect = false,\n /** The asking view's alpha and layer accessors. A bare scene answers for\n * itself; a view that dims or reorders layers has to supply its own. */\n opts?: ScenePickSourceOptions<unknown>,\n /** How to read this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n const region = regionOf(area, areaBounds, areaIsRect);\n return region ? walkArea(scene, region, 'intersect', opts, descriptor, query) : [];\n}\n\n/**\n * Lasso entry point: which nodes a closed lasso polygon picks under `mode`\n * (see `regionTakes`).\n */\nexport function hitTestLassoPolygon(\n scene: Scene<unknown, string, unknown>,\n polygon: ReadonlyArray<{ x: number; y: number }>,\n mode: LassoHitMode,\n opts?: ScenePickSourceOptions<unknown>,\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n const region = polygonRegion(polygon);\n return region ? walkArea(scene, region, mode, opts, descriptor, query) : [];\n}\n\n/** `shape` as a region reads it: a polygon path's open subpaths enclose\n * nothing when the node's painter lays down no fill. */\nfunction asInked<T>(filled: () => boolean, shape: T): T {\n if ((shape as { kind?: unknown } | null)?.kind !== 'polygon') return shape;\n return filled() ? shape : { ...shape, openFill: 'none' };\n}\n\n/** The walk every region query shares: skip groups unless asked, gate on\n * the clip, and let `regionTakes` decide each node. */\nfunction walkArea(\n scene: Scene<unknown, string, unknown>,\n region: SelectRegion,\n mode: LassoHitMode,\n opts: ScenePickSourceOptions<unknown> | undefined,\n descriptor: PoseDescriptor<unknown>,\n query: RegionQueryOptions,\n): NodeId[] {\n const src = scenePickSource(scene, { ...opts, index: { descriptor } });\n return pickWalk<unknown>(src, regionPickQuery(region, mode, descriptor, query)) as NodeId[];\n}\n\n/** The query a marquee or a lasso over `r` asks of each node. */\nexport function regionPickQuery(\n r: SelectRegion,\n mode: LassoHitMode,\n descriptor: PoseDescriptor<unknown>,\n query: RegionQueryOptions,\n): PickQuery<unknown> {\n return {\n includeGroups: query.includeGroups === true,\n broad: { box: r.bounds },\n clipAdmits: (clip, node, pose, derived) => {\n const g = poseDescriptorForNode(descriptor, node);\n return pathIntersectsRect(clip, derived ? boundsOfPath(derived) : visualBoundsViaDescriptor(pose, g))\n && pathIntersectsRect(clip, r.bounds);\n },\n hits: (node, pose, derived, inherited) => {\n // The descriptor sees the node here, so a pose shape whose extent\n // depends on `node.data` bounds itself rather than its pose's default.\n const g = poseDescriptorForNode(descriptor, node);\n // A derived node's pose is a placeholder: its path is the extent and the fallback outline.\n const b = derived ? boundsOfPath(derived) : regionBoundsOf(node as object, pose, g);\n let ink: boolean | undefined;\n const filled = (): boolean => ink ??= findShapeInk(node as never, pose, { inherited })?.filled !== false;\n return regionTakes(r, mode, asInked(filled, pose), b, () =>\n asInked(filled, findShapeSilhouette(node as never, pose, { derivedPath: derived }) ?? derived ?? poseOutline(pose, b)), filled);\n },\n inPlane: (m) => regionPickQuery(regionInPlane(r, m), mode, descriptor, query),\n // A copy is tested by every walk, so the query carried into one asks no index.\n inCopy: (m) => ({ ...regionPickQuery(regionInCopy(r, m), mode, descriptor, query), broad: undefined }),\n };\n}\n\n/** `r` carried into a plane's world. The map is axis-aligned, so a rect stays\n * one; a flipped axis reverses the winding, which no region test reads. */\nfunction regionInPlane(r: SelectRegion, m: PlaneMap): SelectRegion {\n const coords: number[] = [];\n for (const v of r.verts) {\n const p = toPlane(m, v);\n coords.push(p.x, p.y);\n }\n return regionOf(coords, rectToPlane(m, r.bounds), r.isRect)!;\n}\n\n/** `r` carried by an affine into a copy's source frame. A rect stays one only\n * while the map keeps the axes; a turn makes it a polygon. */\nfunction regionInCopy(r: SelectRegion, m: Affine): SelectRegion {\n const coords: number[] = [];\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n for (const v of r.verts) {\n const [x, y] = applyToPoint(m, v.x, v.y);\n coords.push(x, y);\n x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y);\n }\n const keepsAxes = (m[1] === 0 && m[2] === 0) || (m[0] === 0 && m[3] === 0);\n return regionOf(coords, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 }, r.isRect && keepsAxes)!;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hitTestArea.js","names":[],"sources":["../../../src/canvas/deps/hitTestArea.ts"],"sourcesContent":["/**\n * Silhouette-aware area hit-test for marquee and lasso selection.\n *\n * The per-node decision is `regionTakes` (`core/geometry/regionHit`). This\n * module supplies the scene walk and the node's drawn outline: the painter's\n * world-frame silhouette (`findShapeSilhouette`), or with no painter the pose\n * rect, rotated if the pose is.\n */\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { applyToPoint, boundsOfPath, pathIntersectsRect, type Affine } from '@weasel-js/geom';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport {\n poseDescriptorForNode,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport {\n pickWalk,\n scenePickSource,\n type PickQuery,\n type ScenePickSourceOptions,\n} from 'canvas/pickWalk';\nimport { rectToPlane, toPlane, type PlaneMap } from 'core/viewport/parallax';\n\nimport type { HitTestView } from 'interactions/actions/depSchema';\nimport { findShapeInk, findShapeSilhouette } from 'canvas/NodeShape';\nimport {\n polygonRegion,\n poseOutline,\n rectRegion,\n regionBoundsOf,\n regionOf,\n regionTakes,\n regionTakesByBounds,\n type SelectRegion,\n} from 'core/geometry/regionHit';\n\nexport interface AABBBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** What a region query takes back beyond leaves. */\nexport interface RegionQueryOptions {\n /** Default `false`: a region that takes a group and its members\n * selects the same ink twice. A bare-adapter consumer, which has nothing\n * to fold members back into their group, asks for them. */\n includeGroups?: boolean;\n}\n\n\n/** The pick options a region dep hands the shared walk: the asking view's\n * layer gate and camera, and the surface's alpha. */\nexport function regionPickOptions(\n view: HitTestView | undefined,\n alphaOf: ((id: string) => number) | undefined,\n): ScenePickSourceOptions<unknown> {\n const camera = view?.get?.();\n return {\n ...(alphaOf ? { alphaOf } : {}),\n ...(view?.layerIsPainted ? { layerIsPainted: (layer: string) => view.layerIsPainted!(layer) } : {}),\n ...(camera ? { camera } : {}),\n };\n}\n\n/** Marquee entry point: every node the rect `bounds` meets. */\nexport function hitTestArea(\n scene: Scene<unknown, string, unknown>,\n bounds: AABBBounds,\n opts?: ScenePickSourceOptions<unknown>,\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n return walkArea(scene, rectRegion(bounds), 'intersect', opts, descriptor, query);\n}\n\n/**\n * Every node an arbitrary closed area polygon meets. `area` is interleaved\n * `[x0,y0,x1,y1,…]`; `areaBounds` is its AABB, derived when omitted.\n */\nexport function hitTestAreaPolygon(\n scene: Scene<unknown, string, unknown>,\n area: ArrayLike<number>,\n areaBounds?: AABBBounds,\n /** True when `area` IS its own bounding rect, so `areaBounds` containment\n * implies containment in the area proper. Never pass this for a lasso\n * polygon — a node inside the hull's box can miss the hull. */\n areaIsRect = false,\n /** The asking view's alpha and layer accessors. A bare scene answers for\n * itself; a view that dims or reorders layers has to supply its own. */\n opts?: ScenePickSourceOptions<unknown>,\n /** How to read this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n const region = regionOf(area, areaBounds, areaIsRect);\n return region ? walkArea(scene, region, 'intersect', opts, descriptor, query) : [];\n}\n\n/**\n * Lasso entry point: which nodes a closed lasso polygon picks under `mode`\n * (see `regionTakes`).\n */\nexport function hitTestLassoPolygon(\n scene: Scene<unknown, string, unknown>,\n polygon: ReadonlyArray<{ x: number; y: number }>,\n mode: LassoHitMode,\n opts?: ScenePickSourceOptions<unknown>,\n descriptor: PoseDescriptor<unknown> = AUTO_POSE_DESCRIPTOR,\n query: RegionQueryOptions = {},\n): NodeId[] {\n const region = polygonRegion(polygon);\n return region ? walkArea(scene, region, mode, opts, descriptor, query) : [];\n}\n\n/** `shape` as a region reads it: a polygon path's open subpaths enclose\n * nothing when the node's painter lays down no fill. */\nfunction asInked<T>(filled: () => boolean, shape: T): T {\n if ((shape as { kind?: unknown } | null)?.kind !== 'polygon') return shape;\n return filled() ? shape : { ...shape, openFill: 'none' };\n}\n\n/** The walk every region query shares: skip groups unless asked, gate on\n * the clip, and let `regionTakes` decide each node. */\nfunction walkArea(\n scene: Scene<unknown, string, unknown>,\n region: SelectRegion,\n mode: LassoHitMode,\n opts: ScenePickSourceOptions<unknown> | undefined,\n descriptor: PoseDescriptor<unknown>,\n query: RegionQueryOptions,\n): NodeId[] {\n const src = scenePickSource(scene, { ...opts, index: { descriptor } });\n return pickWalk<unknown>(src, regionPickQuery(region, mode, descriptor, query)) as NodeId[];\n}\n\n/** The query a marquee or a lasso over `r` asks of each node. */\nexport function regionPickQuery(\n r: SelectRegion,\n mode: LassoHitMode,\n descriptor: PoseDescriptor<unknown>,\n query: RegionQueryOptions,\n): PickQuery<unknown> {\n return {\n includeGroups: query.includeGroups === true,\n broad: { box: r.bounds },\n clipAdmits: (clip, node, pose, derived) => {\n const g = poseDescriptorForNode(descriptor, node);\n return pathIntersectsRect(clip, derived ? boundsOfPath(derived) : visualBoundsViaDescriptor(pose, g))\n && pathIntersectsRect(clip, r.bounds);\n },\n hits: (node, pose, derived, inherited) => {\n // The descriptor sees the node here, so a pose shape whose extent\n // depends on `node.data` bounds itself rather than its pose's default.\n const g = poseDescriptorForNode(descriptor, node);\n // A derived node's pose is a placeholder: its path is the extent and the fallback outline.\n const b = derived ? boundsOfPath(derived) : regionBoundsOf(node as object, pose, g);\n const decided = regionTakesByBounds(r, mode, b);\n if (decided !== undefined) return decided;\n let ink: boolean | undefined;\n const filled = (): boolean => ink ??= findShapeInk(node as never, pose, { inherited })?.filled !== false;\n return regionTakes(r, mode, asInked(filled, pose), b, () =>\n asInked(filled, findShapeSilhouette(node as never, pose, { derivedPath: derived }) ?? derived ?? poseOutline(pose, b)), filled);\n },\n inPlane: (m) => regionPickQuery(regionInPlane(r, m), mode, descriptor, query),\n // A copy is tested by every walk, so the query carried into one asks no index.\n inCopy: (m) => ({ ...regionPickQuery(regionInCopy(r, m), mode, descriptor, query), broad: undefined }),\n };\n}\n\n/** `r` carried into a plane's world. The map is axis-aligned, so a rect stays\n * one; a flipped axis reverses the winding, which no region test reads. */\nfunction regionInPlane(r: SelectRegion, m: PlaneMap): SelectRegion {\n const coords: number[] = [];\n for (const v of r.verts) {\n const p = toPlane(m, v);\n coords.push(p.x, p.y);\n }\n return regionOf(coords, rectToPlane(m, r.bounds), r.isRect)!;\n}\n\n/** `r` carried by an affine into a copy's source frame. A rect stays one only\n * while the map keeps the axes; a turn makes it a polygon. */\nfunction regionInCopy(r: SelectRegion, m: Affine): SelectRegion {\n const coords: number[] = [];\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n for (const v of r.verts) {\n const [x, y] = applyToPoint(m, v.x, v.y);\n coords.push(x, y);\n x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y);\n }\n const keepsAxes = (m[1] === 0 && m[2] === 0) || (m[0] === 0 && m[3] === 0);\n return regionOf(coords, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 }, r.isRect && keepsAxes)!;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useLatest } from "@weasel-js/react";
|
|
2
|
+
import { useDepSource } from "@weasel-js/routing/react";
|
|
3
|
+
import { defaultToolOf } from "@weasel-js/modes";
|
|
4
|
+
|
|
5
|
+
//#region src/canvas/deps/modeDefaultTool.ts
|
|
6
|
+
/**
|
|
7
|
+
* Wires the `modeDefaultTool` dep: the tool the canvas's active mode starts
|
|
8
|
+
* on, when the canvas mounts it. Absent with no mode registry.
|
|
9
|
+
*/
|
|
10
|
+
function useModeDefaultToolDepSource(modes, tools) {
|
|
11
|
+
const ref = useLatest({
|
|
12
|
+
modes,
|
|
13
|
+
tools
|
|
14
|
+
});
|
|
15
|
+
useDepSource("modeDefaultTool", () => {
|
|
16
|
+
const { modes: m } = ref.current;
|
|
17
|
+
return m ? () => defaultToolOf(m.current(), ref.current.tools.has) : void 0;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { useModeDefaultToolDepSource };
|
|
23
|
+
//# sourceMappingURL=modeDefaultTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modeDefaultTool.js","names":[],"sources":["../../../src/canvas/deps/modeDefaultTool.ts"],"sourcesContent":["/**\n * Wires the `modeDefaultTool` dep: the tool the canvas's active mode starts\n * on, when the canvas mounts it. Absent with no mode registry.\n */\nimport { defaultToolOf, type ModeRegistry } from '@weasel-js/modes';\nimport { useLatest } from '@weasel-js/react';\nimport { useDepSource } from '@weasel-js/routing/react';\nimport type { ToolsApi } from 'tools/overlayBinding';\n\nexport function useModeDefaultToolDepSource(modes: ModeRegistry | undefined, tools: Pick<ToolsApi, 'has'>): void {\n const ref = useLatest({ modes, tools });\n useDepSource('modeDefaultTool', () => {\n const { modes: m } = ref.current;\n return m ? () => defaultToolOf(m.current(), ref.current.tools.has) : undefined;\n });\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,4BAA4B,OAAiC,OAAoC;CAC/G,MAAM,MAAM,UAAU;EAAE;EAAO;CAAM,CAAC;CACtC,aAAa,yBAAyB;EACpC,MAAM,EAAE,OAAO,MAAM,IAAI;EACzB,OAAO,UAAU,cAAc,EAAE,QAAQ,GAAG,IAAI,QAAQ,MAAM,GAAG,IAAI;CACvE,CAAC;AACH"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { sceneDependentsOf } from "../../core/scene/dependents.js";
|
|
2
2
|
import { createTransformOp } from "../../core/ops/transform.js";
|
|
3
3
|
import { createSetDataOp } from "../../core/ops/setData.js";
|
|
4
|
+
import { withoutShapeGenerator } from "../../domains/paths/shapeGenerator.js";
|
|
4
5
|
import { createDeleteOp } from "../../core/ops/delete.js";
|
|
5
6
|
import { pathEditPathOf, storeEditablePath } from "../../domains/paths/editablePath.js";
|
|
6
|
-
import { withoutShapeGenerator } from "../../domains/paths/shapeGenerator.js";
|
|
7
7
|
import { withoutGeometryBoundData } from "../../domains/paths/pathEditExtension.js";
|
|
8
8
|
import { writtenBy } from "../../core/ops/writtenBy.js";
|
|
9
9
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useDepRegistry, useDepSource } from "@weasel-js/routing/react";
|
|
2
|
+
|
|
3
|
+
//#region src/canvas/deps/selectionSnapshot.ts
|
|
4
|
+
var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
5
|
+
function useSelectionSnapshotDepSource() {
|
|
6
|
+
const registry = useDepRegistry();
|
|
7
|
+
useDepSource("selectionSnapshot", () => ({ take() {
|
|
8
|
+
const ids = registry.get("selection")?.get() ?? null;
|
|
9
|
+
const edit = registry.get("editAnchors");
|
|
10
|
+
const editing = edit?.editingId ?? "";
|
|
11
|
+
const anchors = edit ? [...edit.selectedAnchors] : [];
|
|
12
|
+
return () => {
|
|
13
|
+
const selection = registry.get("selection");
|
|
14
|
+
if (ids && selection && !sameIds(selection.get(), ids)) selection.set([...ids]);
|
|
15
|
+
const now = registry.get("editAnchors");
|
|
16
|
+
if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);
|
|
17
|
+
};
|
|
18
|
+
} }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { useSelectionSnapshotDepSource };
|
|
23
|
+
//# sourceMappingURL=selectionSnapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectionSnapshot.js","names":[],"sources":["../../../src/canvas/deps/selectionSnapshot.ts"],"sourcesContent":["/**\n * The `selectionSnapshot` dep: the node selection and, in path edit, the anchor\n * selection, captured together, so the last operation puts both back before it\n * reruns an action that changed them.\n */\nimport type { SelectionSnapshotDep } from '@weasel-js/routing';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type {} from '../../interactions/actions/depSchema';\n\nconst sameIds = (a: readonly string[], b: readonly string[]): boolean =>\n a.length === b.length && a.every((id, i) => id === b[i]);\n\nexport function useSelectionSnapshotDepSource(): void {\n const registry = useDepRegistry();\n useDepSource('selectionSnapshot', (): SelectionSnapshotDep => ({\n take() {\n const ids = registry.get('selection')?.get() ?? null;\n const edit = registry.get('editAnchors');\n const editing = edit?.editingId ?? '';\n const anchors = edit ? [...edit.selectedAnchors] : [];\n return () => {\n const selection = registry.get('selection');\n if (ids && selection && !sameIds(selection.get(), ids)) selection.set([...ids] as never);\n const now = registry.get('editAnchors');\n if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);\n };\n },\n }));\n}\n"],"mappings":";;;AASA,IAAM,WAAW,GAAsB,MACrC,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,IAAI,MAAM,OAAO,EAAE,EAAE;AAEzD,SAAgB,gCAAsC;CACpD,MAAM,WAAW,eAAe;CAChC,aAAa,4BAAkD,EAC7D,OAAO;EACL,MAAM,MAAM,SAAS,IAAI,WAAW,CAAC,EAAE,IAAI,KAAK;EAChD,MAAM,OAAO,SAAS,IAAI,aAAa;EACvC,MAAM,UAAU,MAAM,aAAa;EACnC,MAAM,UAAU,OAAO,CAAC,GAAG,KAAK,eAAe,IAAI,CAAC;EACpD,aAAa;GACX,MAAM,YAAY,SAAS,IAAI,WAAW;GAC1C,IAAI,OAAO,aAAa,CAAC,QAAQ,UAAU,IAAI,GAAG,GAAG,GAAG,UAAU,IAAI,CAAC,GAAG,GAAG,CAAU;GACvF,MAAM,MAAM,SAAS,IAAI,aAAa;GACtC,IAAI,WAAW,KAAK,cAAc,SAAS,IAAI,mBAAmB,OAAO;EAC3E;CACF,EACF,EAAE;AACJ"}
|
package/dist/canvas/deps/snap.js
CHANGED
|
@@ -53,6 +53,7 @@ function useSnapDepSource(viewRef, session, store) {
|
|
|
53
53
|
reachPx,
|
|
54
54
|
...s.poseDescriptor ? { poseDescriptor: s.poseDescriptor } : {},
|
|
55
55
|
...s.originProjection ? { originProjection: s.originProjection } : {},
|
|
56
|
+
...s.anchors ? { anchors: s.anchors } : {},
|
|
56
57
|
...debug ? { debug } : {},
|
|
57
58
|
store
|
|
58
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snap.js","names":[],"sources":["../../../src/canvas/deps/snap.ts"],"sourcesContent":["/**\n * `useSnapDepSource` — wires the `snap` dep whose `begin` every snapping\n * gesture runs: a session over the snap sources `session` names, writing its\n * marks to the outcome store this hook returns.\n */\nimport { useDepSource } from '@weasel-js/routing/react';\nimport { useKitSettings, useLatest } from '@weasel-js/react';\nimport type { DebugSink, WorldPoint } from '@weasel-js/routing';\nimport type { SnapDep } from 'interactions/actions/snapDep';\nimport { ownGeometryAt } from 'domains/paths/pathSnap/pathSnapSource';\nimport type { View } from 'core/viewport/view';\nimport type { Scene } from 'core/scene/types';\nimport type { DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport type { SnapOutcomeStore } from 'domains/snap/outcomeStore';\nimport type { SnapSourceRegistry } from 'domains/snap/registry';\nimport { assertSnapSourceIds, snapSourcesFor } from 'domains/snap/gather';\nimport { sceneSession2d } from 'domains/snap/sceneSession2d';\nimport { DEFAULT_SNAP_PX } from 'domains/snap/session2d';\nimport type { SnapReadoutDep } from 'domains/snap/readoutDep';\nimport { rotationSnapSource } from 'domains/snap/rotationSource';\nimport { pathSnapSource } from 'domains/paths/pathSnap/pathSnapSource';\nimport type { SnapSource } from 'domains/snap/types';\n\n/** What `begin` reads: the scene its probes come from and the sources it asks. */\nexport interface SnapSessionSources {\n scene: Scene<unknown, string, unknown>;\n /** The boxes the subject is drawn with, read when a session begins. */\n boxes: () => DrawnBoxes;\n /** `<SceneCanvas snap.sources>`, then the presets' `snapSources`. */\n sources: readonly SnapSource[];\n /** Sources `useSnapSource` adds, after `sources`. */\n registry?: SnapSourceRegistry;\n /** Ids of the canvas's default sources to leave out. */\n omit?: readonly string[];\n /** Overrides the kit setting `targets.snapPx`. */\n reachPx?: number;\n /** How a node's rotation is read, for angle targets. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** How a pose's origin is read, for the `origin` probe. */\n originProjection?: OriginProjection<unknown>;\n /** The canvas's debug sink, read when a session begins: each frame's\n * candidates are recorded into it. */\n debug?: () => DebugSink | null | undefined;\n}\n\n/** The sources every canvas asks after its own, unless `omit` names one:\n * quarter turns and other nodes' angles for a rotate, and other nodes'\n * anchors for a point gesture, under `pathSnapSource`'s own id, so a\n * consumer's `pathSnapSource()` takes its place. */\nconst DEFAULT_SNAP_SOURCES: readonly SnapSource[] = [\n rotationSnapSource(),\n { ...pathSnapSource({ only: ['anchor'] }), gestures: ['point'] },\n];\n\n/** `listed`, which must not repeat an id, then each default whose id is\n * neither listed nor in `omit`. */\nfunction withDefaults(listed: readonly SnapSource[], omit: readonly string[] = []): SnapSource[] {\n assertSnapSourceIds(listed);\n const taken = new Set([...listed.map((s) => s.id), ...omit]);\n return [...listed, ...DEFAULT_SNAP_SOURCES.filter((s) => !taken.has(s.id))];\n}\n\nexport function useSnapDepSource(\n viewRef: React.RefObject<View | null>,\n session: SnapSessionSources,\n /** The canvas's outcome store, which its readout layer draws. */\n store: SnapOutcomeStore,\n): SnapOutcomeStore {\n const sessionRef = useLatest(session);\n const kitSettings = useKitSettings();\n useDepSource('snapReadout', (): SnapReadoutDep => ({ store }));\n\n useDepSource('snap', (): SnapDep => ({\n begin: (req) => {\n const s = sessionRef.current;\n const view = viewRef.current;\n const omit = new Set(req.omit);\n const sources = [\n ...withDefaults([...s.sources, ...(s.registry?.list() ?? [])], s.omit).filter((src) => !omit.has(src.id)),\n ...(req.sources ?? []),\n ];\n assertSnapSourceIds(sources);\n const wantsGeometry = snapSourcesFor(sources, req.gesture).some((src) => src.roles.includes('geometry'));\n const debug = s.debug?.() ?? undefined;\n const reachPx = s.reachPx ?? kitSettings.get('targets.snapPx') ?? DEFAULT_SNAP_PX;\n return sceneSession2d({\n req,\n scene: s.scene,\n boxes: s.boxes(),\n sources,\n ...(wantsGeometry ? { geometryAt: (press: WorldPoint, ids: readonly string[]) => ownGeometryAt(s.scene, press, ids, view?.scale ?? { x: 1, y: 1 }, reachPx) } : {}),\n view: () => viewRef.current,\n reachPx,\n ...(s.poseDescriptor ? { poseDescriptor: s.poseDescriptor } : {}),\n ...(s.originProjection ? { originProjection: s.originProjection } : {}),\n ...(debug ? { debug } : {}),\n store,\n });\n },\n }));\n return store;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"snap.js","names":[],"sources":["../../../src/canvas/deps/snap.ts"],"sourcesContent":["/**\n * `useSnapDepSource` — wires the `snap` dep whose `begin` every snapping\n * gesture runs: a session over the snap sources `session` names, writing its\n * marks to the outcome store this hook returns.\n */\nimport { useDepSource } from '@weasel-js/routing/react';\nimport { useKitSettings, useLatest } from '@weasel-js/react';\nimport type { DebugSink, WorldPoint } from '@weasel-js/routing';\nimport type { SnapDep } from 'interactions/actions/snapDep';\nimport { ownGeometryAt } from 'domains/paths/pathSnap/pathSnapSource';\nimport type { View } from 'core/viewport/view';\nimport type { Scene } from 'core/scene/types';\nimport type { DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport type { SnapOutcomeStore } from 'domains/snap/outcomeStore';\nimport type { SnapSourceRegistry } from 'domains/snap/registry';\nimport { assertSnapSourceIds, snapSourcesFor } from 'domains/snap/gather';\nimport { sceneSession2d } from 'domains/snap/sceneSession2d';\nimport { DEFAULT_SNAP_PX } from 'domains/snap/session2d';\nimport type { SnapReadoutDep } from 'domains/snap/readoutDep';\nimport { rotationSnapSource } from 'domains/snap/rotationSource';\nimport { pathSnapSource } from 'domains/paths/pathSnap/pathSnapSource';\nimport type { SnapAnchors, SnapSource } from 'domains/snap/types';\n\n/** What `begin` reads: the scene its probes come from and the sources it asks. */\nexport interface SnapSessionSources {\n scene: Scene<unknown, string, unknown>;\n /** The boxes the subject is drawn with, read when a session begins. */\n boxes: () => DrawnBoxes;\n /** `<SceneCanvas snap.sources>`, then the presets' `snapSources`. */\n sources: readonly SnapSource[];\n /** Sources `useSnapSource` adds, after `sources`. */\n registry?: SnapSourceRegistry;\n /** Ids of the canvas's default sources to leave out. */\n omit?: readonly string[];\n /** Overrides the kit setting `targets.snapPx`. */\n reachPx?: number;\n /** How a node's rotation is read, for angle targets. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** How a pose's origin is read, for the `origin` probe. */\n originProjection?: OriginProjection<unknown>;\n /** Each node's snap anchors, for the `anchor` probes of a move. */\n anchors?: SnapAnchors;\n /** The canvas's debug sink, read when a session begins: each frame's\n * candidates are recorded into it. */\n debug?: () => DebugSink | null | undefined;\n}\n\n/** The sources every canvas asks after its own, unless `omit` names one:\n * quarter turns and other nodes' angles for a rotate, and other nodes'\n * anchors for a point gesture, under `pathSnapSource`'s own id, so a\n * consumer's `pathSnapSource()` takes its place. */\nconst DEFAULT_SNAP_SOURCES: readonly SnapSource[] = [\n rotationSnapSource(),\n { ...pathSnapSource({ only: ['anchor'] }), gestures: ['point'] },\n];\n\n/** `listed`, which must not repeat an id, then each default whose id is\n * neither listed nor in `omit`. */\nfunction withDefaults(listed: readonly SnapSource[], omit: readonly string[] = []): SnapSource[] {\n assertSnapSourceIds(listed);\n const taken = new Set([...listed.map((s) => s.id), ...omit]);\n return [...listed, ...DEFAULT_SNAP_SOURCES.filter((s) => !taken.has(s.id))];\n}\n\nexport function useSnapDepSource(\n viewRef: React.RefObject<View | null>,\n session: SnapSessionSources,\n /** The canvas's outcome store, which its readout layer draws. */\n store: SnapOutcomeStore,\n): SnapOutcomeStore {\n const sessionRef = useLatest(session);\n const kitSettings = useKitSettings();\n useDepSource('snapReadout', (): SnapReadoutDep => ({ store }));\n\n useDepSource('snap', (): SnapDep => ({\n begin: (req) => {\n const s = sessionRef.current;\n const view = viewRef.current;\n const omit = new Set(req.omit);\n const sources = [\n ...withDefaults([...s.sources, ...(s.registry?.list() ?? [])], s.omit).filter((src) => !omit.has(src.id)),\n ...(req.sources ?? []),\n ];\n assertSnapSourceIds(sources);\n const wantsGeometry = snapSourcesFor(sources, req.gesture).some((src) => src.roles.includes('geometry'));\n const debug = s.debug?.() ?? undefined;\n const reachPx = s.reachPx ?? kitSettings.get('targets.snapPx') ?? DEFAULT_SNAP_PX;\n return sceneSession2d({\n req,\n scene: s.scene,\n boxes: s.boxes(),\n sources,\n ...(wantsGeometry ? { geometryAt: (press: WorldPoint, ids: readonly string[]) => ownGeometryAt(s.scene, press, ids, view?.scale ?? { x: 1, y: 1 }, reachPx) } : {}),\n view: () => viewRef.current,\n reachPx,\n ...(s.poseDescriptor ? { poseDescriptor: s.poseDescriptor } : {}),\n ...(s.originProjection ? { originProjection: s.originProjection } : {}),\n ...(s.anchors ? { anchors: s.anchors } : {}),\n ...(debug ? { debug } : {}),\n store,\n });\n },\n }));\n return store;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqDA,IAAM,uBAA8C,CAClD,mBAAmB,GACnB;CAAE,GAAG,eAAe,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;CAAG,UAAU,CAAC,OAAO;AAAE,CACjE;;;AAIA,SAAS,aAAa,QAA+B,OAA0B,CAAC,GAAiB;CAC/F,oBAAoB,MAAM;CAC1B,MAAM,wBAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,MAAM,EAAE,EAAE,GAAG,GAAG,IAAI,CAAC;CAC3D,OAAO,CAAC,GAAG,QAAQ,GAAG,qBAAqB,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;AAC5E;AAEA,SAAgB,iBACd,SACA,SAEA,OACkB;CAClB,MAAM,aAAa,UAAU,OAAO;CACpC,MAAM,cAAc,eAAe;CACnC,aAAa,sBAAsC,EAAE,MAAM,EAAE;CAE7D,aAAa,eAAwB,EACnC,QAAQ,QAAQ;EACd,MAAM,IAAI,WAAW;EACrB,MAAM,OAAO,QAAQ;EACrB,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI;EAC7B,MAAM,UAAU,CACd,GAAG,aAAa,CAAC,GAAG,EAAE,SAAS,GAAI,EAAE,UAAU,KAAK,KAAK,CAAC,CAAE,GAAG,EAAE,IAAI,CAAC,CAAC,QAAQ,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC,GACxG,GAAI,IAAI,WAAW,CAAC,CACtB;EACA,oBAAoB,OAAO;EAC3B,MAAM,gBAAgB,eAAe,SAAS,IAAI,OAAO,CAAC,CAAC,MAAM,QAAQ,IAAI,MAAM,SAAS,UAAU,CAAC;EACvG,MAAM,QAAQ,EAAE,QAAQ,KAAK;EAC7B,MAAM,UAAU,EAAE,WAAW,YAAY,IAAI,gBAAgB,KAAK;EAClE,OAAO,eAAe;GACpB;GACA,OAAO,EAAE;GACT,OAAO,EAAE,MAAM;GACf;GACA,GAAI,gBAAgB,EAAE,aAAa,OAAmB,QAA2B,cAAc,EAAE,OAAO,OAAO,KAAK,MAAM,SAAS;IAAE,GAAG;IAAG,GAAG;GAAE,GAAG,OAAO,EAAE,IAAI,CAAC;GACjK,YAAY,QAAQ;GACpB;GACA,GAAI,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,IAAI,CAAC;GAC/D,GAAI,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,IAAI,CAAC;GACrE,GAAI,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;GAC1C,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GACzB;EACF,CAAC;CACH,EACF,EAAE;CACF,OAAO;AACT"}
|
package/dist/canvas/pickReach.js
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
//#region src/canvas/pickReach.ts
|
|
2
2
|
/**
|
|
3
3
|
* Whether `q` lands on `node` anywhere it is painted: as authored, or in any
|
|
4
|
-
* copy an ancestor draws of it. `
|
|
4
|
+
* copy an ancestor draws of it. `parentsOf` answers innermost first.
|
|
5
5
|
*
|
|
6
6
|
* A copy is the clipped content transformed, so the query is carried into the
|
|
7
7
|
* copy's source frame before the copying container's own clip is asked, and
|
|
8
8
|
* every clip below it is asked in that frame too.
|
|
9
9
|
*
|
|
10
|
-
* `
|
|
10
|
+
* `parentsOf` is called only when the answer needs it — always when
|
|
11
11
|
* `copying` says some container in the walk draws copies, otherwise only
|
|
12
12
|
* after the node hits as authored.
|
|
13
13
|
*/
|
|
14
14
|
function reaches(node, pose, derived, inherited, q, parentsOf, copying, look) {
|
|
15
|
-
const
|
|
16
|
-
const clip = look.clipOf(ancestor);
|
|
17
|
-
return clip === null || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);
|
|
18
|
-
};
|
|
19
|
-
const parents = copying && q.inCopy !== void 0 ? parentsOf() : null;
|
|
15
|
+
const parents = copying && q.inCopy !== void 0 ? parentsOf(node) : null;
|
|
20
16
|
if (parents === null || !parents.some((a) => look.copiesOf(a).length > 0)) {
|
|
21
17
|
if (!q.hits(node, pose, derived, inherited)) return false;
|
|
22
|
-
|
|
18
|
+
const all = parents ?? parentsOf(node);
|
|
19
|
+
for (let i = 0; i < all.length; i++) if (!admitsIn(look, q, all[i], node, pose, derived)) return false;
|
|
20
|
+
return true;
|
|
23
21
|
}
|
|
22
|
+
const admits = (query, ancestor) => admitsIn(look, query, ancestor, node, pose, derived);
|
|
24
23
|
const outermostFirst = [...parents].reverse();
|
|
25
24
|
const from = (i, query) => {
|
|
26
25
|
if (i === outermostFirst.length) return query.hits(node, pose, derived, inherited);
|
|
@@ -34,6 +33,11 @@ function reaches(node, pose, derived, inherited, q, parentsOf, copying, look) {
|
|
|
34
33
|
};
|
|
35
34
|
return from(0, q);
|
|
36
35
|
}
|
|
36
|
+
/** Whether `ancestor`'s clip, carried into `node`'s plane, leaves `query` anything of it. */
|
|
37
|
+
function admitsIn(look, query, ancestor, node, pose, derived) {
|
|
38
|
+
const clip = look.clipOf(ancestor);
|
|
39
|
+
return clip === null || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
//#endregion
|
|
39
43
|
export { reaches };
|