@weasel-js/core 1.15.1 → 1.16.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/dist/animation/poseTarget.js +3 -3
  3. package/dist/animation/poseTarget.js.map +1 -1
  4. package/dist/animation/reflow.js +7 -7
  5. package/dist/animation/reflow.js.map +1 -1
  6. package/dist/animation/rig/rigNode.js +2 -2
  7. package/dist/animation/rig/rigNode.js.map +1 -1
  8. package/dist/barrel/input.js +2 -1
  9. package/dist/barrel/paint.js +2 -2
  10. package/dist/barrel/viewport.js +3 -3
  11. package/dist/canvas/NodeShape.js +23 -10
  12. package/dist/canvas/NodeShape.js.map +1 -1
  13. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
  14. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
  15. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  16. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +1 -1
  17. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  18. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js +4 -1
  19. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js +3 -3
  21. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js.map +1 -1
  22. package/dist/canvas/SceneCanvas.js +1 -1
  23. package/dist/canvas/SceneCanvas.js.map +1 -1
  24. package/dist/canvas/SceneViewCanvas.js +4 -1
  25. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  26. package/dist/canvas/accentColor.js +3 -3
  27. package/dist/canvas/accentColor.js.map +1 -1
  28. package/dist/canvas/deps/enteredGroup.js +5 -4
  29. package/dist/canvas/deps/enteredGroup.js.map +1 -1
  30. package/dist/canvas/deps/selectionSnapshot.js +7 -2
  31. package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
  32. package/dist/canvas/nodeShapes/derivedPainter.js +57 -8
  33. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  34. package/dist/canvas/nodeShapes/nodePaint.js +33 -2
  35. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  36. package/dist/canvas/nodeShapes/pathPainter.js +5 -4
  37. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  38. package/dist/canvas/nodeShapes/pickOutline.js +48 -0
  39. package/dist/canvas/nodeShapes/pickOutline.js.map +1 -0
  40. package/dist/canvas/nodeShapes/shapePainter.js +6 -4
  41. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  42. package/dist/canvas/paintOverrides.js +43 -0
  43. package/dist/canvas/paintOverrides.js.map +1 -0
  44. package/dist/canvas/renderSceneToPixels.js +1 -1
  45. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  46. package/dist/canvas/sceneSlotWiring.js +3 -3
  47. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  48. package/dist/canvas/sceneViewRender.js +11 -7
  49. package/dist/canvas/sceneViewRender.js.map +1 -1
  50. package/dist/canvas/slopsDebugLayer.js +8 -2
  51. package/dist/canvas/slopsDebugLayer.js.map +1 -1
  52. package/dist/canvas/trimOverrides.js +18 -0
  53. package/dist/canvas/trimOverrides.js.map +1 -0
  54. package/dist/canvas/useViewRegistrySurface.js +6 -3
  55. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  56. package/dist/canvas/viewRegistry.js +5 -5
  57. package/dist/canvas/viewRegistry.js.map +1 -1
  58. package/dist/core/ops/reorder/index.js +2 -6
  59. package/dist/core/ops/reorder/index.js.map +1 -1
  60. package/dist/core/pages/fitViewToPage.js +2 -7
  61. package/dist/core/pages/fitViewToPage.js.map +1 -1
  62. package/dist/core/pages/pages.js +3 -9
  63. package/dist/core/pages/pages.js.map +1 -1
  64. package/dist/core/paint/meshKindLazy.js +13 -6
  65. package/dist/core/paint/meshKindLazy.js.map +1 -1
  66. package/dist/core/scene/pathEffects/blend.js +2 -2
  67. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  68. package/dist/core/scene/pathEffects/textAlong.js +6 -5
  69. package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
  70. package/dist/core/scene/scene.js +12 -1
  71. package/dist/core/scene/scene.js.map +1 -1
  72. package/dist/core/viewport/clientToWorld.js +7 -1
  73. package/dist/core/viewport/clientToWorld.js.map +1 -1
  74. package/dist/core/viewport/zoomAt.js +6 -6
  75. package/dist/core/viewport/zoomAt.js.map +1 -1
  76. package/dist/devtools/root.js +11 -4
  77. package/dist/devtools/root.js.map +1 -1
  78. package/dist/devtools/transport.js +12 -16
  79. package/dist/devtools/transport.js.map +1 -1
  80. package/dist/devtools-protocol.d.ts +0 -11
  81. package/dist/domains/measure/measureHit.js +2 -8
  82. package/dist/domains/measure/measureHit.js.map +1 -1
  83. package/dist/domains/paths/anchorMarkers.js +5 -11
  84. package/dist/domains/paths/anchorMarkers.js.map +1 -1
  85. package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
  86. package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
  87. package/dist/domains/paths/pathLayer.js +20 -6
  88. package/dist/domains/paths/pathLayer.js.map +1 -1
  89. package/dist/domains/paths/penPreviewLayer.js +9 -12
  90. package/dist/domains/paths/penPreviewLayer.js.map +1 -1
  91. package/dist/domains/paths/shapeGenerator.js +1 -2
  92. package/dist/domains/paths/shapeGenerator.js.map +1 -1
  93. package/dist/domains/snap/angle2d.js +2 -1
  94. package/dist/domains/snap/angle2d.js.map +1 -1
  95. package/dist/domains/snap/outcomeStore.js +5 -8
  96. package/dist/domains/snap/outcomeStore.js.map +1 -1
  97. package/dist/domains/text/useSceneTextEdit.js +9 -4
  98. package/dist/domains/text/useSceneTextEdit.js.map +1 -1
  99. package/dist/domains/viewports/aim.js +3 -8
  100. package/dist/domains/viewports/aim.js.map +1 -1
  101. package/dist/domains/viewports/viewResolver.js +5 -3
  102. package/dist/domains/viewports/viewResolver.js.map +1 -1
  103. package/dist/index.d.ts +66 -50
  104. package/dist/index.js +5 -4
  105. package/dist/input/usePublishPointer.js +59 -0
  106. package/dist/input/usePublishPointer.js.map +1 -0
  107. package/dist/interactions/actions/defaults/containerScroll.js +9 -3
  108. package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
  109. package/dist/interactions/actions/defaults/enterGroup.js +3 -3
  110. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
  111. package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
  112. package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
  113. package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
  114. package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
  115. package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
  116. package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
  117. package/dist/interactions/actions/move/layout/container.js +3 -6
  118. package/dist/interactions/actions/move/layout/container.js.map +1 -1
  119. package/dist/interactions/actions/move/layout/exits.js +2 -2
  120. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  121. package/dist/interactions/actions/transform-again/memory.js +2 -2
  122. package/dist/interactions/actions/transform-again/memory.js.map +1 -1
  123. package/dist/move.d.ts +18 -3
  124. package/dist/move.js +2 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +25 -25
  127. package/dist/canvas/colorOverrides.js +0 -23
  128. package/dist/canvas/colorOverrides.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 { 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 { useMagnificationCheck } from './SceneCanvas/useMagnificationCheck';\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 useMagnificationCheck(scene as Scene<unknown, string, unknown>);\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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"}
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 { useMagnificationCheck } from './SceneCanvas/useMagnificationCheck';\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 useMagnificationCheck(scene as Scene<unknown, string, unknown>);\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, paintOverrides: props.animator, 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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"}
@@ -2,6 +2,8 @@ import { useCanvasSurface } from "./useCanvasSurface.js";
2
2
  import { SurfaceCanvas } from "./SurfaceCanvas.js";
3
3
  import { useSurfaceSize } from "./useSurfaceSize.js";
4
4
  import { sceneViewFrame } from "./sceneViewRender.js";
5
+ import { paintOverrideSources } from "./paintOverrides.js";
6
+ import { subscribeToSources } from "../core/layers/redrawSource.js";
5
7
  import { normalizeView } from "../core/viewport/view.js";
6
8
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
7
9
  import { assignRef } from "@weasel-js/react";
@@ -40,7 +42,7 @@ function SceneViewCanvasInner(props) {
40
42
  alphaFor,
41
43
  layerVisibility,
42
44
  layerOrder,
43
- colorOverrides: animator?.colorOverrides,
45
+ overrides: animator,
44
46
  cull,
45
47
  paintBounds
46
48
  });
@@ -60,6 +62,7 @@ function SceneViewCanvasInner(props) {
60
62
  useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);
61
63
  useEffect(() => scene.scroll?.subscribe(requestRedraw), [scene, requestRedraw]);
62
64
  useEffect(() => animator?.onTick(requestRedraw), [animator, requestRedraw]);
