@weasel-js/core 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/WeaselProvider.js +1 -1
- package/dist/affordances/pathAnchors.js +10 -6
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/canvas.js +3 -1
- package/dist/barrel/paint.js +4 -4
- package/dist/barrel/paths.js +8 -4
- package/dist/barrel/renderer.js +3 -2
- package/dist/barrel/routing.js +4 -4
- package/dist/barrel/scene.js +2 -1
- package/dist/barrel/text.js +2 -2
- package/dist/barrel/viewport.js +6 -2
- package/dist/canvas/Canvas.js +2 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/CanvasView.js +23 -34
- package/dist/canvas/CanvasView.js.map +1 -1
- package/dist/canvas/ModalityHud.js +1 -1
- package/dist/canvas/NodeShape.js +4 -1
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
- package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
- package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
- package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
- package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
- package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
- package/dist/canvas/SceneCanvas/props.js.map +1 -1
- package/dist/canvas/SceneCanvas/scenePointQuery.js +7 -3
- package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
- package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
- package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
- package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasTools.js +7 -2
- package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
- package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +10 -2
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
- package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +2 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +11 -10
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/ViewMarker.js +35 -0
- package/dist/canvas/ViewMarker.js.map +1 -0
- package/dist/canvas/defaultDrawOne.js +2 -0
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/deps/booleans.js +2 -1
- package/dist/canvas/deps/booleans.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +2 -2
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/insert.js +6 -2
- package/dist/canvas/deps/insert.js.map +1 -1
- package/dist/canvas/deps/pathEditCommits.js +3 -7
- package/dist/canvas/deps/pathEditCommits.js.map +1 -1
- package/dist/canvas/drawnBounds.js +11 -3
- package/dist/canvas/drawnBounds.js.map +1 -1
- package/dist/canvas/inheritedPaints.js +44 -0
- package/dist/canvas/inheritedPaints.js.map +1 -0
- package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
- package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/nodePaint.js +13 -34
- package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
- package/dist/canvas/nodeShapes/pathPainter.js +9 -8
- package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
- package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/shapePainter.js +11 -10
- package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
- package/dist/canvas/nodeShapes/textPainter.js +13 -12
- package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
- package/dist/canvas/pickReach.js +3 -3
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +3 -1
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/renderSceneToPixels.js +1 -1
- package/dist/canvas/renderSceneToPixels.js.map +1 -1
- package/dist/canvas/sceneSlotWiring.js +2 -1
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +2 -1
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/canvas/surfaceContribution.js +12 -8
- package/dist/canvas/surfaceContribution.js.map +1 -1
- package/dist/canvas/useCanvasView.js +4 -2
- package/dist/canvas/useCanvasView.js.map +1 -1
- package/dist/canvas/useLayerStack.js +6 -2
- package/dist/canvas/useLayerStack.js.map +1 -1
- package/dist/canvas/useViewAim.js +121 -0
- package/dist/canvas/useViewAim.js.map +1 -0
- package/dist/canvas/useViewNesting.js +3 -2
- package/dist/canvas/useViewNesting.js.map +1 -1
- package/dist/canvas/useViewRegistrySurface.js +4 -2
- package/dist/canvas/useViewRegistrySurface.js.map +1 -1
- package/dist/canvas/viewMarkers.js +63 -0
- package/dist/canvas/viewMarkers.js.map +1 -0
- package/dist/canvas/viewRegistry.js +27 -1
- package/dist/canvas/viewRegistry.js.map +1 -1
- package/dist/core/pages/pageEdits.js +2 -3
- package/dist/core/pages/pageEdits.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +59 -2
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/clipNothing.js +13 -0
- package/dist/core/scene/clipNothing.js.map +1 -0
- package/dist/core/scene/depMemo.js +11 -3
- package/dist/core/scene/depMemo.js.map +1 -1
- package/dist/core/scene/dependents.js +7 -1
- package/dist/core/scene/dependents.js.map +1 -1
- package/dist/core/scene/derivedParts.js +10 -11
- package/dist/core/scene/derivedParts.js.map +1 -1
- package/dist/core/scene/effectivePose.js +7 -2
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/fontReads.js +37 -0
- package/dist/core/scene/fontReads.js.map +1 -0
- package/dist/core/scene/inheritedPaint.js +75 -0
- package/dist/core/scene/inheritedPaint.js.map +1 -0
- package/dist/core/scene/kitRegistry.js +5 -0
- package/dist/core/scene/kitRegistry.js.map +1 -1
- package/dist/core/scene/nodeMemo.js +15 -7
- package/dist/core/scene/nodeMemo.js.map +1 -1
- package/dist/core/scene/pathEffects/blend.js.map +1 -1
- package/dist/core/scene/pathEffects/derive.js +7 -2
- package/dist/core/scene/pathEffects/derive.js.map +1 -1
- package/dist/core/scene/pathEffects/effects.js +3 -1
- package/dist/core/scene/pathEffects/effects.js.map +1 -1
- package/dist/core/scene/pathEffects/textAlong.js +95 -0
- package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
- package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-p8fDIAKt.d.ts} +2 -1
- package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-mG19aEdD.d.ts} +61 -13
- package/dist/devtools/registry.js +4 -4
- package/dist/devtools/registry.js.map +1 -1
- package/dist/features/measure/measureContribution.js +4 -2
- package/dist/features/measure/measureContribution.js.map +1 -1
- package/dist/features/modes/modeTriggers.js +7 -3
- package/dist/features/modes/modeTriggers.js.map +1 -1
- package/dist/features/pathEffects/effectSources.js +60 -0
- package/dist/features/pathEffects/effectSources.js.map +1 -0
- package/dist/features/pathEffects/readSource.js +17 -1
- package/dist/features/pathEffects/readSource.js.map +1 -1
- package/dist/features/pathEffects/textItems.js +53 -0
- package/dist/features/pathEffects/textItems.js.map +1 -0
- package/dist/features/pathEffects/useEffectSourcesLayer.js +57 -0
- package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
- package/dist/features/paths/pathEditState.js +5 -2
- package/dist/features/paths/pathEditState.js.map +1 -1
- package/dist/features/text/sceneTextDrafts.js +65 -0
- package/dist/features/text/sceneTextDrafts.js.map +1 -0
- package/dist/features/text/textHitAt.js +31 -2
- package/dist/features/text/textHitAt.js.map +1 -1
- package/dist/features/text/useSceneTextEdit.js +22 -18
- package/dist/features/text/useSceneTextEdit.js.map +1 -1
- package/dist/features/viewports/aim.js +105 -0
- package/dist/features/viewports/aim.js.map +1 -0
- package/dist/features/viewports/viewMarker.js +82 -0
- package/dist/features/viewports/viewMarker.js.map +1 -0
- package/dist/features/viewports/viewMarkerDrag.js +55 -0
- package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
- package/dist/index.d.ts +500 -201
- package/dist/index.js +23 -12
- package/dist/interactions/actions/booleans/booleans.js +2 -1
- package/dist/interactions/actions/booleans/booleans.js.map +1 -1
- package/dist/interactions/actions/booleans/operands.js +6 -5
- package/dist/interactions/actions/booleans/operands.js.map +1 -1
- package/dist/interactions/actions/clipboard/action.js +27 -17
- package/dist/interactions/actions/clipboard/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
- package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +7 -4
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
- package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
- package/dist/interactions/actions/defaults/contextMenu.js +17 -8
- package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
- package/dist/interactions/actions/defaults/delete.js +28 -14
- package/dist/interactions/actions/defaults/delete.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js +11 -8
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js +19 -9
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/escape.js +11 -8
- package/dist/interactions/actions/defaults/escape.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js +19 -13
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/ingest.js +10 -4
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
- package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js +8 -2
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
- package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
- package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
- package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
- package/dist/interactions/actions/defaults/reorder.js +24 -6
- package/dist/interactions/actions/defaults/reorder.js.map +1 -1
- package/dist/interactions/actions/defaults/selectAll.js +8 -5
- package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
- package/dist/interactions/actions/defaults/setFill.js +7 -3
- package/dist/interactions/actions/defaults/setFill.js.map +1 -1
- package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
- package/dist/interactions/actions/defaults/setStroke.js +12 -11
- package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
- package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
- package/dist/interactions/actions/defaults/toolActivate.js +7 -1
- package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
- package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
- package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js +19 -13
- package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
- package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
- package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +7 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/flip/action.js +8 -2
- package/dist/interactions/actions/flip/action.js.map +1 -1
- package/dist/interactions/actions/insert/repositionAction.js +2 -1
- package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
- package/dist/interactions/actions/move/action.js +7 -4
- package/dist/interactions/actions/move/action.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
- package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
- package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
- package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
- package/dist/interactions/actions/resize/cornerHandles.js +2 -1
- package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
- package/dist/interactions/actions/selectionSubtree.js +12 -7
- package/dist/interactions/actions/selectionSubtree.js.map +1 -1
- package/dist/interactions/actions/transform-again/action.js +8 -5
- package/dist/interactions/actions/transform-again/action.js.map +1 -1
- package/dist/json.d.ts +4 -0
- package/dist/json.js +3 -0
- package/dist/kitSettingsSchema.js +8 -1
- package/dist/kitSettingsSchema.js.map +1 -1
- package/dist/{math-CrJULaZI.d.ts → math-B56zxVs5.d.ts} +10 -1
- package/dist/math.d.ts +1 -1
- package/dist/math.js +2 -1
- package/dist/resize.d.ts +1 -1
- package/dist/test-seams.d.ts +2 -2
- package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
- package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
- package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
- package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
- package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
- package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
- package/dist/tools/builtin/hand/useHandTool.js +7 -4
- package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
- package/dist/tools/builtin/image/useImageTool.js +1 -0
- package/dist/tools/builtin/image/useImageTool.js.map +1 -1
- package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
- package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
- package/dist/tools/builtin/line/useLineTool.js +1 -0
- package/dist/tools/builtin/line/useLineTool.js.map +1 -1
- package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
- package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +14 -6
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
- package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
- package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
- package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
- package/dist/tools/builtin/rect/useRectTool.js +1 -0
- package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
- package/dist/tools/builtin/select/adapterPointQuery.js +3 -2
- package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
- package/dist/tools/builtin/select/pressDecision.js +32 -0
- package/dist/tools/builtin/select/pressDecision.js.map +1 -0
- package/dist/tools/builtin/select/selectionContributions.js +22 -13
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js +13 -10
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/slice/useSliceTool.js +22 -8
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/tools/builtin/star/useStarTool.js +13 -4
- package/dist/tools/builtin/star/useStarTool.js.map +1 -1
- package/dist/tools/builtin/sticky/stickyNode.js +4 -1
- package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
- package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
- package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
- package/dist/tools/builtin/text/useTextTool.js +3 -1
- package/dist/tools/builtin/text/useTextTool.js.map +1 -1
- package/dist/util/paint.js +1 -3
- package/dist/util/paint.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +28 -24
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readSource.js","names":[],"sources":["../../../src/features/pathEffects/readSource.ts"],"sourcesContent":["/**\n * How the kit's path effects read a source node: its outline from its\n * painter's silhouette, its paint as `kit:path` resolves it.\n */\nimport { pathThroughPoseFrame, poseLinearOf } from '@weasel-js/geom';\nimport type { Fill, Stroke } from '@weasel-js/paint';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport type { EffectSource } from 'core/scene/pathEffects/source';\nimport type { DerivedDep } from 'core/scene/types';\nimport { pathPaintOf } from 'canvas/nodeShapes/nodePaint';\n\n/** `dep` as a path effect's source, or `null` when it has no outline. */\nexport function readEffectSource(dep: DerivedDep<unknown>): EffectSource | null {\n const data = (dep.node.data ?? {}) as { fill?: Fill | null; stroke?: Stroke | null };\n const { fill, stroke } = pathPaintOf(data);\n const paint = { fill, stroke: stroke ?? null };\n const shown = dep.path;\n if (shown !== null) return { world: shown, ...paint };\n const local = findNodeShape(dep.node)?.silhouette?.(dep.node, dep.pose) ?? null;\n if (local === null) return null;\n const world = pathThroughPoseFrame(local, dep.pose);\n const pose = dep.pose;\n if (!isRectPose(pose) || poseLinearOf(pose) !== null) return { world, ...paint };\n return {\n world,\n framed: { path: local, cx: pose.x + pose.width / 2, cy: pose.y + pose.height / 2, rotation: pose.rotation ?? 0 },\n ...paint,\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"readSource.js","names":[],"sources":["../../../src/features/pathEffects/readSource.ts"],"sourcesContent":["/**\n * How the kit's path effects read a source node: its outline from its\n * painter's silhouette, its paint as `kit:path` resolves it.\n */\nimport { pathThroughPoseFrame, poseLinearOf, type PathInWorldPose } from '@weasel-js/geom';\nimport type { Fill, Stroke } from '@weasel-js/paint';\nimport { isTextData, type TextOutlineSource } from '@weasel-js/text';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport { noteFontRead } from 'core/scene/fontReads';\nimport type { EffectSource } from 'core/scene/pathEffects/source';\nimport type { DerivedDep } from 'core/scene/types';\nimport { pathPaintOf } from 'canvas/nodeShapes/nodePaint';\nimport { textEffectItems } from './textItems';\n\n/** `dep` as a path effect's source, or `null` when it has no outline. A text\n * node's source also carries its grapheme clusters as items. */\nexport function readEffectSource(dep: DerivedDep<unknown>): EffectSource | null {\n const data = dep.node.data;\n const text = isTextData(data);\n // A text node's outline is laid out in its face, which may not have landed: without this read, an effect that found\n // no outline while the face loads would never look again.\n if (text) noteFontRead();\n const source = readShape(dep);\n const pose = dep.pose;\n if (source === null || !text || !isRectPose(pose)) return source;\n return { ...source, items: () => textEffectItems(data as TextOutlineSource, pose as PathInWorldPose) };\n}\n\nfunction readShape(dep: DerivedDep<unknown>): EffectSource | null {\n const data = (dep.node.data ?? {}) as { fill?: Fill | null; stroke?: Stroke | null };\n const { fill, stroke } = pathPaintOf(data);\n const paint = { fill, stroke: stroke ?? null };\n const shown = dep.path;\n if (shown !== null) return { world: shown, ...paint };\n const local = findNodeShape(dep.node)?.silhouette?.(dep.node, dep.pose) ?? null;\n if (local === null) return null;\n const world = pathThroughPoseFrame(local, dep.pose);\n const pose = dep.pose;\n if (!isRectPose(pose) || poseLinearOf(pose) !== null) return { world, ...paint };\n return {\n world,\n framed: { path: local, cx: pose.x + pose.width / 2, cy: pose.y + pose.height / 2, rotation: pose.rotation ?? 0 },\n ...paint,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,KAA+C;CAC9E,MAAM,OAAO,IAAI,KAAK;CACtB,MAAM,OAAO,WAAW,IAAI;CAG5B,IAAI,MAAM,aAAa;CACvB,MAAM,SAAS,UAAU,GAAG;CAC5B,MAAM,OAAO,IAAI;CACjB,IAAI,WAAW,QAAQ,CAAC,QAAQ,CAAC,WAAW,IAAI,GAAG,OAAO;CAC1D,OAAO;EAAE,GAAG;EAAQ,aAAa,gBAAgB,MAA2B,IAAuB;CAAE;AACvG;AAEA,SAAS,UAAU,KAA+C;CAChE,MAAM,OAAQ,IAAI,KAAK,QAAQ,CAAC;CAChC,MAAM,EAAE,MAAM,WAAW,YAAY,IAAI;CACzC,MAAM,QAAQ;EAAE;EAAM,QAAQ,UAAU;CAAK;CAC7C,MAAM,QAAQ,IAAI;CAClB,IAAI,UAAU,MAAM,OAAO;EAAE,OAAO;EAAO,GAAG;CAAM;CACpD,MAAM,QAAQ,cAAc,IAAI,IAAI,CAAC,EAAE,aAAa,IAAI,MAAM,IAAI,IAAI,KAAK;CAC3E,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,QAAQ,qBAAqB,OAAO,IAAI,IAAI;CAClD,MAAM,OAAO,IAAI;CACjB,IAAI,CAAC,WAAW,IAAI,KAAK,aAAa,IAAI,MAAM,MAAM,OAAO;EAAE;EAAO,GAAG;CAAM;CAC/E,OAAO;EACL;EACA,QAAQ;GAAE,MAAM;GAAO,IAAI,KAAK,IAAI,KAAK,QAAQ;GAAG,IAAI,KAAK,IAAI,KAAK,SAAS;GAAG,UAAU,KAAK,YAAY;EAAE;EAC/G,GAAG;CACL;AACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { noteFontRead } from "../../core/scene/fontReads.js";
|
|
2
|
+
import { poseFrameOf, transformPath } from "@weasel-js/geom";
|
|
3
|
+
import { TextOutlinesError, loadTextOutlines, textClusters } from "@weasel-js/text";
|
|
4
|
+
|
|
5
|
+
//#region src/features/pathEffects/textItems.ts
|
|
6
|
+
/**
|
|
7
|
+
* A text node as a path effect's items: its first line's grapheme clusters,
|
|
8
|
+
* each one's outlines in its own frame, sized as the node draws them.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The clusters of `data`'s first line as items, laid in `pose`'s box width.
|
|
12
|
+
* The pose's scale grows them; its rotation and skew are dropped, since the
|
|
13
|
+
* path an effect lays them along decides their turn. `[]` while a face's
|
|
14
|
+
* outlines are loading — the load is started, and the font read is noted so
|
|
15
|
+
* the derivation re-runs once it lands.
|
|
16
|
+
*/
|
|
17
|
+
function textEffectItems(data, pose) {
|
|
18
|
+
noteFontRead();
|
|
19
|
+
let clusters;
|
|
20
|
+
try {
|
|
21
|
+
clusters = textClusters(data, { width: pose.width });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (!(err instanceof TextOutlinesError)) throw err;
|
|
24
|
+
loadTextOutlines(data).catch(() => {});
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const k = scaleOf(pose);
|
|
28
|
+
const grow = k === 1 ? null : [
|
|
29
|
+
k,
|
|
30
|
+
0,
|
|
31
|
+
0,
|
|
32
|
+
k,
|
|
33
|
+
0,
|
|
34
|
+
0
|
|
35
|
+
];
|
|
36
|
+
return clusters.map((c) => ({
|
|
37
|
+
length: c.advance * k,
|
|
38
|
+
middle: c.middle * k,
|
|
39
|
+
parts: c.paths.map((p) => ({
|
|
40
|
+
path: grow === null ? p.path : transformPath(p.path, grow),
|
|
41
|
+
fill: p.fill,
|
|
42
|
+
stroke: p.stroke ?? null
|
|
43
|
+
}))
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
function scaleOf(pose) {
|
|
47
|
+
const m = poseFrameOf(pose);
|
|
48
|
+
return m === null ? 1 : Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { textEffectItems };
|
|
53
|
+
//# sourceMappingURL=textItems.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textItems.js","names":[],"sources":["../../../src/features/pathEffects/textItems.ts"],"sourcesContent":["/**\n * A text node as a path effect's items: its first line's grapheme clusters,\n * each one's outlines in its own frame, sized as the node draws them.\n */\nimport { poseFrameOf, transformPath, type Affine, type PathInWorldPose } from '@weasel-js/geom';\nimport {\n loadTextOutlines, textClusters, TextOutlinesError, type TextCluster, type TextOutlineSource,\n} from '@weasel-js/text';\nimport { noteFontRead } from 'core/scene/fontReads';\nimport type { EffectItem } from 'core/scene/pathEffects/source';\n\n/**\n * The clusters of `data`'s first line as items, laid in `pose`'s box width.\n * The pose's scale grows them; its rotation and skew are dropped, since the\n * path an effect lays them along decides their turn. `[]` while a face's\n * outlines are loading — the load is started, and the font read is noted so\n * the derivation re-runs once it lands.\n */\nexport function textEffectItems(data: TextOutlineSource, pose: PathInWorldPose): readonly EffectItem[] {\n noteFontRead();\n let clusters: TextCluster[];\n try {\n clusters = textClusters(data, { width: pose.width });\n } catch (err) {\n if (!(err instanceof TextOutlinesError)) throw err;\n loadTextOutlines(data).catch(() => {});\n return [];\n }\n const k = scaleOf(pose);\n const grow: Affine | null = k === 1 ? null : [k, 0, 0, k, 0, 0];\n return clusters.map((c) => ({\n length: c.advance * k,\n middle: c.middle * k,\n parts: c.paths.map((p) => ({\n path: grow === null ? p.path : transformPath(p.path, grow),\n fill: p.fill,\n stroke: p.stroke ?? null,\n })),\n }));\n}\n\nfunction scaleOf(pose: PathInWorldPose): number {\n const m = poseFrameOf(pose);\n return m === null ? 1 : Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAAgB,MAAyB,MAA8C;CACrG,aAAa;CACb,IAAI;CACJ,IAAI;EACF,WAAW,aAAa,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC;CACrD,SAAS,KAAK;EACZ,IAAI,EAAE,eAAe,oBAAoB,MAAM;EAC/C,iBAAiB,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC;EACrC,OAAO,CAAC;CACV;CACA,MAAM,IAAI,QAAQ,IAAI;CACtB,MAAM,OAAsB,MAAM,IAAI,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CAC9D,OAAO,SAAS,KAAK,OAAO;EAC1B,QAAQ,EAAE,UAAU;EACpB,QAAQ,EAAE,SAAS;EACnB,OAAO,EAAE,MAAM,KAAK,OAAO;GACzB,MAAM,SAAS,OAAO,EAAE,OAAO,cAAc,EAAE,MAAM,IAAI;GACzD,MAAM,EAAE;GACR,QAAQ,EAAE,UAAU;EACtB,EAAE;CACJ,EAAE;AACJ;AAEA,SAAS,QAAQ,MAA+B;CAC9C,MAAM,IAAI,YAAY,IAAI;CAC1B,OAAO,MAAM,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;AACvE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { sceneDepLookup } from "../../canvas/derivedPath.js";
|
|
2
|
+
import { pathEffectOf } from "../../core/scene/pathEffects/derive.js";
|
|
3
|
+
import { PATH_OVERLAY_ROLES } from "../../canvas/pathOverlay.js";
|
|
4
|
+
import { chromeStateFrom } from "../../canvas/drawEnvelope.js";
|
|
5
|
+
import { readEffectSource } from "./readSource.js";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useLatest } from "@weasel-js/react";
|
|
8
|
+
|
|
9
|
+
//#region src/features/pathEffects/useEffectSourcesLayer.ts
|
|
10
|
+
/**
|
|
11
|
+
* The layer that shows what a selected path effect was made from: the outlines
|
|
12
|
+
* of the sources it hides, drawn faintly under the selection chrome.
|
|
13
|
+
*/
|
|
14
|
+
var FAINT = .4;
|
|
15
|
+
/** For each selected path-effect node with `hideSources`, its hidden sources' outlines as the effect reads them, in a
|
|
16
|
+
* one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */
|
|
17
|
+
function useEffectSourcesLayer(scene) {
|
|
18
|
+
const sceneRef = useLatest(scene);
|
|
19
|
+
return useMemo(() => ({
|
|
20
|
+
id: "effect-sources",
|
|
21
|
+
label: "Hidden effect sources",
|
|
22
|
+
space: "world",
|
|
23
|
+
draw: (data) => {
|
|
24
|
+
const selected = chromeStateFrom(data)?.selection ?? [];
|
|
25
|
+
const sc = sceneRef.current;
|
|
26
|
+
const stroke = PATH_OVERLAY_ROLES["chrome"].stroke;
|
|
27
|
+
const depOf = sceneDepLookup(sc);
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const id of selected) {
|
|
30
|
+
const node = sc.get(id);
|
|
31
|
+
if (pathEffectOf(node)?.hideSources !== true || !Array.isArray(node.dependsOn)) continue;
|
|
32
|
+
for (const source of node.dependsOn) {
|
|
33
|
+
if (!sc.isHidden(source)) continue;
|
|
34
|
+
const dep = depOf(source);
|
|
35
|
+
const path = dep === void 0 ? null : readEffectSource(dep)?.world ?? null;
|
|
36
|
+
if (path !== null) lines.push({
|
|
37
|
+
kind: "path",
|
|
38
|
+
path,
|
|
39
|
+
stroke: {
|
|
40
|
+
paint: stroke,
|
|
41
|
+
width: { px: 1 }
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return lines.length === 0 ? [] : [{
|
|
47
|
+
kind: "group",
|
|
48
|
+
alpha: FAINT,
|
|
49
|
+
children: lines
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
}), [sceneRef]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { useEffectSourcesLayer };
|
|
57
|
+
//# sourceMappingURL=useEffectSourcesLayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectSourcesLayer.js","names":[],"sources":["../../../src/features/pathEffects/useEffectSourcesLayer.ts"],"sourcesContent":["/**\n * The layer that shows what a selected path effect was made from: the outlines\n * of the sources it hides, drawn faintly under the selection chrome.\n */\nimport { useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { PATH_OVERLAY_ROLES } from 'canvas/pathOverlay';\nimport { sceneDepLookup } from 'canvas/derivedPath';\nimport { chromeStateFrom } from 'canvas/drawEnvelope';\nimport type { RenderLayer } from 'core/layers/render';\nimport { pathEffectOf } from 'core/scene/pathEffects/derive';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { readEffectSource } from './readSource';\n\nconst FAINT = 0.4;\n\n/** For each selected path-effect node with `hideSources`, its hidden sources' outlines as the effect reads them, in a\n * one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */\nexport function useEffectSourcesLayer<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n): RenderLayer<unknown> {\n const sceneRef = useLatest(scene);\n return useMemo<RenderLayer<unknown>>(() => ({\n id: 'effect-sources',\n label: 'Hidden effect sources',\n space: 'world',\n draw: (data) => {\n const selected = chromeStateFrom(data)?.selection ?? [];\n const sc = sceneRef.current;\n const stroke = PATH_OVERLAY_ROLES['chrome']!.stroke!;\n const depOf = sceneDepLookup(sc);\n const lines: DrawCommand[] = [];\n for (const id of selected) {\n const node = sc.get(id);\n if (pathEffectOf(node)?.hideSources !== true || !Array.isArray(node!.dependsOn)) continue;\n for (const source of node!.dependsOn as readonly NodeId[]) {\n if (!sc.isHidden(source)) continue;\n const dep = depOf(source);\n const path = dep === undefined ? null : readEffectSource(dep as never)?.world ?? null;\n if (path !== null) lines.push({ kind: 'path', path, stroke: { paint: stroke, width: { px: 1 } } });\n }\n }\n return lines.length === 0 ? [] : [{ kind: 'group', alpha: FAINT, children: lines }];\n },\n }), [sceneRef]);\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,IAAM,QAAQ;;;AAId,SAAgB,sBACd,OACsB;CACtB,MAAM,WAAW,UAAU,KAAK;CAChC,OAAO,eAAqC;EAC1C,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,SAAS;GACd,MAAM,WAAW,gBAAgB,IAAI,CAAC,EAAE,aAAa,CAAC;GACtD,MAAM,KAAK,SAAS;GACpB,MAAM,SAAS,mBAAmB,SAAS,CAAE;GAC7C,MAAM,QAAQ,eAAe,EAAE;GAC/B,MAAM,QAAuB,CAAC;GAC9B,KAAK,MAAM,MAAM,UAAU;IACzB,MAAM,OAAO,GAAG,IAAI,EAAE;IACtB,IAAI,aAAa,IAAI,CAAC,EAAE,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,KAAM,SAAS,GAAG;IACjF,KAAK,MAAM,UAAU,KAAM,WAAgC;KACzD,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG;KAC1B,MAAM,MAAM,MAAM,MAAM;KACxB,MAAM,OAAO,QAAQ,SAAY,OAAO,iBAAiB,GAAY,CAAC,EAAE,SAAS;KACjF,IAAI,SAAS,MAAM,MAAM,KAAK;MAAE,MAAM;MAAQ;MAAM,QAAQ;OAAE,OAAO;OAAQ,OAAO,EAAE,IAAI,EAAE;MAAE;KAAE,CAAC;IACnG;GACF;GACA,OAAO,MAAM,WAAW,IAAI,CAAC,IAAI,CAAC;IAAE,MAAM;IAAS,OAAO;IAAO,UAAU;GAAM,CAAC;EACpF;CACF,IAAI,CAAC,QAAQ,CAAC;AAChB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectSourceFor } from "../pathEffects/effectSources.js";
|
|
1
2
|
import { createEmitter } from "@weasel-js/registry";
|
|
2
3
|
|
|
3
4
|
//#region src/features/paths/pathEditState.ts
|
|
@@ -10,9 +11,11 @@ import { createEmitter } from "@weasel-js/registry";
|
|
|
10
11
|
var NO_IDS = /* @__PURE__ */ new Set();
|
|
11
12
|
var NO_EXTENSIONS = [];
|
|
12
13
|
var NO_ANCHORS = /* @__PURE__ */ new Set();
|
|
13
|
-
/** Whether `id` names a node `scene` holds and `selection` includes
|
|
14
|
+
/** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited
|
|
15
|
+
* through. */
|
|
14
16
|
function isSelectedNode(scene, selection, id) {
|
|
15
|
-
|
|
17
|
+
if (scene.get(id) == null) return false;
|
|
18
|
+
return selection.current.includes(id) || selection.current.some((sel) => effectSourceFor(scene, sel, "path") === id);
|
|
16
19
|
}
|
|
17
20
|
function createPathEditState(source = {}) {
|
|
18
21
|
let ownId = "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/features/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /**\n * The path-edit tool in use, or null for plain anchor editing. Path edit has\n * a set of named tools, one active at a time, each changing what a plain\n * drag does: `'simplifyBrush'` brushes the path simpler. A tool belongs to\n * the open path; with none open, or a different one, there is none.\n */\n getTool(): string | null;\n /** Switch tool, or pass null to go back to plain anchor editing. Ignored\n * with no path open. */\n setTool(id: string | null): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes. */\nexport function isSelectedNode(\n scene: { get(id: never): unknown },\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n return scene.get(id as never) != null && selection.current.includes(id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let ownTool: { id: string; on: string } | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (next(id) !== getEditingId()) ownTool = null;\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n getTool() {\n const on = getEditingId();\n const host = source.host?.();\n if (host?.tool) return on ? host.tool() : null;\n // Dropped on the read that finds its path gone: a host can close the\n // path without telling this state.\n if (ownTool && ownTool.on !== on) ownTool = null;\n return ownTool?.id ?? null;\n },\n setTool(id) {\n const host = source.host?.();\n if (host?.tool && host.setTool) { host.setTool(id); return; }\n const on = getEditingId();\n const tool = id && on ? { id, on } : null;\n if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;\n ownTool = tool;\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":";;;;;;;;;AAuFA,IAAM,yBAA8B,IAAI,IAAI;AAC5C,IAAM,gBAA8C,CAAC;AACrD,IAAM,6BAAkC,IAAI,IAAI;;AAGhD,SAAgB,eACd,OACA,WACA,IACS;CACT,OAAO,MAAM,IAAI,EAAW,KAAK,QAAQ,UAAU,QAAQ,SAAS,EAAE;AACxE;AAEA,SAAgB,oBAAoB,SAAyB,CAAC,GAAkB;CAC9E,IAAI,QAAQ;CACZ,IAAI,kBAAkB;CACtB,IAAI,UAAgC;CACpC,IAAI,cAAkC;CACtC,IAAI,UAA6C;CACjD,IAAI,QAAQ;CACZ,IAAI,eAAoC;CACxC,IAAI,aAAa,OAAO,cAAc;CACtC,MAAM,UAAU,cAAc;CAC9B,MAAM,SAAS,QAAQ;CAEvB,MAAM,QAAQ,OAA8B,MAAM;CAClD,MAAM,qBAA6B;EACjC,MAAM,OAAO,OAAO,OAAO;EAC3B,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK;EAC3C,IAAI,CAAC,IAAI,OAAO;EAChB,IAAI,OAAO,aAAa,CAAC,OAAO,UAAU,GAAG,OAAO;EACpD,IAAI,OAAO,kBAAkB,CAAC,OAAO,eAAe,EAAE,GAAG,OAAO;EAChE,OAAO;CACT;CAEA,OAAO;EACL;EACA,gBAAgB;GACd,MAAM,KAAK,aAAa;GACxB,IAAI,CAAC,IAAI,OAAO;GAChB,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,wBAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;GACxC,OAAO;EACT;EACA,aAAa,IAAI;GACf,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,KAAK,EAAE,MAAM,aAAa,GAAG,UAAU;GAC3C,IAAI,MAAM;IAAE,KAAK,aAAa,EAAE;IAAG;GAAQ;GAC3C,IAAI,KAAK,EAAE,MAAM,OAAO;GACxB,QAAQ,KAAK,EAAE;GACf,OAAO;EACT;EACA,0BAA0B;GACxB,MAAM,OAAO,OAAO,OAAO;GAC3B,OAAO,MAAM,mBAAmB,KAAK,qBAAqB,KAAK,gBAAgB,IAAI;EACrF;EACA,mBAAmB,MAAM;GACvB,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,mBAAmB,KAAK,oBAAoB;IAAE,KAAK,mBAAmB,IAAI;IAAG;GAAQ;GAC/F,kBAAkB;GAClB,OAAO;EACT;EACA,kBAAkB;EAClB,WAAW,MAAM;GACf,UAAU;GACV,OAAO;EACT;EACA,uBAAuB;EACvB,gBAAgB,MAAM;GACpB,IAAI,MAAM,UAAU,cAAc,SAAS,MAAM,UAAU,cAAc,OAAO;GAChF,eAAe;GACf,OAAO;EACT;EACA,qBAAqB;EACrB,cAAc,MAAM;GAClB,IAAI,SAAS,YAAY;GACzB,aAAa;GACb,OAAO;EACT;EACA,sBAAsB;EACtB,eAAe,MAAM;GACnB,cAAc;IAAE,OAAO,KAAK;IAAO,QAAQ,KAAK;GAAO;GACvD,OAAO;EACT;EACA,UAAU;GACR,MAAM,KAAK,aAAa;GACxB,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,MAAM,OAAO,KAAK,KAAK,KAAK,IAAI;GAG1C,IAAI,WAAW,QAAQ,OAAO,IAAI,UAAU;GAC5C,OAAO,SAAS,MAAM;EACxB;EACA,QAAQ,IAAI;GACV,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,QAAQ,KAAK,SAAS;IAAE,KAAK,QAAQ,EAAE;IAAG;GAAQ;GAC5D,MAAM,KAAK,aAAa;GACxB,MAAM,OAAO,MAAM,KAAK;IAAE;IAAI;GAAG,IAAI;GACrC,IAAI,MAAM,OAAO,SAAS,MAAM,MAAM,OAAO,SAAS,IAAI;GAC1D,UAAU;GACV,OAAO;EACT;EACA,WAAW,QAAQ;CACrB;AACF"}
|
|
1
|
+
{"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/features/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { effectSourceFor } from 'features/pathEffects/effectSources';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /**\n * The path-edit tool in use, or null for plain anchor editing. Path edit has\n * a set of named tools, one active at a time, each changing what a plain\n * drag does: `'simplifyBrush'` brushes the path simpler. A tool belongs to\n * the open path; with none open, or a different one, there is none.\n */\n getTool(): string | null;\n /** Switch tool, or pass null to go back to plain anchor editing. Ignored\n * with no path open. */\n setTool(id: string | null): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited\n * through. */\nexport function isSelectedNode(\n scene: Pick<Scene<unknown, string, unknown>, 'get'>,\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n if (scene.get(id as NodeId) == null) return false;\n return selection.current.includes(id)\n || selection.current.some((sel) => effectSourceFor(scene, sel as NodeId, 'path') === id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let ownTool: { id: string; on: string } | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (next(id) !== getEditingId()) ownTool = null;\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n getTool() {\n const on = getEditingId();\n const host = source.host?.();\n if (host?.tool) return on ? host.tool() : null;\n // Dropped on the read that finds its path gone: a host can close the\n // path without telling this state.\n if (ownTool && ownTool.on !== on) ownTool = null;\n return ownTool?.id ?? null;\n },\n setTool(id) {\n const host = source.host?.();\n if (host?.tool && host.setTool) { host.setTool(id); return; }\n const on = getEditingId();\n const tool = id && on ? { id, on } : null;\n if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;\n ownTool = tool;\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sceneDependentsOf } from "../../core/scene/dependents.js";
|
|
2
|
+
import { asNodeId } from "../../core/scene/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/features/text/sceneTextDrafts.ts
|
|
5
|
+
/**
|
|
6
|
+
* A text edit written through to the scene keystroke by keystroke, for a node
|
|
7
|
+
* the canvas shows somewhere besides the overlay: text something derives
|
|
8
|
+
* from, such as the hidden text a path effect lays along a path.
|
|
9
|
+
*/
|
|
10
|
+
/** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */
|
|
11
|
+
function editedData(data, edit, { setText, setRuns, setStyle }) {
|
|
12
|
+
let next = setText ? setText(data, edit.text) : {
|
|
13
|
+
...data,
|
|
14
|
+
text: edit.text
|
|
15
|
+
};
|
|
16
|
+
if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : {
|
|
17
|
+
...next,
|
|
18
|
+
runs: edit.runs
|
|
19
|
+
};
|
|
20
|
+
if (edit.style) next = setStyle ? setStyle(next, edit.style) : {
|
|
21
|
+
...next,
|
|
22
|
+
style: edit.style
|
|
23
|
+
};
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
var nextSession = 0;
|
|
27
|
+
/** Where an edit's writes go. Each `draft` is written through when something derives from the node; the `commit`
|
|
28
|
+
* writes the result; `end` closes the edit, first putting back the data the drafts replaced when no commit came. All
|
|
29
|
+
* the writes of one edit fold into one undo entry, so a commit undoes to the text before the first draft, and a
|
|
30
|
+
* cancel leaves an entry that changes nothing. */
|
|
31
|
+
function sceneTextDrafts(sceneRef, optsRef) {
|
|
32
|
+
let from = null;
|
|
33
|
+
let session = nextSession++;
|
|
34
|
+
const write = (id, data) => {
|
|
35
|
+
const scene = sceneRef.current;
|
|
36
|
+
scene.batch("Edit text", () => scene.update(asNodeId(id), { data }), { coalesceKey: `text-edit:${id}:${session}` });
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
draft: (id, draft) => {
|
|
40
|
+
const scene = sceneRef.current;
|
|
41
|
+
const nid = asNodeId(id);
|
|
42
|
+
const node = scene.get(nid);
|
|
43
|
+
if (!node || sceneDependentsOf(scene, nid).length === 0) return;
|
|
44
|
+
if (from?.id !== id) from = {
|
|
45
|
+
id,
|
|
46
|
+
data: node.data
|
|
47
|
+
};
|
|
48
|
+
write(id, editedData(node.data, draft, optsRef.current));
|
|
49
|
+
},
|
|
50
|
+
commit: (id, data) => {
|
|
51
|
+
from = null;
|
|
52
|
+
write(id, data);
|
|
53
|
+
},
|
|
54
|
+
end: (id) => {
|
|
55
|
+
const held = from;
|
|
56
|
+
from = null;
|
|
57
|
+
if (held?.id === id && sceneRef.current.get(asNodeId(id))) write(id, held.data);
|
|
58
|
+
session = nextSession++;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { editedData, sceneTextDrafts };
|
|
65
|
+
//# sourceMappingURL=sceneTextDrafts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sceneTextDrafts.js","names":[],"sources":["../../../src/features/text/sceneTextDrafts.ts"],"sourcesContent":["/**\n * A text edit written through to the scene keystroke by keystroke, for a node\n * the canvas shows somewhere besides the overlay: text something derives\n * from, such as the hidden text a path effect lays along a path.\n */\nimport { sceneDependentsOf } from '../../core/scene/dependents';\nimport { asNodeId, type Scene } from '../../core/scene/types';\nimport type { StyledRun, TextStyle } from '@weasel-js/text';\nimport type { TextEditDraft } from '@weasel-js/text-dom';\nimport type { DefaultTextData, UseSceneTextEditOptions } from './useSceneTextEdit';\n\n/** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */\nexport function editedData<TData extends DefaultTextData>(\n data: TData,\n edit: { text: string; runs?: readonly StyledRun[]; style?: TextStyle },\n { setText, setRuns, setStyle }: UseSceneTextEditOptions<TData>,\n): TData {\n let next = setText ? setText(data, edit.text) : { ...data, text: edit.text };\n if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : { ...next, runs: edit.runs };\n if (edit.style) next = setStyle ? setStyle(next, edit.style) : { ...next, style: edit.style };\n return next;\n}\n\n// Shared by every instance, so two editors of one node never fold their edits together.\nlet nextSession = 0;\n\n/** Where an edit's writes go. Each `draft` is written through when something derives from the node; the `commit`\n * writes the result; `end` closes the edit, first putting back the data the drafts replaced when no commit came. All\n * the writes of one edit fold into one undo entry, so a commit undoes to the text before the first draft, and a\n * cancel leaves an entry that changes nothing. */\nexport function sceneTextDrafts<TData extends DefaultTextData, TLayer extends string, TPose>(\n sceneRef: { readonly current: Scene<TData, TLayer, TPose> },\n optsRef: { readonly current: UseSceneTextEditOptions<TData> },\n): {\n draft: (id: string, draft: TextEditDraft) => void;\n commit: (id: string, data: TData) => void;\n end: (id: string) => void;\n} {\n let from: { id: string; data: TData } | null = null;\n let session = nextSession++;\n const write = (id: string, data: TData): void => {\n const scene = sceneRef.current;\n scene.batch('Edit text', () => scene.update(asNodeId(id), { data }), { coalesceKey: `text-edit:${id}:${session}` });\n };\n return {\n draft: (id, draft) => {\n const scene = sceneRef.current;\n const nid = asNodeId(id);\n const node = scene.get(nid);\n if (!node || sceneDependentsOf(scene, nid).length === 0) return;\n if (from?.id !== id) from = { id, data: node.data };\n write(id, editedData(node.data, draft, optsRef.current));\n },\n commit: (id, data) => {\n from = null;\n write(id, data);\n },\n end: (id) => {\n const held = from;\n from = null;\n if (held?.id === id && sceneRef.current.get(asNodeId(id))) write(id, held.data);\n session = nextSession++;\n },\n };\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,WACd,MACA,MACA,EAAE,SAAS,SAAS,YACb;CACP,IAAI,OAAO,UAAU,QAAQ,MAAM,KAAK,IAAI,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3E,IAAI,KAAK,MAAM,OAAO,UAAU,QAAQ,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3F,IAAI,KAAK,OAAO,OAAO,WAAW,SAAS,MAAM,KAAK,KAAK,IAAI;EAAE,GAAG;EAAM,OAAO,KAAK;CAAM;CAC5F,OAAO;AACT;AAGA,IAAI,cAAc;;;;;AAMlB,SAAgB,gBACd,UACA,SAKA;CACA,IAAI,OAA2C;CAC/C,IAAI,UAAU;CACd,MAAM,SAAS,IAAY,SAAsB;EAC/C,MAAM,QAAQ,SAAS;EACvB,MAAM,MAAM,mBAAmB,MAAM,OAAO,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,aAAa,aAAa,GAAG,GAAG,UAAU,CAAC;CACpH;CACA,OAAO;EACL,QAAQ,IAAI,UAAU;GACpB,MAAM,QAAQ,SAAS;GACvB,MAAM,MAAM,SAAS,EAAE;GACvB,MAAM,OAAO,MAAM,IAAI,GAAG;GAC1B,IAAI,CAAC,QAAQ,kBAAkB,OAAO,GAAG,CAAC,CAAC,WAAW,GAAG;GACzD,IAAI,MAAM,OAAO,IAAI,OAAO;IAAE;IAAI,MAAM,KAAK;GAAK;GAClD,MAAM,IAAI,WAAW,KAAK,MAAM,OAAO,QAAQ,OAAO,CAAC;EACzD;EACA,SAAS,IAAI,SAAS;GACpB,OAAO;GACP,MAAM,IAAI,IAAI;EAChB;EACA,MAAM,OAAO;GACX,MAAM,OAAO;GACb,OAAO;GACP,IAAI,MAAM,OAAO,MAAM,SAAS,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,MAAM,IAAI,KAAK,IAAI;GAC9E,UAAU;EACZ;CACF;AACF"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { derivedPartsOf } from "../../core/scene/derivedParts.js";
|
|
1
2
|
import { targetTransformOf, worldToLocal } from "../../core/geometry/targetFrame.js";
|
|
2
3
|
import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
|
|
3
|
-
import { scenePaintedPoseOf } from "../../canvas/derivedPath.js";
|
|
4
|
+
import { sceneDerivedPathOf, scenePaintedPoseOf } from "../../canvas/derivedPath.js";
|
|
5
|
+
import { effectSourceFor } from "../pathEffects/effectSources.js";
|
|
6
|
+
import { boundsOfPath, pathContainsPoint } from "@weasel-js/geom";
|
|
4
7
|
import { caretIndexAt, pointInTextPose } from "@weasel-js/text";
|
|
5
8
|
|
|
6
9
|
//#region src/features/text/textHitAt.ts
|
|
@@ -10,14 +13,30 @@ import { caretIndexAt, pointInTextPose } from "@weasel-js/text";
|
|
|
10
13
|
* `pointInTextPose` and `caretIndexAt` work in the node's own box, so the
|
|
11
14
|
* point is taken back through the frame the node is drawn in. The whole box
|
|
12
15
|
* takes the point, padding included; the caret is placed in the inset one.
|
|
16
|
+
*
|
|
17
|
+
* A path effect hiding a text source answers for it within the bounds of any
|
|
18
|
+
* glyph it lays, counters included, with the caret at the end of the text.
|
|
19
|
+
* A curved glyph takes a double-click anywhere in its axis-aligned box, wider
|
|
20
|
+
* than the ink selection picks.
|
|
13
21
|
*/
|
|
14
22
|
function textHitAt(scene, read, cx, cy) {
|
|
15
23
|
const order = scene.renderOrderNodes();
|
|
16
24
|
const paintedPoseOf = scenePaintedPoseOf(scene);
|
|
25
|
+
const derivedPathOf = sceneDerivedPathOf(scene);
|
|
17
26
|
for (let i = order.length - 1; i >= 0; i--) {
|
|
18
27
|
const node = order[i];
|
|
19
28
|
if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;
|
|
20
|
-
if (!read.isText(node.data))
|
|
29
|
+
if (!read.isText(node.data)) {
|
|
30
|
+
const source = effectSourceFor(scene, node.id, "text");
|
|
31
|
+
const text = source === null || scene.isLocked(source) ? void 0 : scene.get(source);
|
|
32
|
+
if (text === void 0 || !read.isText(text.data)) continue;
|
|
33
|
+
const path = derivedPathOf(node);
|
|
34
|
+
if (path !== null && onAGlyph(path, cx, cy)) return {
|
|
35
|
+
id: String(source),
|
|
36
|
+
caret: read.text(text.data).length
|
|
37
|
+
};
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
21
40
|
const at = paintedPoseOf(node);
|
|
22
41
|
const p = worldToLocal(targetTransformOf(at), cx, cy);
|
|
23
42
|
if (!p) continue;
|
|
@@ -45,6 +64,16 @@ function textHitAt(scene, read, cx, cy) {
|
|
|
45
64
|
}
|
|
46
65
|
return null;
|
|
47
66
|
}
|
|
67
|
+
/** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the
|
|
68
|
+
* hole in an o still take the click. */
|
|
69
|
+
function onAGlyph(path, x, y) {
|
|
70
|
+
const parts = derivedPartsOf(path);
|
|
71
|
+
if (parts === void 0) return pathContainsPoint(path, x, y);
|
|
72
|
+
return parts.some(({ path: part }) => {
|
|
73
|
+
const b = boundsOfPath(part);
|
|
74
|
+
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
48
77
|
|
|
49
78
|
//#endregion
|
|
50
79
|
export { textHitAt };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n const order = scene.renderOrderNodes();\n const paintedPoseOf = scenePaintedPoseOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) continue;\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { boundsOfPath, pathContainsPoint, type Path } from '@weasel-js/geom';\nimport { sceneDerivedPathOf, scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { derivedPartsOf } from '../../core/scene/derivedParts';\nimport { effectSourceFor } from '../pathEffects/effectSources';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n *\n * A path effect hiding a text source answers for it within the bounds of any\n * glyph it lays, counters included, with the caret at the end of the text.\n * A curved glyph takes a double-click anywhere in its axis-aligned box, wider\n * than the ink selection picks.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n const order = scene.renderOrderNodes();\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const derivedPathOf = sceneDerivedPathOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) {\n const source = effectSourceFor(scene, node.id, 'text');\n const text = source === null || scene.isLocked(source) ? undefined : scene.get(source);\n if (text === undefined || !read.isText(text.data)) continue;\n const path = derivedPathOf(node);\n if (path !== null && onAGlyph(path, cx, cy)) return { id: String(source), caret: read.text(text.data).length };\n continue;\n }\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n\n/** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the\n * hole in an o still take the click. */\nfunction onAGlyph(path: Path, x: number, y: number): boolean {\n const parts = derivedPartsOf(path);\n if (parts === undefined) return pathContainsPoint(path, x, y);\n return parts.some(({ path: part }) => {\n const b = boundsOfPath(part);\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,UACd,OACA,MACA,IACA,IACsC;CAEtC,MAAM,QAAQ,MAAM,iBAAiB;CACrC,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,EAAE,GAAG;EACxD,IAAI,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GAC3B,MAAM,SAAS,gBAAgB,OAAO,KAAK,IAAI,MAAM;GACrD,MAAM,OAAO,WAAW,QAAQ,MAAM,SAAS,MAAM,IAAI,SAAY,MAAM,IAAI,MAAM;GACrF,IAAI,SAAS,UAAa,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GACnD,MAAM,OAAO,cAAc,IAAI;GAC/B,IAAI,SAAS,QAAQ,SAAS,MAAM,IAAI,EAAE,GAAG,OAAO;IAAE,IAAI,OAAO,MAAM;IAAG,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC;GAAO;GAC7G;EACF;EACA,MAAM,KAAK,cAAc,IAAI;EAC7B,MAAM,IAAI,aAAa,kBAAkB,EAAE,GAAG,IAAI,EAAE;EACpD,IAAI,CAAC,GAAG;EACR,MAAM,OAAO;GACX,GAAG,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE;GACrD,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,OAAO,KAAK,MAAM,KAAK,IAAI;GAC3B,eAAe,KAAK,cAAc,KAAK,IAAI;EAC7C;EACA,MAAM,MAAM;GAAE,GAAG,GAAG;GAAG,GAAG,GAAG;GAAG,OAAO,GAAG;GAAO,QAAQ,GAAG;EAAO;EACnE,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG;GAAE,GAAG;GAAM,GAAG;EAAI,CAAC,GAAG;EACrD,OAAO;GAAE,IAAI,OAAO,KAAK,EAAE;GAAG,OAAO,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI;EAAE;CACpE;CACA,OAAO;AACT;;;AAIA,SAAS,SAAS,MAAY,GAAW,GAAoB;CAC3D,MAAM,QAAQ,eAAe,IAAI;CACjC,IAAI,UAAU,QAAW,OAAO,kBAAkB,MAAM,GAAG,CAAC;CAC5D,OAAO,MAAM,MAAM,EAAE,MAAM,WAAW;EACpC,MAAM,IAAI,aAAa,IAAI;EAC3B,OAAO,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE;CACpE,CAAC;AACH"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { withInheritedPaint } from "../../core/scene/inheritedPaint.js";
|
|
2
|
+
import { inheritedPaintIn } from "../../canvas/inheritedPaints.js";
|
|
1
3
|
import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
|
|
2
4
|
import { asNodeId } from "../../core/scene/types.js";
|
|
3
5
|
import { scenePaintedPoseOf } from "../../canvas/derivedPath.js";
|
|
4
6
|
import { findMountedCanvas } from "../../canvas/mountedCanvases.js";
|
|
5
7
|
import { drawOneEditing } from "./editingDrawOne.js";
|
|
6
8
|
import { textHitAt } from "./textHitAt.js";
|
|
9
|
+
import { editedData, sceneTextDrafts } from "./sceneTextDrafts.js";
|
|
7
10
|
import { poseFrameOf } from "@weasel-js/geom";
|
|
8
11
|
import { useCallback, useMemo, useRef } from "react";
|
|
9
12
|
import { useLatest } from "@weasel-js/react";
|
|
@@ -47,6 +50,7 @@ function useSceneTextEdit(scene, container, options = {}) {
|
|
|
47
50
|
const optsRef = useLatest(options);
|
|
48
51
|
const containerRef = useLatest(container);
|
|
49
52
|
const openedOnRef = useRef(null);
|
|
53
|
+
const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);
|
|
50
54
|
const surface = () => {
|
|
51
55
|
const root = containerRef.current;
|
|
52
56
|
const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;
|
|
@@ -70,7 +74,11 @@ function useSceneTextEdit(scene, container, options = {}) {
|
|
|
70
74
|
escape: options.escape,
|
|
71
75
|
fontHold: options.fontHold,
|
|
72
76
|
onStart: (id) => optsRef.current.onStart?.(id),
|
|
73
|
-
onEnd: (id) =>
|
|
77
|
+
onEnd: (id) => {
|
|
78
|
+
drafts.end(id);
|
|
79
|
+
optsRef.current.onEnd?.(id);
|
|
80
|
+
},
|
|
81
|
+
onDraft: drafts.draft,
|
|
74
82
|
getText: (id) => {
|
|
75
83
|
const node = sceneRef.current.get(asNodeId(id));
|
|
76
84
|
return node ? read.text(node.data) : "";
|
|
@@ -83,10 +91,7 @@ function useSceneTextEdit(scene, container, options = {}) {
|
|
|
83
91
|
const node = sceneRef.current.get(asNodeId(id));
|
|
84
92
|
if (!node) return void 0;
|
|
85
93
|
const get = optsRef.current.getPaint;
|
|
86
|
-
return get ? get(node.data) :
|
|
87
|
-
fill: node.data.fill,
|
|
88
|
-
stroke: node.data.stroke
|
|
89
|
-
};
|
|
94
|
+
return get ? get(node.data) : shownTextPaint(sceneRef.current, node);
|
|
90
95
|
},
|
|
91
96
|
getScreenPose: (id) => {
|
|
92
97
|
const scene = sceneRef.current;
|
|
@@ -105,20 +110,11 @@ function useSceneTextEdit(scene, container, options = {}) {
|
|
|
105
110
|
const nid = asNodeId(id);
|
|
106
111
|
const node = sceneRef.current.get(nid);
|
|
107
112
|
if (!node) return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
text
|
|
112
|
-
};
|
|
113
|
-
if (runs) data = setRuns ? setRuns(data, runs) : {
|
|
114
|
-
...data,
|
|
115
|
-
runs
|
|
116
|
-
};
|
|
117
|
-
if (style) data = setStyle ? setStyle(data, style) : {
|
|
118
|
-
...data,
|
|
113
|
+
drafts.commit(id, editedData(node.data, {
|
|
114
|
+
text,
|
|
115
|
+
runs,
|
|
119
116
|
style
|
|
120
|
-
};
|
|
121
|
-
sceneRef.current.update(nid, { data });
|
|
117
|
+
}, optsRef.current));
|
|
122
118
|
}
|
|
123
119
|
});
|
|
124
120
|
const onDoubleClick = useCallback((e) => {
|
|
@@ -219,6 +215,14 @@ function frameLinearOf(pose, stretch) {
|
|
|
219
215
|
d * stretch
|
|
220
216
|
];
|
|
221
217
|
}
|
|
218
|
+
/** A text node's paint as the canvas paints it: an inherited one is its container's. */
|
|
219
|
+
function shownTextPaint(scene, node) {
|
|
220
|
+
const { fill, stroke } = withInheritedPaint(node.data, inheritedPaintIn(scene, node));
|
|
221
|
+
return {
|
|
222
|
+
fill,
|
|
223
|
+
stroke
|
|
224
|
+
};
|
|
225
|
+
}
|
|
222
226
|
|
|
223
227
|
//#endregion
|
|
224
228
|
export { useSceneTextEdit };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSceneTextEdit.js","names":[],"sources":["../../../src/features/text/useSceneTextEdit.ts"],"sourcesContent":["/**\n * `useSceneTextEdit` — scene-aware wrapper around `useTextEdit`: scene\n * lookups for text / style / runs / screen-pose, and\n * `scene.update(id, { data })` for commits. The overlay and a double-click\n * both read the pose a node is painted at, so text inside a container whose\n * pose is a frame is edited where it is drawn.\n *\n * Defaults assume the node's `data` carries `text` / `style` / `runs`\n * directly. For custom data shapes, override any of the optional projections.\n * Consumers with non-rect poses should drop down to raw `useTextEdit` and\n * supply their own `getScreenPose`.\n */\nimport { useCallback, useMemo, useRef, type MouseEvent } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { asNodeId } from '../../core/scene/types';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport type { Scene } from '../../core/scene/types';\nimport { drawOneEditing, type EditingDrawOne } from './editingDrawOne';\nimport { textHitAt } from './textHitAt';\nimport type { Stroke, Fill } from '@weasel-js/paint';\nimport { resolveScreenLength } from '@weasel-js/paint';\nimport { clientToCanvas } from '@weasel-js/routing';\nimport { findMountedCanvas, type MountedCanvas } from '../../canvas/mountedCanvases';\nimport type { View } from '../../core/viewport/view';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { poseFrameOf } from '@weasel-js/geom';\nimport { isTextData, type StyledRun, type TextPaint, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf, type TextOverflow } from '../../canvas/nodeShapes/textPainter';\nimport { useTextEdit, type TextEditClipRect, type TextEditScreenPose, type UseTextEditReturn } from '@weasel-js/text-dom';\n\n/** Shape the default projections expect `data` to satisfy. All fields\n * optional so any object satisfies the constraint — projections fill in\n * the gaps for non-default shapes. */\nexport interface DefaultTextData {\n text?: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | null;\n stroke?: Stroke | null;\n verticalAlign?: TextVerticalAlign;\n padding?: number;\n overflow?: TextOverflow;\n}\n\n/** All-optional projections + fontSize fallback. */\nexport interface UseSceneTextEditOptions<TData> {\n /**\n * Is this node text? A double-click opens an edit only on a node that\n * answers yes, and passes through any other to the text under it. Default:\n * `isTextData`, the test `inferredNodeRouting` classifies a text node by. Pass one when `getText` reads some other shape.\n */\n isText?: (data: TData) => boolean;\n /** Read plain text from `data`. Default: `data.text ?? ''`. */\n getText?: (data: TData) => string;\n /** Read style from `data`. Default: `data.style`. */\n getStyle?: (data: TData) => TextStyle | undefined;\n /** Read the node's paint from `data`. Default: `data.fill` / `data.stroke`. */\n getPaint?: (data: TData) => TextPaint | undefined;\n /** Read rich-text runs from `data`. Default: `data.runs`. */\n getRuns?: (data: TData) => readonly StyledRun[] | undefined;\n /** Read the box vertical alignment from `data`. Default: `data.verticalAlign`. */\n getVerticalAlign?: (data: TData) => TextVerticalAlign | undefined;\n /** Read the inset from the node's box to its text from `data`. Default: `data.padding`. */\n getPadding?: (data: TData) => number | undefined;\n /** Read whether the node cuts its text at its box from `data`, which the\n * overlay then does too. Default: `data.overflow`. */\n getOverflow?: (data: TData) => TextOverflow | undefined;\n /** Produce updated data with new text. Default: `{ ...data, text }`. */\n setText?: (data: TData, text: string) => TData;\n /** Produce updated data with new runs. Default: `{ ...data, runs }`. */\n setRuns?: (data: TData, runs: StyledRun[]) => TData;\n /**\n * Produce updated data with a new node-wide style. Default:\n * `{ ...data, style }`. Written only when a range toggle has to *clear* a\n * flag the node itself sets — the run algebra is additive, so that edit\n * lowers the node flag and raises it on the runs outside the range. See\n * `useTextEdit`'s `setStyle`.\n *\n * An edit's text, runs, and style are folded through these three and\n * written as one `scene.update`, so a commit is one undo step.\n */\n setStyle?: (data: TData, style: TextStyle) => TData;\n /** Fallback fontSize when `style.fontSize` is unset. Default `16`. */\n defaultFontSize?: number;\n /**\n * The view to project through. Omit it and the view is read live from the\n * weasel canvas mounted inside `container` (the one a double-click landed\n * on, when there are several). With no canvas there, the node's world box\n * is passed through as screen pixels.\n *\n * A thunk is re-read on every projection; a plain `View` is the value from\n * the render that supplied it.\n */\n view?: View | (() => View);\n /**\n * Forwarded to `useTextEdit`: is `el` part of the editor's own chrome?\n * Focus moving into it does not end the edit. Wire it to whatever renders\n * the character controls.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** Forwarded to `useTextEdit`: what Escape does to the edit. */\n escape?: 'cancel' | 'commit';\n /** Forwarded to `useTextEdit`: how long an edit waits for its face. */\n fontHold?: number;\n /** Forwarded to `useTextEdit`: called as an edit of the node `id` opens,\n * whichever way it was started. */\n onStart?: (id: string) => void;\n /** Forwarded to `useTextEdit`: called as the edit of `id` closes, after\n * any commit has been written to the scene. */\n onEnd?: (id: string) => void;\n}\n\nfunction resolveView(view: View | (() => View) | undefined): View | undefined {\n return typeof view === 'function' ? view() : view;\n}\n\n/** Return shape extends `UseTextEditReturn` with an `onDoubleClick`\n * binding for the canvas container. Double-clicking inside a text\n * node enters edit mode with the caret seeded at the clicked glyph. */\nexport interface UseSceneTextEditReturn extends UseTextEditReturn {\n /** Bind to the element wrapping `<SceneCanvas>` (the same element\n * passed as `container`, or any ancestor that receives the dblclick\n * bubble). Starts an edit on the top-most text node under the cursor. */\n onDoubleClick: (e: MouseEvent<HTMLElement>) => void;\n /** The `isText` option asked of the node with this id: what\n * `TextEditDep.isTextNode` wants, so both ways into an edit agree. */\n isTextNode: (id: string) => boolean;\n /**\n * `defaultDrawOne`, less the glyphs of the node being edited: the overlay\n * stands in for its text, and the rest of the node, a `background` among\n * it, goes on painting. Pass it as `layers.scene.drawOne`, with\n * `paintBounds: defaultPaintBounds`, in place of an `alphaFor` that hides\n * the whole node.\n */\n drawOne: EditingDrawOne;\n}\n\n/** How each field is read from a node's `data`: the option's projection, else the default shape. */\nfunction projectionsOf<TData extends DefaultTextData>(opts: { readonly current: UseSceneTextEditOptions<TData> }) {\n return {\n isText: (data: TData): boolean =>\n opts.current.isText ? opts.current.isText(data) : isTextData(data),\n text: (data: TData): string =>\n opts.current.getText ? opts.current.getText(data) : (data.text ?? ''),\n style: (data: TData): TextStyle | undefined =>\n opts.current.getStyle ? opts.current.getStyle(data) : data.style,\n runs: (data: TData): readonly StyledRun[] | undefined =>\n opts.current.getRuns ? opts.current.getRuns(data) : data.runs,\n verticalAlign: (data: TData): TextVerticalAlign | undefined =>\n opts.current.getVerticalAlign ? opts.current.getVerticalAlign(data) : data.verticalAlign,\n padding: (data: TData): number | undefined =>\n opts.current.getPadding ? opts.current.getPadding(data) : data.padding,\n overflow: (data: TData): TextOverflow | undefined =>\n opts.current.getOverflow ? opts.current.getOverflow(data) : data.overflow,\n };\n}\n\n/** Wire double-click-to-edit text on a scene: mounts a DOM editing overlay\n * over the node being edited and writes the result back as an undoable op. */\nexport function useSceneTextEdit<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n // Refs, so the useTextEdit callbacks always see the current values\n // without re-binding.\n const sceneRef = useLatest(scene);\n const optsRef = useLatest(options);\n const containerRef = useLatest(container);\n // The last double-click's target, so the overlay projects through the\n // canvas the edit was opened on.\n const openedOnRef = useRef<EventTarget | null>(null);\n\n // The canvas's box in container pixels is both the origin its view projects\n // from and the region its glyphs are clipped to.\n const surface = (): { canvas: MountedCanvas; box: TextEditClipRect } | null => {\n const root = containerRef.current;\n const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;\n if (!root || !canvas) return null;\n const a = canvas.element.getBoundingClientRect();\n const b = root.getBoundingClientRect();\n return {\n canvas,\n box: {\n x: a.left - b.left - root.clientLeft + root.scrollLeft,\n y: a.top - b.top - root.clientTop + root.scrollTop,\n width: a.width,\n height: a.height,\n },\n };\n };\n\n const read = useMemo(() => projectionsOf(optsRef), [optsRef]);\n\n const edit = useTextEdit({\n container,\n isEditorChrome: (el) => optsRef.current.isEditorChrome?.(el) ?? false,\n escape: options.escape,\n fontHold: options.fontHold,\n onStart: (id) => optsRef.current.onStart?.(id),\n onEnd: (id) => optsRef.current.onEnd?.(id),\n getText: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.text(node.data) : '';\n },\n getStyle: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.style(node.data) : undefined;\n },\n getPaint: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n if (!node) return undefined;\n const get = optsRef.current.getPaint;\n return get ? get(node.data) : { fill: node.data.fill, stroke: node.data.stroke };\n },\n getScreenPose: (id) => {\n const scene = sceneRef.current;\n const node = scene.get(asNodeId(id));\n if (!node) return null;\n const at = surface();\n const view = resolveView(optsRef.current.view) ?? at?.canvas.getView();\n return screenPoseOf(node.data, scenePaintedPoseOf(scene)(node), read, view, at?.box, optsRef.current.defaultFontSize);\n },\n getClipRect: () => surface()?.box ?? null,\n getRuns: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.runs(node.data) : undefined;\n },\n commit: (id, { text, runs, style }) => {\n const nid = asNodeId(id);\n const node = sceneRef.current.get(nid);\n if (!node) return;\n const { setText, setRuns, setStyle } = optsRef.current;\n let data = setText ? setText(node.data, text) : { ...node.data, text };\n if (runs) data = setRuns ? setRuns(data, runs) : { ...data, runs };\n if (style) data = setStyle ? setStyle(data, style) : { ...data, style };\n sceneRef.current.update(nid, { data });\n },\n });\n\n const onDoubleClick = useCallback((e: MouseEvent<HTMLElement>) => {\n // The dblclick bubbles from the canvas up to any wrapping container; the\n // element it started on is what the click coordinates are relative to.\n const canvas = e.target instanceof HTMLElement ? e.target : null;\n if (!canvas) return;\n // Inside the editor a double-click selects a word, and its target is a run\n // span whose box the click would be measured against as if it were a canvas.\n if (canvas.closest('[contenteditable=\"true\"]')) return;\n\n // The click is un-projected here; `textHitAt` takes it back through the\n // frame the node is drawn in.\n const [canvasX, canvasY] = clientToCanvas(canvas, e.clientX, e.clientY);\n const root = containerRef.current;\n const view = resolveView(optsRef.current.view)\n ?? (root ? findMountedCanvas(root, canvas)?.getView() : undefined);\n const cx = view ? canvasX / view.scale.x + view.x : canvasX;\n const cy = view ? canvasY / view.scale.y + view.y : canvasY;\n const hit = textHitAt(sceneRef.current, read, cx, cy);\n if (!hit) return;\n openedOnRef.current = canvas;\n edit.startEdit(hit.id, { caret: hit.caret });\n }, [edit, containerRef, optsRef, read, sceneRef]);\n\n const isTextNode = useCallback((id: string): boolean => {\n const node = sceneRef.current.get(asNodeId(id));\n return node !== undefined && read.isText(node.data);\n }, [read, sceneRef]);\n\n const drawOne = useMemo(() => drawOneEditing(edit.editingId), [edit.editingId]);\n return { ...edit, onDoubleClick, isTextNode, drawOne };\n}\n\n/** The overlay's pose for a node painted at `painted`: its text box, inset\n * about the box center, which is all the overlay's frame needs, projected\n * through `view`. */\nfunction screenPoseOf<TData extends DefaultTextData>(\n d: TData,\n painted: RectPose,\n read: ReturnType<typeof projectionsOf<TData>>,\n view: View | undefined,\n origin: TextEditClipRect | undefined,\n defaultFontSize = 16,\n): TextEditScreenPose {\n const inset = textBoxOf({ padding: read.padding(d) }, painted);\n const box = screenBoxOf({ ...painted, ...inset }, view, origin);\n return {\n ...box,\n // Against `zoom`, the same factor the overlay scales back up by, so a\n // `{ px }` size arrives on screen at the pixel count it names.\n fontSize: resolveScreenLength(read.style(d)?.fontSize ?? defaultFontSize, box.zoom),\n verticalAlign: read.verticalAlign(d),\n ...(read.overflow(d) === 'clip' ? { clip: clipToBox(inset, painted) } : {}),\n };\n}\n\n/** How far `box` reaches past `inner` on each side: the overlay's cut at\n * `box` when its own pose is `inner`. */\nfunction clipToBox(inner: RectPose, box: RectPose): NonNullable<TextEditScreenPose['clip']> {\n return {\n top: inner.y - box.y,\n left: inner.x - box.x,\n bottom: box.y + box.height - (inner.y + inner.height),\n right: box.x + box.width - (inner.x + inner.width),\n };\n}\n\n/**\n * `pose`'s box as the overlay takes it, `origin` being the canvas's corner in\n * container pixels. Only the box center is projected, since the overlay's\n * frame is about it. Width and height stay in world units and reach the\n * screen through the overlay's own `scale(zoom)` — see\n * `TextEditScreenPose.zoom`.\n */\nfunction screenBoxOf(\n pose: RectPose,\n view: View | undefined,\n origin: { x: number; y: number } | undefined,\n): Pick<TextEditScreenPose, 'x' | 'y' | 'width' | 'height' | 'frame'> & { zoom: number } {\n const zoom = view ? view.scale.x : 1;\n const zoomY = view ? view.scale.y : 1;\n const cx = (origin?.x ?? 0) + (pose.x + pose.width / 2 - (view?.x ?? 0)) * zoom;\n const cy = (origin?.y ?? 0) + (pose.y + pose.height / 2 - (view?.y ?? 0)) * zoomY;\n return {\n x: cx - (pose.width * zoom) / 2,\n y: cy - (pose.height * zoom) / 2,\n width: pose.width,\n height: pose.height,\n zoom,\n frame: frameLinearOf(pose, zoomY / zoom),\n };\n}\n\n/** The linear part of the frame `pose` is drawn in, as the overlay takes it.\n * `stretch` is the view's y scale over its x scale: the overlay scales both\n * axes by the x scale, so the frame carries what is left of y. */\nfunction frameLinearOf(pose: unknown, stretch: number): TextEditScreenPose['frame'] {\n const m = poseFrameOf(pose);\n if (!m && stretch === 1) return undefined;\n const [a, b, c, d] = m ?? [1, 0, 0, 1];\n return [a, b * stretch, c, d * stretch];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSceneTextEdit.js","names":[],"sources":["../../../src/features/text/useSceneTextEdit.ts"],"sourcesContent":["/**\n * `useSceneTextEdit` — scene-aware wrapper around `useTextEdit`: scene\n * lookups for text / style / runs / screen-pose, and\n * `scene.update(id, { data })` for commits. The overlay and a double-click\n * both read the pose a node is painted at, so text inside a container whose\n * pose is a frame is edited where it is drawn.\n *\n * Defaults assume the node's `data` carries `text` / `style` / `runs`\n * directly. For custom data shapes, override any of the optional projections.\n * Consumers with non-rect poses should drop down to raw `useTextEdit` and\n * supply their own `getScreenPose`.\n */\nimport { useCallback, useMemo, useRef, type MouseEvent } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { asNodeId } from '../../core/scene/types';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport type { Scene } from '../../core/scene/types';\nimport { drawOneEditing, type EditingDrawOne } from './editingDrawOne';\nimport { textHitAt } from './textHitAt';\nimport { editedData, sceneTextDrafts } from './sceneTextDrafts';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport { resolveScreenLength } from '@weasel-js/paint';\nimport { inheritedPaintIn } from '../../canvas/inheritedPaints';\nimport { withInheritedPaint } from '../../core/scene/inheritedPaint';\nimport { clientToCanvas } from '@weasel-js/routing';\nimport { findMountedCanvas, type MountedCanvas } from '../../canvas/mountedCanvases';\nimport type { View } from '../../core/viewport/view';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { poseFrameOf } from '@weasel-js/geom';\nimport { isTextData, type StyledRun, type TextPaint, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf, type TextOverflow } from '../../canvas/nodeShapes/textPainter';\nimport { useTextEdit, type TextEditClipRect, type TextEditScreenPose, type UseTextEditReturn } from '@weasel-js/text-dom';\n\n/** Shape the default projections expect `data` to satisfy. All fields\n * optional so any object satisfies the constraint — projections fill in\n * the gaps for non-default shapes. */\nexport interface DefaultTextData {\n text?: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n verticalAlign?: TextVerticalAlign;\n padding?: number;\n overflow?: TextOverflow;\n}\n\n/** All-optional projections + fontSize fallback. */\nexport interface UseSceneTextEditOptions<TData> {\n /**\n * Is this node text? A double-click opens an edit only on a node that\n * answers yes, and passes through any other to the text under it. Default:\n * `isTextData`, the test `inferredNodeRouting` classifies a text node by. Pass one when `getText` reads some other shape.\n */\n isText?: (data: TData) => boolean;\n /** Read plain text from `data`. Default: `data.text ?? ''`. */\n getText?: (data: TData) => string;\n /** Read style from `data`. Default: `data.style`. */\n getStyle?: (data: TData) => TextStyle | undefined;\n /** Read the node's paint from `data`. Default: `data.fill` / `data.stroke`. */\n getPaint?: (data: TData) => TextPaint | undefined;\n /** Read rich-text runs from `data`. Default: `data.runs`. */\n getRuns?: (data: TData) => readonly StyledRun[] | undefined;\n /** Read the box vertical alignment from `data`. Default: `data.verticalAlign`. */\n getVerticalAlign?: (data: TData) => TextVerticalAlign | undefined;\n /** Read the inset from the node's box to its text from `data`. Default: `data.padding`. */\n getPadding?: (data: TData) => number | undefined;\n /** Read whether the node cuts its text at its box from `data`, which the\n * overlay then does too. Default: `data.overflow`. */\n getOverflow?: (data: TData) => TextOverflow | undefined;\n /** Produce updated data with new text. Default: `{ ...data, text }`. */\n setText?: (data: TData, text: string) => TData;\n /** Produce updated data with new runs. Default: `{ ...data, runs }`. */\n setRuns?: (data: TData, runs: StyledRun[]) => TData;\n /**\n * Produce updated data with a new node-wide style. Default:\n * `{ ...data, style }`. Written only when a range toggle has to *clear* a\n * flag the node itself sets — the run algebra is additive, so that edit\n * lowers the node flag and raises it on the runs outside the range. See\n * `useTextEdit`'s `setStyle`.\n *\n * An edit's text, runs, and style are folded through these three and\n * written as one `scene.update`, so a commit is one undo step.\n */\n setStyle?: (data: TData, style: TextStyle) => TData;\n /** Fallback fontSize when `style.fontSize` is unset. Default `16`. */\n defaultFontSize?: number;\n /**\n * The view to project through. Omit it and the view is read live from the\n * weasel canvas mounted inside `container` (the one a double-click landed\n * on, when there are several). With no canvas there, the node's world box\n * is passed through as screen pixels.\n *\n * A thunk is re-read on every projection; a plain `View` is the value from\n * the render that supplied it.\n */\n view?: View | (() => View);\n /**\n * Forwarded to `useTextEdit`: is `el` part of the editor's own chrome?\n * Focus moving into it does not end the edit. Wire it to whatever renders\n * the character controls.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** Forwarded to `useTextEdit`: what Escape does to the edit. */\n escape?: 'cancel' | 'commit';\n /** Forwarded to `useTextEdit`: how long an edit waits for its face. */\n fontHold?: number;\n /** Forwarded to `useTextEdit`: called as an edit of the node `id` opens,\n * whichever way it was started. */\n onStart?: (id: string) => void;\n /** Forwarded to `useTextEdit`: called as the edit of `id` closes, after\n * any commit has been written to the scene. */\n onEnd?: (id: string) => void;\n}\n\nfunction resolveView(view: View | (() => View) | undefined): View | undefined {\n return typeof view === 'function' ? view() : view;\n}\n\n/** Return shape extends `UseTextEditReturn` with an `onDoubleClick`\n * binding for the canvas container. Double-clicking inside a text\n * node enters edit mode with the caret seeded at the clicked glyph. */\nexport interface UseSceneTextEditReturn extends UseTextEditReturn {\n /** Bind to the element wrapping `<SceneCanvas>` (the same element\n * passed as `container`, or any ancestor that receives the dblclick\n * bubble). Starts an edit on the top-most text node under the cursor. */\n onDoubleClick: (e: MouseEvent<HTMLElement>) => void;\n /** The `isText` option asked of the node with this id: what\n * `TextEditDep.isTextNode` wants, so both ways into an edit agree. */\n isTextNode: (id: string) => boolean;\n /**\n * `defaultDrawOne`, less the glyphs of the node being edited: the overlay\n * stands in for its text, and the rest of the node, a `background` among\n * it, goes on painting. Pass it as `layers.scene.drawOne`, with\n * `paintBounds: defaultPaintBounds`, in place of an `alphaFor` that hides\n * the whole node.\n */\n drawOne: EditingDrawOne;\n}\n\n/** How each field is read from a node's `data`: the option's projection, else the default shape. */\nfunction projectionsOf<TData extends DefaultTextData>(opts: { readonly current: UseSceneTextEditOptions<TData> }) {\n return {\n isText: (data: TData): boolean =>\n opts.current.isText ? opts.current.isText(data) : isTextData(data),\n text: (data: TData): string =>\n opts.current.getText ? opts.current.getText(data) : (data.text ?? ''),\n style: (data: TData): TextStyle | undefined =>\n opts.current.getStyle ? opts.current.getStyle(data) : data.style,\n runs: (data: TData): readonly StyledRun[] | undefined =>\n opts.current.getRuns ? opts.current.getRuns(data) : data.runs,\n verticalAlign: (data: TData): TextVerticalAlign | undefined =>\n opts.current.getVerticalAlign ? opts.current.getVerticalAlign(data) : data.verticalAlign,\n padding: (data: TData): number | undefined =>\n opts.current.getPadding ? opts.current.getPadding(data) : data.padding,\n overflow: (data: TData): TextOverflow | undefined =>\n opts.current.getOverflow ? opts.current.getOverflow(data) : data.overflow,\n };\n}\n\n/** Wire double-click-to-edit text on a scene: mounts a DOM editing overlay\n * over the node being edited and writes the result back as an undoable op. */\nexport function useSceneTextEdit<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n // Refs, so the useTextEdit callbacks always see the current values\n // without re-binding.\n const sceneRef = useLatest(scene);\n const optsRef = useLatest(options);\n const containerRef = useLatest(container);\n // The last double-click's target, so the overlay projects through the\n // canvas the edit was opened on.\n const openedOnRef = useRef<EventTarget | null>(null);\n const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);\n\n // The canvas's box in container pixels is both the origin its view projects\n // from and the region its glyphs are clipped to.\n const surface = (): { canvas: MountedCanvas; box: TextEditClipRect } | null => {\n const root = containerRef.current;\n const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;\n if (!root || !canvas) return null;\n const a = canvas.element.getBoundingClientRect();\n const b = root.getBoundingClientRect();\n return {\n canvas,\n box: {\n x: a.left - b.left - root.clientLeft + root.scrollLeft,\n y: a.top - b.top - root.clientTop + root.scrollTop,\n width: a.width,\n height: a.height,\n },\n };\n };\n\n const read = useMemo(() => projectionsOf(optsRef), [optsRef]);\n\n const edit = useTextEdit({\n container,\n isEditorChrome: (el) => optsRef.current.isEditorChrome?.(el) ?? false,\n escape: options.escape,\n fontHold: options.fontHold,\n onStart: (id) => optsRef.current.onStart?.(id),\n onEnd: (id) => {\n drafts.end(id);\n optsRef.current.onEnd?.(id);\n },\n onDraft: drafts.draft,\n getText: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.text(node.data) : '';\n },\n getStyle: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.style(node.data) : undefined;\n },\n getPaint: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n if (!node) return undefined;\n const get = optsRef.current.getPaint;\n return get ? get(node.data) : shownTextPaint(sceneRef.current, node);\n },\n getScreenPose: (id) => {\n const scene = sceneRef.current;\n const node = scene.get(asNodeId(id));\n if (!node) return null;\n const at = surface();\n const view = resolveView(optsRef.current.view) ?? at?.canvas.getView();\n return screenPoseOf(node.data, scenePaintedPoseOf(scene)(node), read, view, at?.box, optsRef.current.defaultFontSize);\n },\n getClipRect: () => surface()?.box ?? null,\n getRuns: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.runs(node.data) : undefined;\n },\n commit: (id, { text, runs, style }) => {\n const nid = asNodeId(id);\n const node = sceneRef.current.get(nid);\n if (!node) return;\n drafts.commit(id, editedData(node.data, { text, runs, style }, optsRef.current));\n },\n });\n\n const onDoubleClick = useCallback((e: MouseEvent<HTMLElement>) => {\n // The dblclick bubbles from the canvas up to any wrapping container; the\n // element it started on is what the click coordinates are relative to.\n const canvas = e.target instanceof HTMLElement ? e.target : null;\n if (!canvas) return;\n // Inside the editor a double-click selects a word, and its target is a run\n // span whose box the click would be measured against as if it were a canvas.\n if (canvas.closest('[contenteditable=\"true\"]')) return;\n\n // The click is un-projected here; `textHitAt` takes it back through the\n // frame the node is drawn in.\n const [canvasX, canvasY] = clientToCanvas(canvas, e.clientX, e.clientY);\n const root = containerRef.current;\n const view = resolveView(optsRef.current.view)\n ?? (root ? findMountedCanvas(root, canvas)?.getView() : undefined);\n const cx = view ? canvasX / view.scale.x + view.x : canvasX;\n const cy = view ? canvasY / view.scale.y + view.y : canvasY;\n const hit = textHitAt(sceneRef.current, read, cx, cy);\n if (!hit) return;\n openedOnRef.current = canvas;\n edit.startEdit(hit.id, { caret: hit.caret });\n }, [edit, containerRef, optsRef, read, sceneRef]);\n\n const isTextNode = useCallback((id: string): boolean => {\n const node = sceneRef.current.get(asNodeId(id));\n return node !== undefined && read.isText(node.data);\n }, [read, sceneRef]);\n\n const drawOne = useMemo(() => drawOneEditing(edit.editingId), [edit.editingId]);\n return { ...edit, onDoubleClick, isTextNode, drawOne };\n}\n\n/** The overlay's pose for a node painted at `painted`: its text box, inset\n * about the box center, which is all the overlay's frame needs, projected\n * through `view`. */\nfunction screenPoseOf<TData extends DefaultTextData>(\n d: TData,\n painted: RectPose,\n read: ReturnType<typeof projectionsOf<TData>>,\n view: View | undefined,\n origin: TextEditClipRect | undefined,\n defaultFontSize = 16,\n): TextEditScreenPose {\n const inset = textBoxOf({ padding: read.padding(d) }, painted);\n const box = screenBoxOf({ ...painted, ...inset }, view, origin);\n return {\n ...box,\n // Against `zoom`, the same factor the overlay scales back up by, so a\n // `{ px }` size arrives on screen at the pixel count it names.\n fontSize: resolveScreenLength(read.style(d)?.fontSize ?? defaultFontSize, box.zoom),\n verticalAlign: read.verticalAlign(d),\n ...(read.overflow(d) === 'clip' ? { clip: clipToBox(inset, painted) } : {}),\n };\n}\n\n/** How far `box` reaches past `inner` on each side: the overlay's cut at\n * `box` when its own pose is `inner`. */\nfunction clipToBox(inner: RectPose, box: RectPose): NonNullable<TextEditScreenPose['clip']> {\n return {\n top: inner.y - box.y,\n left: inner.x - box.x,\n bottom: box.y + box.height - (inner.y + inner.height),\n right: box.x + box.width - (inner.x + inner.width),\n };\n}\n\n/**\n * `pose`'s box as the overlay takes it, `origin` being the canvas's corner in\n * container pixels. Only the box center is projected, since the overlay's\n * frame is about it. Width and height stay in world units and reach the\n * screen through the overlay's own `scale(zoom)` — see\n * `TextEditScreenPose.zoom`.\n */\nfunction screenBoxOf(\n pose: RectPose,\n view: View | undefined,\n origin: { x: number; y: number } | undefined,\n): Pick<TextEditScreenPose, 'x' | 'y' | 'width' | 'height' | 'frame'> & { zoom: number } {\n const zoom = view ? view.scale.x : 1;\n const zoomY = view ? view.scale.y : 1;\n const cx = (origin?.x ?? 0) + (pose.x + pose.width / 2 - (view?.x ?? 0)) * zoom;\n const cy = (origin?.y ?? 0) + (pose.y + pose.height / 2 - (view?.y ?? 0)) * zoomY;\n return {\n x: cx - (pose.width * zoom) / 2,\n y: cy - (pose.height * zoom) / 2,\n width: pose.width,\n height: pose.height,\n zoom,\n frame: frameLinearOf(pose, zoomY / zoom),\n };\n}\n\n/** The linear part of the frame `pose` is drawn in, as the overlay takes it.\n * `stretch` is the view's y scale over its x scale: the overlay scales both\n * axes by the x scale, so the frame carries what is left of y. */\nfunction frameLinearOf(pose: unknown, stretch: number): TextEditScreenPose['frame'] {\n const m = poseFrameOf(pose);\n if (!m && stretch === 1) return undefined;\n const [a, b, c, d] = m ?? [1, 0, 0, 1];\n return [a, b * stretch, c, d * stretch];\n}\n\n/** A text node's paint as the canvas paints it: an inherited one is its container's. */\nfunction shownTextPaint<TData extends DefaultTextData>(\n scene: Scene<TData, string, unknown>,\n node: NonNullable<ReturnType<Scene<TData, string, unknown>['get']>>,\n): TextPaint {\n const { fill, stroke } = withInheritedPaint(node.data, inheritedPaintIn(scene, node));\n return { fill, stroke };\n}\n"],"mappings":"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"}
|