65
+ useEffect(() => subscribeToSources(paintOverrideSources(animator))(requestRedraw), [animator, requestRedraw]);
63
66
  return /* @__PURE__ */ jsx(SurfaceCanvas, {
64
67
  ref: setCanvasRef,
65
68
  width: props.width,
@@ -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', 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"}
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 { paintOverrideSources } from './paintOverrides';\nimport { subscribeToSources } from '../core/layers/render';\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` and `trimOverrides`. Pass the main canvas's\n * animator so both show the same colors and trims. */\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 overrides: animator,\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 useEffect(() => subscribeToSources(paintOverrideSources(animator))(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":";;;;;;;;;;;;;;;;;;;;;AAyEA,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,WAAW;GACX;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;CAC1E,gBAAgB,mBAAmB,qBAAqB,QAAQ,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,UAAU,aAAa,CAAC;CAE5G,OACE,oBAAC,eAAD;EACE,KAAK;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACH;CACZ,CAAA;AAEL;;;;;;;;AASA,IAAa,kBAAkB"}
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from "react";
2
+ import { watchColorScheme } from "@weasel-js/react";
2
3
 
3
4
  // src/canvas/accentColor.ts
4
5
  /** The accent canvas chrome paints in when the element sits under no theme. */
@@ -24,11 +25,10 @@ function watchThemeOf(el, fn) {
24
25
  if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();
25
26
  });
26
27
  for (let node = el() ?? document.documentElement; node; node = node.parentElement) observer.observe(node, { attributes: true });
27
- const scheme = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : null;
28
- scheme?.addEventListener?.("change", fn);
28
+ const stopScheme = watchColorScheme(fn);
29
29
  return () => {
30
30
  observer.disconnect();
31
- scheme?.removeEventListener?.("change", fn);
31
+ stopScheme();
32
32
  };
33
33
  }
34
34
  /** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */
@@ -1 +1 @@
1
- {"version":3,"file":"accentColor.js","names":[],"sources":["../../src/canvas/accentColor.ts"],"sourcesContent":["import { useMemo, type RefObject } from 'react';\n\n/** The accent canvas chrome paints in when the element sits under no theme. */\nexport const FALLBACK_ACCENT = '#4c8dff';\n\n/** Read the color custom property `name` off `el`, where the theme cascade lands on a canvas. */\nexport function chromeColorOf(el: Element | null | undefined, name: `--${string}`, fallback: string): string {\n if (!el || typeof getComputedStyle !== 'function') return fallback;\n return getComputedStyle(el).getPropertyValue(name).trim() || fallback;\n}\n\n/** Read `--wzl-accent` off `el`, where the theme cascade lands on a canvas. */\nexport function accentColorOf(el: Element | null | undefined): string {\n return chromeColorOf(el, '--wzl-accent', FALLBACK_ACCENT);\n}\n\nconst THEME_ATTRIBUTE = /^(data-wzl-|class$)/;\n\n/**\n * Hear that the colors read off an element may have changed: a theme, mode or class attribute\n * changing on it or anything above it, or the system color scheme flipping. `el` is read when\n * this is called; with none yet, the document root is watched. Returns the stop.\n */\nexport function watchThemeOf(el: () => Element | null | undefined, fn: () => void): () => void {\n if (typeof MutationObserver !== 'function' || typeof document === 'undefined') return () => {};\n const observer = new MutationObserver((records) => {\n if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();\n });\n for (let node: Element | null = el() ?? document.documentElement; node; node = node.parentElement) {\n observer.observe(node, { attributes: true });\n }\n const scheme = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;\n scheme?.addEventListener?.('change', fn);\n return () => {\n observer.disconnect();\n scheme?.removeEventListener?.('change', fn);\n };\n}\n\n/** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */\nexport function useChromeColor(\n ref: RefObject<Element | null>, name: `--${string}`, fallback: string,\n): { read: () => string; changed: (fn: () => void) => () => void } {\n return useMemo(() => ({\n read: () => chromeColorOf(ref.current, name, fallback),\n changed: (fn: () => void) => watchThemeOf(() => ref.current, fn),\n }), [ref, name, fallback]);\n}\n"],"mappings":";;;;AAGA,IAAa,kBAAkB;;AAG/B,SAAgB,cAAc,IAAgC,MAAqB,UAA0B;CAC3G,IAAI,CAAC,MAAM,OAAO,qBAAqB,YAAY,OAAO;CAC1D,OAAO,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,KAAK,KAAK;AAC/D;;AAGA,SAAgB,cAAc,IAAwC;CACpE,OAAO,cAAc,IAAI,gBAAgB,eAAe;AAC1D;AAEA,IAAM,kBAAkB;;;;;;AAOxB,SAAgB,aAAa,IAAsC,IAA4B;CAC7F,IAAI,OAAO,qBAAqB,cAAc,OAAO,aAAa,aAAa,aAAa,CAAC;CAC7F,MAAM,WAAW,IAAI,kBAAkB,YAAY;EACjD,IAAI,QAAQ,MAAM,MAAM,EAAE,kBAAkB,QAAQ,gBAAgB,KAAK,EAAE,aAAa,CAAC,GAAG,GAAG;CACjG,CAAC;CACD,KAAK,IAAI,OAAuB,GAAG,KAAK,SAAS,iBAAiB,MAAM,OAAO,KAAK,eAClF,SAAS,QAAQ,MAAM,EAAE,YAAY,KAAK,CAAC;CAE7C,MAAM,SAAS,OAAO,eAAe,aAAa,WAAW,8BAA8B,IAAI;CAC/F,QAAQ,mBAAmB,UAAU,EAAE;CACvC,aAAa;EACX,SAAS,WAAW;EACpB,QAAQ,sBAAsB,UAAU,EAAE;CAC5C;AACF;;AAGA,SAAgB,eACd,KAAgC,MAAqB,UACY;CACjE,OAAO,eAAe;EACpB,YAAY,cAAc,IAAI,SAAS,MAAM,QAAQ;EACrD,UAAU,OAAmB,mBAAmB,IAAI,SAAS,EAAE;CACjE,IAAI;EAAC;EAAK;EAAM;CAAQ,CAAC;AAC3B"}
1
+ {"version":3,"file":"accentColor.js","names":[],"sources":["../../src/canvas/accentColor.ts"],"sourcesContent":["import { watchColorScheme } from '@weasel-js/react';\nimport { useMemo, type RefObject } from 'react';\n\n/** The accent canvas chrome paints in when the element sits under no theme. */\nexport const FALLBACK_ACCENT = '#4c8dff';\n\n/** Read the color custom property `name` off `el`, where the theme cascade lands on a canvas. */\nexport function chromeColorOf(el: Element | null | undefined, name: `--${string}`, fallback: string): string {\n if (!el || typeof getComputedStyle !== 'function') return fallback;\n return getComputedStyle(el).getPropertyValue(name).trim() || fallback;\n}\n\n/** Read `--wzl-accent` off `el`, where the theme cascade lands on a canvas. */\nexport function accentColorOf(el: Element | null | undefined): string {\n return chromeColorOf(el, '--wzl-accent', FALLBACK_ACCENT);\n}\n\nconst THEME_ATTRIBUTE = /^(data-wzl-|class$)/;\n\n/**\n * Hear that the colors read off an element may have changed: a theme, mode or class attribute\n * changing on it or anything above it, or the system color scheme flipping. `el` is read when\n * this is called; with none yet, the document root is watched. Returns the stop.\n */\nexport function watchThemeOf(el: () => Element | null | undefined, fn: () => void): () => void {\n if (typeof MutationObserver !== 'function' || typeof document === 'undefined') return () => {};\n const observer = new MutationObserver((records) => {\n if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();\n });\n for (let node: Element | null = el() ?? document.documentElement; node; node = node.parentElement) {\n observer.observe(node, { attributes: true });\n }\n const stopScheme = watchColorScheme(fn);\n return () => {\n observer.disconnect();\n stopScheme();\n };\n}\n\n/** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */\nexport function useChromeColor(\n ref: RefObject<Element | null>, name: `--${string}`, fallback: string,\n): { read: () => string; changed: (fn: () => void) => () => void } {\n return useMemo(() => ({\n read: () => chromeColorOf(ref.current, name, fallback),\n changed: (fn: () => void) => watchThemeOf(() => ref.current, fn),\n }), [ref, name, fallback]);\n}\n"],"mappings":";;;;;AAIA,IAAa,kBAAkB;;AAG/B,SAAgB,cAAc,IAAgC,MAAqB,UAA0B;CAC3G,IAAI,CAAC,MAAM,OAAO,qBAAqB,YAAY,OAAO;CAC1D,OAAO,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,KAAK,KAAK;AAC/D;;AAGA,SAAgB,cAAc,IAAwC;CACpE,OAAO,cAAc,IAAI,gBAAgB,eAAe;AAC1D;AAEA,IAAM,kBAAkB;;;;;;AAOxB,SAAgB,aAAa,IAAsC,IAA4B;CAC7F,IAAI,OAAO,qBAAqB,cAAc,OAAO,aAAa,aAAa,aAAa,CAAC;CAC7F,MAAM,WAAW,IAAI,kBAAkB,YAAY;EACjD,IAAI,QAAQ,MAAM,MAAM,EAAE,kBAAkB,QAAQ,gBAAgB,KAAK,EAAE,aAAa,CAAC,GAAG,GAAG;CACjG,CAAC;CACD,KAAK,IAAI,OAAuB,GAAG,KAAK,SAAS,iBAAiB,MAAM,OAAO,KAAK,eAClF,SAAS,QAAQ,MAAM,EAAE,YAAY,KAAK,CAAC;CAE7C,MAAM,aAAa,iBAAiB,EAAE;CACtC,aAAa;EACX,SAAS,WAAW;EACpB,WAAW;CACb;AACF;;AAGA,SAAgB,eACd,KAAgC,MAAqB,UACY;CACjE,OAAO,eAAe;EACpB,YAAY,cAAc,IAAI,SAAS,MAAM,QAAQ;EACrD,UAAU,OAAmB,mBAAmB,IAAI,SAAS,EAAE;CACjE,IAAI;EAAC;EAAK;EAAM;CAAQ,CAAC;AAC3B"}
@@ -1,3 +1,4 @@
1
+ import { createEmitter } from "@weasel-js/registry";
1
2
  import { useEffect, useMemo } from "react";
2
3
  import { useDepRegistry, useDepSource } from "@weasel-js/routing/react";
3
4
 
@@ -28,13 +29,13 @@ function reachableInGroup(scene, group, id) {
28
29
  function createEnteredGroup(scene) {
29
30
  let chain = [];
30
31
  let announced = null;
31
- const listeners = /* @__PURE__ */ new Set();
32
+ const listeners = createEmitter();
32
33
  const isGroup = (id) => isEnterable(scene.get(id));
33
34
  const get = () => chain.find(isGroup) ?? null;
34
35
  const groupsFrom = (id) => [id, ...scene.ancestorsOf(id)].filter(isGroup);
35
36
  const announce = () => {
36
37
  announced = get();
37
- for (const fn of [...listeners]) fn();
38
+ listeners.emit();
38
39
  };
39
40
  let stopScene = null;
40
41
  const watchScene = () => scene.subscribe(() => {
@@ -63,9 +64,9 @@ function createEnteredGroup(scene) {
63
64
  announced = get();
64
65
  stopScene = watchScene();
65
66
  }
66
- listeners.add(fn);
67
+ const off = listeners.subscribe(fn);
67
68
  return () => {
68
- listeners.delete(fn);
69
+ off();
69
70
  if (listeners.size === 0) {
70
71
  stopScene?.();
71
72
  stopScene = null;
@@ -1 +1 @@
1
- {"version":3,"file":"enteredGroup.js","names":[],"sources":["../../../src/canvas/deps/enteredGroup.ts"],"sourcesContent":["/**\n * The entered group: the group a press resolves inside of. Session state beside\n * the selection, not scene data, so it is never saved and never undone.\n *\n * It remembers the entered group and the groups above it. `get()` answers the\n * innermost of those still in the scene, so deleting the group lands on its\n * parent, and an undo that brings it back lands on it again.\n */\nimport { useEffect, useMemo } from 'react';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\n\n/** The group a press resolves inside of, held beside the selection; `<SceneCanvas enteredGroup>` takes one. */\nexport interface EnteredGroupApi {\n get(): NodeId | null;\n enter(id: NodeId): void;\n /** Moves to the nearest group above the current one, or to none. */\n leave(): void;\n /** Leaves every entered group at once. */\n leaveAll(): void;\n subscribe(fn: () => void): () => void;\n}\n\n/** Whether a node is entered as a group is: a group, or a node that consumes its children. */\nexport function isEnterable(node: { kind?: string; consumesChildren?: boolean } | undefined): boolean {\n return node?.kind === 'group' || node?.consumesChildren === true;\n}\n\n/** Whether `id` is reachable while `group` is entered: inside it, and not the group itself, and\n * not consumed by a node inside it (`SceneNode.consumesChildren`). With nothing entered every node\n * no node consumes is. Picking, region selects and select-all all ask this. */\nexport function reachableInGroup(\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf' | 'get'>,\n group: NodeId | null,\n id: string,\n): boolean {\n for (const above of scene.ancestorsOf(id as NodeId)) {\n if (above === group) return true;\n if (scene.get(above)?.consumesChildren === true) return false;\n }\n return group === null;\n}\n\ntype SceneReads = Pick<Scene<unknown, string, unknown>, 'get' | 'ancestorsOf' | 'subscribe'>;\n\n/** An entered-group store over `scene`, with nothing entered. */\nexport function createEnteredGroup(scene: SceneReads): EnteredGroupApi {\n let chain: NodeId[] = [];\n let announced: NodeId | null = null;\n const listeners = new Set<() => void>();\n\n const isGroup = (id: NodeId) => isEnterable(scene.get(id));\n const get = (): NodeId | null => chain.find(isGroup) ?? null;\n const groupsFrom = (id: NodeId): NodeId[] => [id, ...scene.ancestorsOf(id)].filter(isGroup);\n\n const announce = () => {\n announced = get();\n for (const fn of [...listeners]) fn();\n };\n\n let stopScene: (() => void) | null = null;\n const watchScene = () => scene.subscribe(() => {\n if (get() !== announced) announce();\n });\n\n return {\n get,\n enter(id) {\n if (!isGroup(id)) return;\n chain = groupsFrom(id);\n announce();\n },\n leave() {\n const here = get();\n if (here === null) return;\n chain = groupsFrom(here).slice(1);\n announce();\n },\n leaveAll() {\n if (get() === null) return;\n chain = [];\n announce();\n },\n subscribe(fn) {\n if (listeners.size === 0) {\n announced = get();\n stopScene = watchScene();\n }\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n if (listeners.size === 0) { stopScene?.(); stopScene = null; }\n };\n },\n };\n}\n\n/** One entered-group store for the life of the scene. */\nexport function useEnteredGroup(scene: SceneReads): EnteredGroupApi {\n return useMemo(() => createEnteredGroup(scene), [scene]);\n}\n\nexport function useEnteredGroupApiSource(dep: EnteredGroupApi): void {\n useDepSource('enteredGroup', () => dep);\n // What is drawn or pasted while a group is entered lands in it, unless a source\n // registered under this one, such as a symmetry group, names a container.\n useDepSource('insertInto', (below) => ({ get: () => below()?.get() ?? dep.get() }));\n const registry = useDepRegistry();\n useEffect(() => dep.subscribe(() => registry.changed?.()), [dep, registry]);\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,SAAgB,YAAY,MAA0E;CACpG,OAAO,MAAM,SAAS,WAAW,MAAM,qBAAqB;AAC9D;;;;AAKA,SAAgB,iBACd,OACA,OACA,IACS;CACT,KAAK,MAAM,SAAS,MAAM,YAAY,EAAY,GAAG;EACnD,IAAI,UAAU,OAAO,OAAO;EAC5B,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,qBAAqB,MAAM,OAAO;CAC1D;CACA,OAAO,UAAU;AACnB;;AAKA,SAAgB,mBAAmB,OAAoC;CACrE,IAAI,QAAkB,CAAC;CACvB,IAAI,YAA2B;CAC/B,MAAM,4BAAY,IAAI,IAAgB;CAEtC,MAAM,WAAW,OAAe,YAAY,MAAM,IAAI,EAAE,CAAC;CACzD,MAAM,YAA2B,MAAM,KAAK,OAAO,KAAK;CACxD,MAAM,cAAc,OAAyB,CAAC,IAAI,GAAG,MAAM,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO;CAE1F,MAAM,iBAAiB;EACrB,YAAY,IAAI;EAChB,KAAK,MAAM,MAAM,CAAC,GAAG,SAAS,GAAG,GAAG;CACtC;CAEA,IAAI,YAAiC;CACrC,MAAM,mBAAmB,MAAM,gBAAgB;EAC7C,IAAI,IAAI,MAAM,WAAW,SAAS;CACpC,CAAC;CAED,OAAO;EACL;EACA,MAAM,IAAI;GACR,IAAI,CAAC,QAAQ,EAAE,GAAG;GAClB,QAAQ,WAAW,EAAE;GACrB,SAAS;EACX;EACA,QAAQ;GACN,MAAM,OAAO,IAAI;GACjB,IAAI,SAAS,MAAM;GACnB,QAAQ,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;GAChC,SAAS;EACX;EACA,WAAW;GACT,IAAI,IAAI,MAAM,MAAM;GACpB,QAAQ,CAAC;GACT,SAAS;EACX;EACA,UAAU,IAAI;GACZ,IAAI,UAAU,SAAS,GAAG;IACxB,YAAY,IAAI;IAChB,YAAY,WAAW;GACzB;GACA,UAAU,IAAI,EAAE;GAChB,aAAa;IACX,UAAU,OAAO,EAAE;IACnB,IAAI,UAAU,SAAS,GAAG;KAAE,YAAY;KAAG,YAAY;IAAM;GAC/D;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,OAAoC;CAClE,OAAO,cAAc,mBAAmB,KAAK,GAAG,CAAC,KAAK,CAAC;AACzD;AAEA,SAAgB,yBAAyB,KAA4B;CACnE,aAAa,sBAAsB,GAAG;CAGtC,aAAa,eAAe,WAAW,EAAE,WAAW,MAAM,CAAC,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,EAAE;CAClF,MAAM,WAAW,eAAe;CAChC,gBAAgB,IAAI,gBAAgB,SAAS,UAAU,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC;AAC5E"}
1
+ {"version":3,"file":"enteredGroup.js","names":[],"sources":["../../../src/canvas/deps/enteredGroup.ts"],"sourcesContent":["/**\n * The entered group: the group a press resolves inside of. Session state beside\n * the selection, not scene data, so it is never saved and never undone.\n *\n * It remembers the entered group and the groups above it. `get()` answers the\n * innermost of those still in the scene, so deleting the group lands on its\n * parent, and an undo that brings it back lands on it again.\n */\nimport { useEffect, useMemo } from 'react';\nimport { createEmitter } from '@weasel-js/registry';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\n\n/** The group a press resolves inside of, held beside the selection; `<SceneCanvas enteredGroup>` takes one. */\nexport interface EnteredGroupApi {\n get(): NodeId | null;\n enter(id: NodeId): void;\n /** Moves to the nearest group above the current one, or to none. */\n leave(): void;\n /** Leaves every entered group at once. */\n leaveAll(): void;\n subscribe(fn: () => void): () => void;\n}\n\n/** Whether a node is entered as a group is: a group, or a node that consumes its children. */\nexport function isEnterable(node: { kind?: string; consumesChildren?: boolean } | undefined): boolean {\n return node?.kind === 'group' || node?.consumesChildren === true;\n}\n\n/** Whether `id` is reachable while `group` is entered: inside it, and not the group itself, and\n * not consumed by a node inside it (`SceneNode.consumesChildren`). With nothing entered every node\n * no node consumes is. Picking, region selects and select-all all ask this. */\nexport function reachableInGroup(\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf' | 'get'>,\n group: NodeId | null,\n id: string,\n): boolean {\n for (const above of scene.ancestorsOf(id as NodeId)) {\n if (above === group) return true;\n if (scene.get(above)?.consumesChildren === true) return false;\n }\n return group === null;\n}\n\ntype SceneReads = Pick<Scene<unknown, string, unknown>, 'get' | 'ancestorsOf' | 'subscribe'>;\n\n/** An entered-group store over `scene`, with nothing entered. */\nexport function createEnteredGroup(scene: SceneReads): EnteredGroupApi {\n let chain: NodeId[] = [];\n let announced: NodeId | null = null;\n const listeners = createEmitter();\n\n const isGroup = (id: NodeId) => isEnterable(scene.get(id));\n const get = (): NodeId | null => chain.find(isGroup) ?? null;\n const groupsFrom = (id: NodeId): NodeId[] => [id, ...scene.ancestorsOf(id)].filter(isGroup);\n\n const announce = () => {\n announced = get();\n listeners.emit();\n };\n\n let stopScene: (() => void) | null = null;\n const watchScene = () => scene.subscribe(() => {\n if (get() !== announced) announce();\n });\n\n return {\n get,\n enter(id) {\n if (!isGroup(id)) return;\n chain = groupsFrom(id);\n announce();\n },\n leave() {\n const here = get();\n if (here === null) return;\n chain = groupsFrom(here).slice(1);\n announce();\n },\n leaveAll() {\n if (get() === null) return;\n chain = [];\n announce();\n },\n subscribe(fn) {\n if (listeners.size === 0) {\n announced = get();\n stopScene = watchScene();\n }\n const off = listeners.subscribe(fn);\n return () => {\n off();\n if (listeners.size === 0) { stopScene?.(); stopScene = null; }\n };\n },\n };\n}\n\n/** One entered-group store for the life of the scene. */\nexport function useEnteredGroup(scene: SceneReads): EnteredGroupApi {\n return useMemo(() => createEnteredGroup(scene), [scene]);\n}\n\nexport function useEnteredGroupApiSource(dep: EnteredGroupApi): void {\n useDepSource('enteredGroup', () => dep);\n // What is drawn or pasted while a group is entered lands in it, unless a source\n // registered under this one, such as a symmetry group, names a container.\n useDepSource('insertInto', (below) => ({ get: () => below()?.get() ?? dep.get() }));\n const registry = useDepRegistry();\n useEffect(() => dep.subscribe(() => registry.changed?.()), [dep, registry]);\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,SAAgB,YAAY,MAA0E;CACpG,OAAO,MAAM,SAAS,WAAW,MAAM,qBAAqB;AAC9D;;;;AAKA,SAAgB,iBACd,OACA,OACA,IACS;CACT,KAAK,MAAM,SAAS,MAAM,YAAY,EAAY,GAAG;EACnD,IAAI,UAAU,OAAO,OAAO;EAC5B,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,qBAAqB,MAAM,OAAO;CAC1D;CACA,OAAO,UAAU;AACnB;;AAKA,SAAgB,mBAAmB,OAAoC;CACrE,IAAI,QAAkB,CAAC;CACvB,IAAI,YAA2B;CAC/B,MAAM,YAAY,cAAc;CAEhC,MAAM,WAAW,OAAe,YAAY,MAAM,IAAI,EAAE,CAAC;CACzD,MAAM,YAA2B,MAAM,KAAK,OAAO,KAAK;CACxD,MAAM,cAAc,OAAyB,CAAC,IAAI,GAAG,MAAM,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO;CAE1F,MAAM,iBAAiB;EACrB,YAAY,IAAI;EAChB,UAAU,KAAK;CACjB;CAEA,IAAI,YAAiC;CACrC,MAAM,mBAAmB,MAAM,gBAAgB;EAC7C,IAAI,IAAI,MAAM,WAAW,SAAS;CACpC,CAAC;CAED,OAAO;EACL;EACA,MAAM,IAAI;GACR,IAAI,CAAC,QAAQ,EAAE,GAAG;GAClB,QAAQ,WAAW,EAAE;GACrB,SAAS;EACX;EACA,QAAQ;GACN,MAAM,OAAO,IAAI;GACjB,IAAI,SAAS,MAAM;GACnB,QAAQ,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;GAChC,SAAS;EACX;EACA,WAAW;GACT,IAAI,IAAI,MAAM,MAAM;GACpB,QAAQ,CAAC;GACT,SAAS;EACX;EACA,UAAU,IAAI;GACZ,IAAI,UAAU,SAAS,GAAG;IACxB,YAAY,IAAI;IAChB,YAAY,WAAW;GACzB;GACA,MAAM,MAAM,UAAU,UAAU,EAAE;GAClC,aAAa;IACX,IAAI;IACJ,IAAI,UAAU,SAAS,GAAG;KAAE,YAAY;KAAG,YAAY;IAAM;GAC/D;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,OAAoC;CAClE,OAAO,cAAc,mBAAmB,KAAK,GAAG,CAAC,KAAK,CAAC;AACzD;AAEA,SAAgB,yBAAyB,KAA4B;CACnE,aAAa,sBAAsB,GAAG;CAGtC,aAAa,eAAe,WAAW,EAAE,WAAW,MAAM,CAAC,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,EAAE;CAClF,MAAM,WAAW,eAAe;CAChC,gBAAgB,IAAI,gBAAgB,SAAS,UAAU,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC;AAC5E"}
@@ -1,7 +1,12 @@
1
+ import { sameList } from "@weasel-js/registry";
1
2
  import { useDepRegistry, useDepSource } from "@weasel-js/routing/react";
2
3
 
3
4
  // src/canvas/deps/selectionSnapshot.ts
4
- var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
5
+ /**
6
+ * The `selectionSnapshot` dep: the node selection and, in path edit, the anchor
7
+ * selection, captured together, so the last operation puts both back before it
8
+ * reruns an action that changed them.
9
+ */
5
10
  function useSelectionSnapshotDepSource() {
6
11
  const registry = useDepRegistry();
7
12
  useDepSource("selectionSnapshot", () => ({ take() {
@@ -11,7 +16,7 @@ function useSelectionSnapshotDepSource() {
11
16
  const anchors = edit ? [...edit.selectedAnchors] : [];
12
17
  return () => {
13
18
  const selection = registry.get("selection");
14
- if (ids && selection && !sameIds(selection.get(), ids)) selection.set([...ids]);
19
+ if (ids && selection && !sameList(selection.get(), ids)) selection.set([...ids]);
15
20
  const now = registry.get("editAnchors");
16
21
  if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);
17
22
  };
@@ -1 +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"}
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 { sameList } from '@weasel-js/registry';\nimport type { SelectionSnapshotDep } from '@weasel-js/routing';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type {} from '../../interactions/actions/depSchema';\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 && !sameList(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":";;;;;;;;;AAUA,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,SAAS,UAAU,IAAI,GAAG,GAAG,GAAG,UAAU,IAAI,CAAC,GAAG,GAAG,CAAU;GACxF,MAAM,MAAM,SAAS,IAAI,aAAa;GACtC,IAAI,WAAW,KAAK,cAAc,SAAS,IAAI,mBAAmB,OAAO;EAC3E;CACF,EACF,EAAE;AACJ"}
@@ -1,12 +1,24 @@
1
1
  import { derivedPartsOf, resolvePartPaint } from "../../core/scene/derivedParts.js";
2
2
  import { derivesPath } from "./memo.js";
3
3
  import { strokedBounds } from "./strokedBounds.js";
4
- import { inkReach, withAnimatedVertexColors } from "./nodePaint.js";
4
+ import { inkCovers, trimmedOutline } from "./pickOutline.js";
5
+ import { inkReach, inkTrim, withAnimatedStrokeTrim, withAnimatedTrim, withAnimatedVertexColors } from "./nodePaint.js";
5
6
  import { commandFill, commandStroke } from "@weasel-js/renderer";
6
- import { boundsOfPath } from "@weasel-js/geom";
7
- import { resolveFill, resolveStroke } from "@weasel-js/paint";
7
+ import { boundsOfPath, pathThroughPoseFrame, poseFrameOf } from "@weasel-js/geom";
8
+ import { INHERIT, resolveFill, resolveStroke, strokeTrimKey } from "@weasel-js/paint";
8
9
 
9
10
  // src/canvas/nodeShapes/derivedPainter.ts
11
+ var FRAMED = /* @__PURE__ */ new WeakMap();
12
+ function framedPart(path, pose) {
13
+ const hit = FRAMED.get(path);
14
+ if (hit && hit.pose === pose) return hit;
15
+ const made = {
16
+ pose,
17
+ world: pathThroughPoseFrame(path, pose)
18
+ };
19
+ FRAMED.set(path, made);
20
+ return made;
21
+ }
10
22
  function pathCommand(path, declared, strokeSpec, vertexColors) {
11
23
  const box = boundsOfPath(path);
12
24
  const fill = declared && commandFill(declared, box);
@@ -49,7 +61,8 @@ function partCommand(part, d, inherited) {
49
61
  * the pose AABB's center, which for a zero-sized pose is its origin.
50
62
  *
51
63
  * A path made by `pathFromParts` paints one command per part, each in the
52
- * part's own fill and stroke.
64
+ * part's own fill and stroke; an animated trim reaches only the parts stroked
65
+ * `INHERIT`, which draw the node's own stroke.
53
66
  */
54
67
  var DERIVED_PAINTER = {
55
68
  id: "kit:derived",
@@ -59,10 +72,13 @@ var DERIVED_PAINTER = {
59
72
  if (path == null) return [];
60
73
  const d = node.data;
61
74
  const parts = derivedPartsOf(path);
62
- if (parts !== void 0) return parts.map((part) => partCommand(part, d, ctx?.inherited));
75
+ if (parts !== void 0) return parts.map((part) => {
76
+ const cmd = partCommand(part, d, ctx?.inherited);
77
+ return part.stroke === INHERIT ? withAnimatedStrokeTrim(cmd, ctx) : cmd;
78
+ });
63
79
  const strokeSpec = resolveStroke(d?.stroke, ctx?.inherited?.stroke);
64
80
  const declared = resolveFill(d?.fill, ctx?.inherited?.fill);
65
- return withAnimatedVertexColors([pathCommand(path, declared, strokeSpec, d?.vertexColors)], ctx);
81
+ return withAnimatedTrim(withAnimatedVertexColors([pathCommand(path, declared, strokeSpec, d?.vertexColors)], ctx), ctx);
66
82
  },
67
83
  silhouette: (_node, _pose, ctx) => ctx?.derivedPath ?? null,
68
84
  drawnBounds: (node, _pose, ctx) => derivedDrawnBounds(node, ctx),
@@ -71,10 +87,43 @@ var DERIVED_PAINTER = {
71
87
  const stroke = resolveStroke(d?.stroke, ctx?.inherited?.stroke);
72
88
  return {
73
89
  filled: resolveFill(d?.fill, ctx?.inherited?.fill) !== null,
74
- ...inkReach(stroke, ctx?.scale)
90
+ ...inkReach(stroke, ctx?.scale),
91
+ ...inkTrim(stroke)
75
92
  };
76
93
  }
77
94
  };
95
+ /**
96
+ * Whether a derived path in `parts` covers the world point (x, y): each part
97
+ * picked as {@link DERIVED_PAINTER} paints it, in its own fill and stroke,
98
+ * a trim cutting each part along its own length.
99
+ */
100
+ function partsCover(node, parts, pose, x, y, opts) {
101
+ const d = node.data;
102
+ const framed = poseFrameOf(pose) !== null;
103
+ return parts.some((part) => {
104
+ const { fill, stroke } = resolvePartPaint(part, d, opts.inherited);
105
+ const framing = framed ? framedPart(part.path, pose) : null;
106
+ const world = framing?.world ?? part.path;
107
+ const ink = {
108
+ filled: fill !== null,
109
+ ...inkReach(stroke, opts.scale)
110
+ };
111
+ const trim = stroke?.trim;
112
+ let trimmed;
113
+ if (trim !== void 0) {
114
+ const key = strokeTrimKey(trim);
115
+ if (framing && framing.trimKey === key && framing.trimmed) trimmed = framing.trimmed;
116
+ else {
117
+ trimmed = trimmedOutline(part.path, world, pose, trim);
118
+ if (framing) Object.assign(framing, {
119
+ trimKey: key,
120
+ trimmed
121
+ });
122
+ }
123
+ }
124
+ return inkCovers(world, ink, trimmed, x, y, opts);
125
+ });
126
+ }
78
127
 
79
- export { DERIVED_PAINTER, derivedDrawnBounds };
128
+ export { DERIVED_PAINTER, derivedDrawnBounds, partsCover };
80
129
  //# sourceMappingURL=derivedPainter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"derivedPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/derivedPainter.ts"],"sourcesContent":["import { boundsOfPath, type Path } from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeInkCtx, NodeShapeEntry } from 'canvas/NodeShape';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { derivedPartsOf, resolvePartPaint, type DerivedPart } from 'core/scene/derivedParts';\nimport { derivesPath } from './memo';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\nimport { strokedBounds } from './strokedBounds';\nimport { inkReach, withAnimatedVertexColors } from './nodePaint';\nimport { resolveFill, resolveStroke, type PaintedFill, type PaintedStroke } from '@weasel-js/paint';\n\ntype PaintData = { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null; vertexColors?: number[] };\n\nfunction pathCommand(\n path: Path,\n declared: PaintedFill | null,\n strokeSpec: PaintedStroke | null,\n vertexColors: number[] | undefined,\n): DrawCommand {\n const box = boundsOfPath(path);\n const fill = declared && commandFill(declared, box);\n const stroke = strokeSpec && commandStroke(strokeSpec, box);\n return {\n kind: 'path',\n path,\n ...(fill ? { fill } : {}),\n ...(fill && vertexColors ? { vertexColors } : {}),\n ...(stroke ? { stroke } : {}),\n };\n}\n\n/** The stroked box of the path on `ctx`, or `null` with no path or no stroke. */\nexport function derivedDrawnBounds(node: { data?: unknown }, ctx: NodeInkCtx | undefined): Bounds | null {\n const path = ctx?.derivedPath;\n const stroke = resolveStroke((node.data as PaintData | null)?.stroke, ctx?.inherited?.stroke);\n return path == null || stroke === null ? null : strokedBounds(path, stroke, ctx?.scale ?? 1, ctx?.screen);\n}\n\n/** One part's command, in its own paint; `INHERIT` takes the node's. */\nfunction partCommand(part: DerivedPart, d: PaintData | null, inherited: InheritedPaint | undefined): DrawCommand {\n const { fill, stroke } = resolvePartPaint(part, d, inherited);\n return pathCommand(part.path, fill, stroke, undefined);\n}\n\n/**\n * Built-in painter for nodes whose geometry is computed from other nodes'\n * poses. The path itself arrives on the paint context — see\n * {@link NodePaintCtx.derivedPath}.\n *\n * Fill and stroke resolve as `kit:path`'s do, but against the **derived path's\n * own bounds**, not the pose. `kit:path` projects its path into the pose frame\n * first, so there the two boxes are the same one; here the path is already\n * absolute and the pose is typically a zero-sized placeholder, which would\n * collapse a box-relative gradient onto a point.\n *\n * Not memoized on {@link PAINT_SLOT} on purpose: that key is `(node, pose,\n * data)` and cannot see `ctx.derivedPath`, so it would serve one caller's\n * commands to a caller that passed a different path.\n *\n * Rotation still comes from the pose — `wrapNodeOutput` turns the output about\n * the pose AABB's center, which for a zero-sized pose is its origin.\n *\n * A path made by `pathFromParts` paints one command per part, each in the\n * part's own fill and stroke.\n */\nexport const DERIVED_PAINTER: NodeShapeEntry = {\n id: 'kit:derived',\n // `dependsOn` alone is not enough: the two fields are independent optionals,\n // and `kit:add` restores a node with its `dependsOn` intact when its\n // `derivePath` key is missing from the registry. Claiming that node would paint\n // nothing where the authored appearance is what the warning promises.\n matches: derivesPath,\n paint: (node, _pose, ctx) => {\n const path = ctx?.derivedPath;\n if (path == null) return [];\n const d = node.data as PaintData | null;\n const parts = derivedPartsOf(path);\n if (parts !== undefined) return parts.map((part) => partCommand(part, d, ctx?.inherited));\n const strokeSpec = resolveStroke(d?.stroke, ctx?.inherited?.stroke);\n const declared = resolveFill(d?.fill, ctx?.inherited?.fill);\n return withAnimatedVertexColors([pathCommand(path, declared, strokeSpec, d?.vertexColors)], ctx);\n },\n // The derived path *is* the silhouette, and it is already absolute — the\n // pose is a placeholder, so nothing here can be recovered from it. Without\n // a `ctx` this reports null, which `shapeCoversPoint` reads as \"no\n // opinion\": picking then falls back to that zero-sized pose.\n silhouette: (_node, _pose, ctx) => ctx?.derivedPath ?? null,\n drawnBounds: (node, _pose, ctx) => derivedDrawnBounds(node, ctx),\n ink: (node, _pose, ctx) => {\n const d = node.data as PaintData | null;\n const stroke = resolveStroke(d?.stroke, ctx?.inherited?.stroke);\n return {\n filled: resolveFill(d?.fill, ctx?.inherited?.fill) !== null,\n ...inkReach(stroke, ctx?.scale),\n };\n },\n};\n"],"mappings":";;;;;;;;;AAeA,SAAS,YACP,MACA,UACA,YACA,cACa;CACb,MAAM,MAAM,aAAa,IAAI;CAC7B,MAAM,OAAO,YAAY,YAAY,UAAU,GAAG;CAClD,MAAM,SAAS,cAAc,cAAc,YAAY,GAAG;CAC1D,OAAO;EACL,MAAM;EACN;EACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EACvB,GAAI,QAAQ,eAAe,EAAE,aAAa,IAAI,CAAC;EAC/C,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC7B;AACF;;AAGA,SAAgB,mBAAmB,MAA0B,KAA4C;CACvG,MAAM,OAAO,KAAK;CAClB,MAAM,SAAS,cAAe,KAAK,MAA2B,QAAQ,KAAK,WAAW,MAAM;CAC5F,OAAO,QAAQ,QAAQ,WAAW,OAAO,OAAO,cAAc,MAAM,QAAQ,KAAK,SAAS,GAAG,KAAK,MAAM;AAC1G;;AAGA,SAAS,YAAY,MAAmB,GAAqB,WAAoD;CAC/G,MAAM,EAAE,MAAM,WAAW,iBAAiB,MAAM,GAAG,SAAS;CAC5D,OAAO,YAAY,KAAK,MAAM,MAAM,QAAQ,MAAS;AACvD;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,kBAAkC;CAC7C,IAAI;CAKJ,SAAS;CACT,QAAQ,MAAM,OAAO,QAAQ;EAC3B,MAAM,OAAO,KAAK;EAClB,IAAI,QAAQ,MAAM,OAAO,CAAC;EAC1B,MAAM,IAAI,KAAK;EACf,MAAM,QAAQ,eAAe,IAAI;EACjC,IAAI,UAAU,QAAW,OAAO,MAAM,KAAK,SAAS,YAAY,MAAM,GAAG,KAAK,SAAS,CAAC;EACxF,MAAM,aAAa,cAAc,GAAG,QAAQ,KAAK,WAAW,MAAM;EAClE,MAAM,WAAW,YAAY,GAAG,MAAM,KAAK,WAAW,IAAI;EAC1D,OAAO,yBAAyB,CAAC,YAAY,MAAM,UAAU,YAAY,GAAG,YAAY,CAAC,GAAG,GAAG;CACjG;CAKA,aAAa,OAAO,OAAO,QAAQ,KAAK,eAAe;CACvD,cAAc,MAAM,OAAO,QAAQ,mBAAmB,MAAM,GAAG;CAC/D,MAAM,MAAM,OAAO,QAAQ;EACzB,MAAM,IAAI,KAAK;EACf,MAAM,SAAS,cAAc,GAAG,QAAQ,KAAK,WAAW,MAAM;EAC9D,OAAO;GACL,QAAQ,YAAY,GAAG,MAAM,KAAK,WAAW,IAAI,MAAM;GACvD,GAAG,SAAS,QAAQ,KAAK,KAAK;EAChC;CACF;AACF"}
1
+ {"version":3,"file":"derivedPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/derivedPainter.ts"],"sourcesContent":["import { boundsOfPath, pathThroughPoseFrame, poseFrameOf, type Path } from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeInkCtx, NodeShapeEntry } from 'canvas/NodeShape';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { derivedPartsOf, resolvePartPaint, type DerivedPart } from 'core/scene/derivedParts';\nimport { derivesPath } from './memo';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\nimport { strokedBounds } from './strokedBounds';\nimport { inkCovers, trimmedOutline, type PickReach, type TrimmedOutline } from './pickOutline';\nimport { inkReach, inkTrim, withAnimatedStrokeTrim, withAnimatedTrim, withAnimatedVertexColors } from './nodePaint';\nimport {\n INHERIT, resolveFill, resolveStroke, strokeTrimKey, type PaintedFill, type PaintedStroke,\n} from '@weasel-js/paint';\n\n/** A derived part's path framed by one pose, and its trimmed outline under one trim. */\ninterface FramedPart {\n pose: unknown;\n world: Path;\n trimKey?: string;\n trimmed?: TrimmedOutline;\n}\n\n// Kept per part path, which is immutable, so the length tables and corners built on the framed\n// outline are reused across picks; a new pose object or trim replaces the entry.\nconst FRAMED = new WeakMap<Path, FramedPart>();\n\nfunction framedPart(path: Path, pose: unknown): FramedPart {\n const hit = FRAMED.get(path);\n if (hit && hit.pose === pose) return hit;\n const made: FramedPart = { pose, world: pathThroughPoseFrame(path, pose) };\n FRAMED.set(path, made);\n return made;\n}\n\n/** Test seam: the trimmed outline kept for `path`'s part, if any. */\nexport function _framedPartOutlineForTests(path: Path): TrimmedOutline | undefined {\n return FRAMED.get(path)?.trimmed;\n}\n\ntype PaintData = { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null; vertexColors?: number[] };\n\nfunction pathCommand(\n path: Path,\n declared: PaintedFill | null,\n strokeSpec: PaintedStroke | null,\n vertexColors: number[] | undefined,\n): DrawCommand {\n const box = boundsOfPath(path);\n const fill = declared && commandFill(declared, box);\n const stroke = strokeSpec && commandStroke(strokeSpec, box);\n return {\n kind: 'path',\n path,\n ...(fill ? { fill } : {}),\n ...(fill && vertexColors ? { vertexColors } : {}),\n ...(stroke ? { stroke } : {}),\n };\n}\n\n/** The stroked box of the path on `ctx`, or `null` with no path or no stroke. */\nexport function derivedDrawnBounds(node: { data?: unknown }, ctx: NodeInkCtx | undefined): Bounds | null {\n const path = ctx?.derivedPath;\n const stroke = resolveStroke((node.data as PaintData | null)?.stroke, ctx?.inherited?.stroke);\n return path == null || stroke === null ? null : strokedBounds(path, stroke, ctx?.scale ?? 1, ctx?.screen);\n}\n\n/** One part's command, in its own paint; `INHERIT` takes the node's. */\nfunction partCommand(part: DerivedPart, d: PaintData | null, inherited: InheritedPaint | undefined): DrawCommand {\n const { fill, stroke } = resolvePartPaint(part, d, inherited);\n return pathCommand(part.path, fill, stroke, undefined);\n}\n\n/**\n * Built-in painter for nodes whose geometry is computed from other nodes'\n * poses. The path itself arrives on the paint context — see\n * {@link NodePaintCtx.derivedPath}.\n *\n * Fill and stroke resolve as `kit:path`'s do, but against the **derived path's\n * own bounds**, not the pose. `kit:path` projects its path into the pose frame\n * first, so there the two boxes are the same one; here the path is already\n * absolute and the pose is typically a zero-sized placeholder, which would\n * collapse a box-relative gradient onto a point.\n *\n * Not memoized on {@link PAINT_SLOT} on purpose: that key is `(node, pose,\n * data)` and cannot see `ctx.derivedPath`, so it would serve one caller's\n * commands to a caller that passed a different path.\n *\n * Rotation still comes from the pose — `wrapNodeOutput` turns the output about\n * the pose AABB's center, which for a zero-sized pose is its origin.\n *\n * A path made by `pathFromParts` paints one command per part, each in the\n * part's own fill and stroke; an animated trim reaches only the parts stroked\n * `INHERIT`, which draw the node's own stroke.\n */\nexport const DERIVED_PAINTER: NodeShapeEntry = {\n id: 'kit:derived',\n // `dependsOn` alone is not enough: the two fields are independent optionals,\n // and `kit:add` restores a node with its `dependsOn` intact when its\n // `derivePath` key is missing from the registry. Claiming that node would paint\n // nothing where the authored appearance is what the warning promises.\n matches: derivesPath,\n paint: (node, _pose, ctx) => {\n const path = ctx?.derivedPath;\n if (path == null) return [];\n const d = node.data as PaintData | null;\n const parts = derivedPartsOf(path);\n if (parts !== undefined) {\n return parts.map((part) => {\n const cmd = partCommand(part, d, ctx?.inherited);\n return part.stroke === INHERIT ? withAnimatedStrokeTrim(cmd, ctx) : cmd;\n });\n }\n const strokeSpec = resolveStroke(d?.stroke, ctx?.inherited?.stroke);\n const declared = resolveFill(d?.fill, ctx?.inherited?.fill);\n return withAnimatedTrim(withAnimatedVertexColors([pathCommand(path, declared, strokeSpec, d?.vertexColors)], ctx), ctx);\n },\n // The derived path *is* the silhouette, and it is already absolute — the\n // pose is a placeholder, so nothing here can be recovered from it. Without\n // a `ctx` this reports null, which `shapeCoversPoint` reads as \"no\n // opinion\": picking then falls back to that zero-sized pose.\n silhouette: (_node, _pose, ctx) => ctx?.derivedPath ?? null,\n drawnBounds: (node, _pose, ctx) => derivedDrawnBounds(node, ctx),\n ink: (node, _pose, ctx) => {\n const d = node.data as PaintData | null;\n const stroke = resolveStroke(d?.stroke, ctx?.inherited?.stroke);\n return {\n filled: resolveFill(d?.fill, ctx?.inherited?.fill) !== null,\n ...inkReach(stroke, ctx?.scale),\n ...inkTrim(stroke),\n };\n },\n};\n\n/**\n * Whether a derived path in `parts` covers the world point (x, y): each part\n * picked as {@link DERIVED_PAINTER} paints it, in its own fill and stroke,\n * a trim cutting each part along its own length.\n */\nexport function partsCover(\n node: { data?: unknown }, parts: readonly DerivedPart[], pose: unknown, x: number, y: number,\n opts: PickReach & { inherited?: InheritedPaint },\n): boolean {\n const d = node.data as PaintData | null;\n const framed = poseFrameOf(pose) !== null;\n return parts.some((part) => {\n const { fill, stroke } = resolvePartPaint(part, d, opts.inherited);\n const framing = framed ? framedPart(part.path, pose) : null;\n const world = framing?.world ?? part.path;\n const ink = { filled: fill !== null, ...inkReach(stroke, opts.scale) };\n const trim = stroke?.trim;\n let trimmed: TrimmedOutline | undefined;\n if (trim !== undefined) {\n const key = strokeTrimKey(trim);\n if (framing && framing.trimKey === key && framing.trimmed) trimmed = framing.trimmed;\n else {\n trimmed = trimmedOutline(part.path, world, pose, trim);\n if (framing) Object.assign(framing, { trimKey: key, trimmed });\n }\n }\n return inkCovers(world, ink, trimmed, x, y, opts);\n });\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { bumpNodeMemoGeneration } from "../../core/scene/nodeMemo.js";
2
2
  import { paintDrawKindRegistry } from "@weasel-js/renderer";
3
3
  import { countPathAnchors } from "@weasel-js/geom";
4
- import { resolveFill, resolveStroke } from "@weasel-js/paint";
4
+ import { resolveFill, resolveStroke, strokeTrimKey } from "@weasel-js/paint";
5
5
  import { markerReach, resolveStrokeWidth, widestStrokeWidth } from "@weasel-js/stroke";
6
6
 
7
7
  // src/canvas/nodeShapes/nodePaint.ts
@@ -42,6 +42,11 @@ function inkReach(stroke, scale) {
42
42
  insetPx: px.inset
43
43
  };
44
44
  }
45
+ /** `stroke`'s trim as {@link NodeInk} carries it, for a painter whose
46
+ * silhouette is the outline the stroke is drawn along. */
47
+ function inkTrim(stroke) {
48
+ return stroke?.trim === void 0 ? {} : { trim: stroke.trim };
49
+ }
45
50
  /** How far `stroke` reaches outside its outline on every side, world units at
46
51
  * `scale`: {@link inkReach}'s outset with its screen part resolved. */
47
52
  function strokeOutset(stroke, scale) {
@@ -103,6 +108,32 @@ function withAnimatedVertexColors(cmds, ctx) {
103
108
  } } : {}
104
109
  }, ...cmds.slice(1)];
105
110
  }
111
+ /** `cmd` with {@link NodePaintCtx.trim} applied to its stroke: `cmd` itself
112
+ * when nothing overrides it or the trim comes back the same, otherwise a
113
+ * copy. A command with no stroke has nothing to trim. */
114
+ function withAnimatedStrokeTrim(cmd, ctx) {
115
+ const resolve = ctx?.trim;
116
+ if (resolve === void 0 || cmd.kind !== "path" || !cmd.stroke) return cmd;
117
+ const base = cmd.stroke.trim;
118
+ const trim = resolve(base);
119
+ if (trim === base || strokeTrimKey(trim) === strokeTrimKey(base)) return cmd;
120
+ const { trim: _old, ...rest } = cmd.stroke;
121
+ return {
122
+ ...cmd,
123
+ stroke: trim === void 0 ? rest : {
124
+ ...rest,
125
+ trim
126
+ }
127
+ };
128
+ }
129
+ /** Apply {@link NodePaintCtx.trim} to a path painter's body, its first
130
+ * command, outside the paint memo as {@link withAnimatedVertexColors} is. */
131
+ function withAnimatedTrim(cmds, ctx) {
132
+ const body = cmds[0];
133
+ if (ctx?.trim === void 0 || body === void 0) return cmds;
134
+ const next = withAnimatedStrokeTrim(body, ctx);
135
+ return next === body ? cmds : [next, ...cmds.slice(1)];
136
+ }
106
137
  /** {@link PathPaint} of a node that keeps its paint in `data.fill` and
107
138
  * `data.stroke`, as the built-in path painter draws it: a stroke or a fill
108
139
  * with no `paint` is none, and an `'inherit'` one is `inherited`'s. */
@@ -115,5 +146,5 @@ function pathPaintOf(data, inherited) {
115
146
  } : { fill };
116
147
  }
117
148
 
118
- export { inkReach, pathPaintOf, strokeOutset, strokeOutsetPerAxis, withAnimatedVertexColors };
149
+ export { inkReach, inkTrim, pathPaintOf, strokeOutset, strokeOutsetPerAxis, withAnimatedStrokeTrim, withAnimatedTrim, withAnimatedVertexColors };
119
150
  //# sourceMappingURL=nodePaint.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nodePaint.js","names":[],"sources":["../../../src/canvas/nodeShapes/nodePaint.ts"],"sourcesContent":["import type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport { resolveFill, resolveStroke } from '@weasel-js/paint';\nimport type { NodeInk, NodeInkCtx, NodePaintCtx } from 'canvas/NodeShape';\nimport { countPathAnchors } from '@weasel-js/geom';\nimport { markerReach } from '@weasel-js/stroke';\nimport { resolveStrokeWidth, widestStrokeWidth } from '@weasel-js/stroke';\nimport type { DrawCommand, PathDrawCommand } from '@weasel-js/renderer';\nimport { paintDrawKindRegistry } from '@weasel-js/renderer';\nimport { bumpNodeMemoGeneration } from 'core/scene/nodeMemo';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n// The painters memoize a node's paint and resolve a fill's frame inside the\n// memo, so the kind set is ambient state that memo cannot see change.\npaintDrawKindRegistry.subscribe(bumpNodeMemoGeneration);\n\n/** Per-side grab reach for a resolved stroke: world units for what the\n * renderer builds in world, screen pixels for what it builds on screen — a\n * `{ px }` width, and each head by its own size's units. */\nexport function inkReach(\n stroke: Stroke | null,\n scale: number | undefined,\n): Pick<NodeInk, 'outset' | 'inset' | 'outsetPx' | 'insetPx'> {\n if (stroke === null) return { outset: 0, inset: 0 };\n // A marker paints past the path's own end, and the kit's rule is that\n // visible chrome is hittable — so the reach has to cover it.\n const heads = markerReach(stroke);\n const sides = (width: number, headReach: number) => {\n switch (stroke.align ?? 'center') {\n case 'inner': return { outset: headReach, inset: width };\n case 'outer': return { outset: width + headReach, inset: 0 };\n default: return { outset: width / 2 + headReach, inset: width / 2 };\n }\n };\n const width = stroke.width ?? 1;\n // Per-vertex widths are world units even on a `{ px }` stroke.\n const worldWidth = widestStrokeWidth(typeof width === 'object' ? 0 : resolveStrokeWidth(width, scale ?? 1), stroke.vertexWidths);\n const world = sides(worldWidth, heads.world);\n if (typeof width !== 'object' && !(heads.px > 0)) return world;\n const px = sides(typeof width === 'object' ? width.px : 0, heads.px);\n return { outset: world.outset, inset: world.inset, outsetPx: px.outset, insetPx: px.inset };\n}\n\n/** How far `stroke` reaches outside its outline on every side, world units at\n * `scale`: {@link inkReach}'s outset with its screen part resolved. */\nexport function strokeOutset(stroke: Stroke | null, scale: number): number {\n const r = inkReach(stroke, scale);\n return r.outset + (r.outsetPx ?? 0) / (scale > 0 ? scale : 1);\n}\n\n/** {@link strokeOutset} per axis of the frame `ctx.screen` maps from: `x` past\n * the left and right sides, `y` past the top and bottom. A `{ px }` reach\n * is a disk on screen, and its extent along each axis of that frame is the\n * support of the disk pulled back through the map. */\nexport function strokeOutsetPerAxis(stroke: Stroke | null, ctx: NodeInkCtx | undefined): { x: number; y: number } {\n const scale = ctx?.scale ?? 1;\n const screen = ctx?.screen;\n if (!screen) {\n const r = strokeOutset(stroke, scale);\n return { x: r, y: r };\n }\n const r = inkReach(stroke, scale);\n const px = r.outsetPx ?? 0;\n if (px === 0) return { x: r.outset, y: r.outset };\n const [a, b, c, d] = screen;\n const det = Math.abs(a * d - b * c);\n if (!(det > 0)) return { x: r.outset, y: r.outset };\n return { x: r.outset + (px * Math.hypot(c, d)) / det, y: r.outset + (px * Math.hypot(a, b)) / det };\n}\n\n/** Apply {@link NodePaintCtx.vertexColors} to a path painter's body — its first\n * command. Applied outside the paint memo, whose key\n * cannot see an override: `cmds` comes back as-is when nothing changes, and\n * otherwise as a copy, since a memoized list is not ours to edit. Colors whose\n * length does not match the path's anchors are dropped. */\nexport function withAnimatedVertexColors(cmds: DrawCommand[], ctx: NodePaintCtx | undefined): DrawCommand[] {\n const resolve = ctx?.vertexColors;\n const body = cmds[0];\n if (resolve === undefined || body?.kind !== 'path') return cmds;\n const expected = 4 * countPathAnchors(body.path);\n const fill = body.fill ? resolve('fill', body.vertexColors) : undefined;\n const stroke = body.stroke ? resolve('stroke', body.stroke.vertexColors) : undefined;\n const fillChanged = fill?.length === expected && fill !== body.vertexColors;\n const strokeChanged = stroke?.length === expected && stroke !== body.stroke?.vertexColors;\n if (!fillChanged && !strokeChanged) return cmds;\n const next: PathDrawCommand = {\n ...body,\n ...(fillChanged ? { vertexColors: fill as number[] } : {}),\n ...(strokeChanged ? { stroke: { ...body.stroke!, vertexColors: stroke as number[] } } : {}),\n };\n return [next, ...cmds.slice(1)];\n}\n\n/** The paint a path node wears, resolved: what an action that turns ink into\n * geometry reads from a node and tells a consumer to put on a result. */\nexport interface PathPaint {\n /** `null` is no fill. */\n fill: Fill | null;\n /** Absent for no stroke. */\n stroke?: Stroke;\n}\n\n/** {@link PathPaint} of a node that keeps its paint in `data.fill` and\n * `data.stroke`, as the built-in path painter draws it: a stroke or a fill\n * with no `paint` is none, and an `'inherit'` one is `inherited`'s. */\nexport function pathPaintOf(\n data: { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null },\n inherited?: InheritedPaint,\n): PathPaint {\n const stroke = resolveStroke(data.stroke, inherited?.stroke);\n const fill = resolveFill(data.fill, inherited?.fill);\n return stroke !== null ? { fill, stroke } : { fill };\n}\n"],"mappings":";;;;;;;AAaA,sBAAsB,UAAU,sBAAsB;;;;AAKtD,SAAgB,SACd,QACA,OAC4D;CAC5D,IAAI,WAAW,MAAM,OAAO;EAAE,QAAQ;EAAG,OAAO;CAAE;CAGlD,MAAM,QAAQ,YAAY,MAAM;CAChC,MAAM,SAAS,OAAe,cAAsB;EAClD,QAAQ,OAAO,SAAS,UAAxB;GACE,KAAK,SAAS,OAAO;IAAE,QAAQ;IAAW,OAAO;GAAM;GACvD,KAAK,SAAS,OAAO;IAAE,QAAQ,QAAQ;IAAW,OAAO;GAAE;GAC3D,SAAS,OAAO;IAAE,QAAQ,QAAQ,IAAI;IAAW,OAAO,QAAQ;GAAE;EACpE;CACF;CACA,MAAM,QAAQ,OAAO,SAAS;CAG9B,MAAM,QAAQ,MADK,kBAAkB,OAAO,UAAU,WAAW,IAAI,mBAAmB,OAAO,SAAS,CAAC,GAAG,OAAO,YAC/F,GAAY,MAAM,KAAK;CAC3C,IAAI,OAAO,UAAU,YAAY,EAAE,MAAM,KAAK,IAAI,OAAO;CACzD,MAAM,KAAK,MAAM,OAAO,UAAU,WAAW,MAAM,KAAK,GAAG,MAAM,EAAE;CACnE,OAAO;EAAE,QAAQ,MAAM;EAAQ,OAAO,MAAM;EAAO,UAAU,GAAG;EAAQ,SAAS,GAAG;CAAM;AAC5F;;;AAIA,SAAgB,aAAa,QAAuB,OAAuB;CACzE,MAAM,IAAI,SAAS,QAAQ,KAAK;CAChC,OAAO,EAAE,UAAU,EAAE,YAAY,MAAM,QAAQ,IAAI,QAAQ;AAC7D;;;;;AAMA,SAAgB,oBAAoB,QAAuB,KAAuD;CAChH,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,SAAS,KAAK;CACpB,IAAI,CAAC,QAAQ;EACX,MAAM,IAAI,aAAa,QAAQ,KAAK;EACpC,OAAO;GAAE,GAAG;GAAG,GAAG;EAAE;CACtB;CACA,MAAM,IAAI,SAAS,QAAQ,KAAK;CAChC,MAAM,KAAK,EAAE,YAAY;CACzB,IAAI,OAAO,GAAG,OAAO;EAAE,GAAG,EAAE;EAAQ,GAAG,EAAE;CAAO;CAChD,MAAM,CAAC,GAAG,GAAG,GAAG,KAAK;CACrB,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI,IAAI,CAAC;CAClC,IAAI,EAAE,MAAM,IAAI,OAAO;EAAE,GAAG,EAAE;EAAQ,GAAG,EAAE;CAAO;CAClD,OAAO;EAAE,GAAG,EAAE,SAAU,KAAK,KAAK,MAAM,GAAG,CAAC,IAAK;EAAK,GAAG,EAAE,SAAU,KAAK,KAAK,MAAM,GAAG,CAAC,IAAK;CAAI;AACpG;;;;;;AAOA,SAAgB,yBAAyB,MAAqB,KAA8C;CAC1G,MAAM,UAAU,KAAK;CACrB,MAAM,OAAO,KAAK;CAClB,IAAI,YAAY,UAAa,MAAM,SAAS,QAAQ,OAAO;CAC3D,MAAM,WAAW,IAAI,iBAAiB,KAAK,IAAI;CAC/C,MAAM,OAAO,KAAK,OAAO,QAAQ,QAAQ,KAAK,YAAY,IAAI;CAC9D,MAAM,SAAS,KAAK,SAAS,QAAQ,UAAU,KAAK,OAAO,YAAY,IAAI;CAC3E,MAAM,cAAc,MAAM,WAAW,YAAY,SAAS,KAAK;CAC/D,MAAM,gBAAgB,QAAQ,WAAW,YAAY,WAAW,KAAK,QAAQ;CAC7E,IAAI,CAAC,eAAe,CAAC,eAAe,OAAO;CAM3C,OAAO,CAAC;EAJN,GAAG;EACH,GAAI,cAAc,EAAE,cAAc,KAAiB,IAAI,CAAC;EACxD,GAAI,gBAAgB,EAAE,QAAQ;GAAE,GAAG,KAAK;GAAS,cAAc;EAAmB,EAAE,IAAI,CAAC;CAEnF,GAAM,GAAG,KAAK,MAAM,CAAC,CAAC;AAChC;;;;AAcA,SAAgB,YACd,MACA,WACW;CACX,MAAM,SAAS,cAAc,KAAK,QAAQ,WAAW,MAAM;CAC3D,MAAM,OAAO,YAAY,KAAK,MAAM,WAAW,IAAI;CACnD,OAAO,WAAW,OAAO;EAAE;EAAM;CAAO,IAAI,EAAE,KAAK;AACrD"}
1
+ {"version":3,"file":"nodePaint.js","names":[],"sources":["../../../src/canvas/nodeShapes/nodePaint.ts"],"sourcesContent":["import type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport { resolveFill, resolveStroke, strokeTrimKey } from '@weasel-js/paint';\nimport type { NodeInk, NodeInkCtx, NodePaintCtx } from 'canvas/NodeShape';\nimport { countPathAnchors } from '@weasel-js/geom';\nimport { markerReach } from '@weasel-js/stroke';\nimport { resolveStrokeWidth, widestStrokeWidth } from '@weasel-js/stroke';\nimport type { DrawCommand, PathDrawCommand } from '@weasel-js/renderer';\nimport { paintDrawKindRegistry } from '@weasel-js/renderer';\nimport { bumpNodeMemoGeneration } from 'core/scene/nodeMemo';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n// The painters memoize a node's paint and resolve a fill's frame inside the\n// memo, so the kind set is ambient state that memo cannot see change.\npaintDrawKindRegistry.subscribe(bumpNodeMemoGeneration);\n\n/** Per-side grab reach for a resolved stroke: world units for what the\n * renderer builds in world, screen pixels for what it builds on screen — a\n * `{ px }` width, and each head by its own size's units. */\nexport function inkReach(\n stroke: Stroke | null,\n scale: number | undefined,\n): Pick<NodeInk, 'outset' | 'inset' | 'outsetPx' | 'insetPx'> {\n if (stroke === null) return { outset: 0, inset: 0 };\n // A marker paints past the path's own end, and the kit's rule is that\n // visible chrome is hittable — so the reach has to cover it.\n const heads = markerReach(stroke);\n const sides = (width: number, headReach: number) => {\n switch (stroke.align ?? 'center') {\n case 'inner': return { outset: headReach, inset: width };\n case 'outer': return { outset: width + headReach, inset: 0 };\n default: return { outset: width / 2 + headReach, inset: width / 2 };\n }\n };\n const width = stroke.width ?? 1;\n // Per-vertex widths are world units even on a `{ px }` stroke.\n const worldWidth = widestStrokeWidth(typeof width === 'object' ? 0 : resolveStrokeWidth(width, scale ?? 1), stroke.vertexWidths);\n const world = sides(worldWidth, heads.world);\n if (typeof width !== 'object' && !(heads.px > 0)) return world;\n const px = sides(typeof width === 'object' ? width.px : 0, heads.px);\n return { outset: world.outset, inset: world.inset, outsetPx: px.outset, insetPx: px.inset };\n}\n\n/** `stroke`'s trim as {@link NodeInk} carries it, for a painter whose\n * silhouette is the outline the stroke is drawn along. */\nexport function inkTrim(stroke: Stroke | null): Pick<NodeInk, 'trim'> {\n return stroke?.trim === undefined ? {} : { trim: stroke.trim };\n}\n\n/** How far `stroke` reaches outside its outline on every side, world units at\n * `scale`: {@link inkReach}'s outset with its screen part resolved. */\nexport function strokeOutset(stroke: Stroke | null, scale: number): number {\n const r = inkReach(stroke, scale);\n return r.outset + (r.outsetPx ?? 0) / (scale > 0 ? scale : 1);\n}\n\n/** {@link strokeOutset} per axis of the frame `ctx.screen` maps from: `x` past\n * the left and right sides, `y` past the top and bottom. A `{ px }` reach\n * is a disk on screen, and its extent along each axis of that frame is the\n * support of the disk pulled back through the map. */\nexport function strokeOutsetPerAxis(stroke: Stroke | null, ctx: NodeInkCtx | undefined): { x: number; y: number } {\n const scale = ctx?.scale ?? 1;\n const screen = ctx?.screen;\n if (!screen) {\n const r = strokeOutset(stroke, scale);\n return { x: r, y: r };\n }\n const r = inkReach(stroke, scale);\n const px = r.outsetPx ?? 0;\n if (px === 0) return { x: r.outset, y: r.outset };\n const [a, b, c, d] = screen;\n const det = Math.abs(a * d - b * c);\n if (!(det > 0)) return { x: r.outset, y: r.outset };\n return { x: r.outset + (px * Math.hypot(c, d)) / det, y: r.outset + (px * Math.hypot(a, b)) / det };\n}\n\n/** Apply {@link NodePaintCtx.vertexColors} to a path painter's body — its first\n * command. Applied outside the paint memo, whose key\n * cannot see an override: `cmds` comes back as-is when nothing changes, and\n * otherwise as a copy, since a memoized list is not ours to edit. Colors whose\n * length does not match the path's anchors are dropped. */\nexport function withAnimatedVertexColors(cmds: DrawCommand[], ctx: NodePaintCtx | undefined): DrawCommand[] {\n const resolve = ctx?.vertexColors;\n const body = cmds[0];\n if (resolve === undefined || body?.kind !== 'path') return cmds;\n const expected = 4 * countPathAnchors(body.path);\n const fill = body.fill ? resolve('fill', body.vertexColors) : undefined;\n const stroke = body.stroke ? resolve('stroke', body.stroke.vertexColors) : undefined;\n const fillChanged = fill?.length === expected && fill !== body.vertexColors;\n const strokeChanged = stroke?.length === expected && stroke !== body.stroke?.vertexColors;\n if (!fillChanged && !strokeChanged) return cmds;\n const next: PathDrawCommand = {\n ...body,\n ...(fillChanged ? { vertexColors: fill as number[] } : {}),\n ...(strokeChanged ? { stroke: { ...body.stroke!, vertexColors: stroke as number[] } } : {}),\n };\n return [next, ...cmds.slice(1)];\n}\n\n/** `cmd` with {@link NodePaintCtx.trim} applied to its stroke: `cmd` itself\n * when nothing overrides it or the trim comes back the same, otherwise a\n * copy. A command with no stroke has nothing to trim. */\nexport function withAnimatedStrokeTrim(cmd: DrawCommand, ctx: NodePaintCtx | undefined): DrawCommand {\n const resolve = ctx?.trim;\n if (resolve === undefined || cmd.kind !== 'path' || !cmd.stroke) return cmd;\n const base = cmd.stroke.trim;\n const trim = resolve(base);\n if (trim === base || strokeTrimKey(trim) === strokeTrimKey(base)) return cmd;\n const { trim: _old, ...rest } = cmd.stroke;\n return { ...cmd, stroke: trim === undefined ? rest : { ...rest, trim } };\n}\n\n/** Apply {@link NodePaintCtx.trim} to a path painter's body, its first\n * command, outside the paint memo as {@link withAnimatedVertexColors} is. */\nexport function withAnimatedTrim(cmds: DrawCommand[], ctx: NodePaintCtx | undefined): DrawCommand[] {\n const body = cmds[0];\n if (ctx?.trim === undefined || body === undefined) return cmds;\n const next = withAnimatedStrokeTrim(body, ctx);\n return next === body ? cmds : [next, ...cmds.slice(1)];\n}\n\n/** The paint a path node wears, resolved: what an action that turns ink into\n * geometry reads from a node and tells a consumer to put on a result. */\nexport interface PathPaint {\n /** `null` is no fill. */\n fill: Fill | null;\n /** Absent for no stroke. */\n stroke?: Stroke;\n}\n\n/** {@link PathPaint} of a node that keeps its paint in `data.fill` and\n * `data.stroke`, as the built-in path painter draws it: a stroke or a fill\n * with no `paint` is none, and an `'inherit'` one is `inherited`'s. */\nexport function pathPaintOf(\n data: { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null },\n inherited?: InheritedPaint,\n): PathPaint {\n const stroke = resolveStroke(data.stroke, inherited?.stroke);\n const fill = resolveFill(data.fill, inherited?.fill);\n return stroke !== null ? { fill, stroke } : { fill };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { nodeMemo } from "../../core/scene/nodeMemo.js";
2
2
  import { DERIVED_PAINTER, derivedDrawnBounds } from "./derivedPainter.js";
3
3
  import { PAINT_SLOT } from "./memo.js";
4
- import { inkReach, pathPaintOf, withAnimatedVertexColors } from "./nodePaint.js";
4
+ import { inkReach, inkTrim, pathPaintOf, withAnimatedTrim, withAnimatedVertexColors } from "./nodePaint.js";
5
5
  import { paintsPose, poseBoxBounds } from "./poseBox.js";
6
6
  import { strokedPoseBox } from "./strokedBounds.js";
7
7
  import { commandFill, commandStroke } from "@weasel-js/renderer";
@@ -15,7 +15,7 @@ var PATH_PAINTER = {
15
15
  if (!paintsPose(node.pose)) return false;
16
16
  return node.data?.path != null;
17
17
  },
18
- paint: (node, pose, ctx) => ctx?.derivedPath != null ? DERIVED_PAINTER.paint(node, pose, ctx) : withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
18
+ paint: (node, pose, ctx) => ctx?.derivedPath != null ? DERIVED_PAINTER.paint(node, pose, ctx) : withAnimatedTrim(withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
19
19
  const d = node.data;
20
20
  const projected = pathInPoseFrame(d.path, pose);
21
21
  const { fill: declared, stroke: strokeSpec } = pathPaintOf(d, ctx?.inherited);
@@ -28,7 +28,7 @@ var PATH_PAINTER = {
28
28
  ...fill && d.vertexColors ? { vertexColors: d.vertexColors } : {},
29
29
  ...stroke ? { stroke } : {}
30
30
  }];
31
- }, ctx?.inherited), ctx),
31
+ }, ctx?.inherited), ctx), ctx),
32
32
  silhouette: (node, pose, ctx) => {
33
33
  if (ctx?.derivedPath != null) return ctx.derivedPath;
34
34
  const d = node.data;
@@ -38,7 +38,8 @@ var PATH_PAINTER = {
38
38
  const { fill, stroke } = pathPaintOf(node.data, ctx?.inherited);
39
39
  return {
40
40
  filled: fill !== null,
41
- ...inkReach(stroke ?? null, ctx?.scale)
41
+ ...inkReach(stroke ?? null, ctx?.scale),
42
+ ...inkTrim(stroke ?? null)
42
43
  };
43
44
  },
44
45
  bounds: (node, pose, ctx) => poseBoxBounds(pose, resolveStroke(node.data.stroke, ctx?.inherited?.stroke), ctx),