fix(editor): enable outlining grouped strokes

This commit is contained in:
Danila Poyarkov 2026-05-17 20:27:45 +03:00
parent 28f7ef165b
commit b1ad6d41e9
5 changed files with 48 additions and 7 deletions

View file

@ -66,7 +66,9 @@ function lineStrokePath(r: SkiaRenderer, node: SceneNode): Path | null {
path.moveTo(0, 0)
path.lineTo(node.width, node.height)
const stroke = node.strokes.find((item) => item.visible)
return path.stroke({ width: stroke?.weight ?? 1 })
const outline = path.stroke({ width: stroke?.weight ?? 1 })
path.delete()
return outline
}
function baseShapePath(r: SkiaRenderer, node: SceneNode): Path | null {
@ -180,6 +182,15 @@ export function makeBooleanSourcePath(
return path
}
export function hasVisibleStrokeSourceNode(node: SceneNode, graph: SceneGraph): boolean {
if (nodeHasVisibleStroke(node)) return true
if (!canContainFlattenableChildren(node)) return false
return node.childIds.some((childId) => {
const child = graph.getNode(childId)
return child?.visible === true && hasVisibleStrokeSourceNode(child, graph)
})
}
export function makeStrokeOutlinePath(
r: SkiaRenderer,
node: SceneNode,
@ -220,7 +231,6 @@ export function makeStrokeOutlinePath(
if (node.type === 'LINE') return path
const outline = new r.ck.Path()
addVisibleStrokeOutlines(outline, path, node)
path.delete()
return outline
}

View file

@ -1,3 +1,8 @@
export { canMakeBooleanSourceNode, canMakeBooleanSourcePath, nodeHasVisibleStroke } from './boolean'
export {
canMakeBooleanSourceNode,
canMakeBooleanSourcePath,
hasVisibleStrokeSourceNode,
nodeHasVisibleStroke
} from './boolean'
export { SkiaRenderer, type RenderOverlays, type RulerTheme } from './renderer'
export { getAbsolutePositionFull, getAbsoluteRotation, getWorldHandles } from './coordinate'

View file

@ -1,4 +1,4 @@
import { canMakeBooleanSourceNode, nodeHasVisibleStroke } from '#core/canvas/boolean'
import { canMakeBooleanSourceNode, hasVisibleStrokeSourceNode } from '#core/canvas/boolean'
import { flattenNodesToVectorProps, outlineStrokeNodesToVectorProps } from '#core/canvas/flatten'
import { restoreSubtree, snapshotSubtree } from '#core/editor/clipboard/subtree-history'
import type { EditorContext } from '#core/editor/types'
@ -76,7 +76,7 @@ export function outlineStrokeSelected(ctx: EditorContext, selectedNodes: SceneNo
return flattenSelected(ctx, selectedNodes, {
label: 'Outline stroke',
canFlattenNode: (node) =>
canMakeBooleanSourceNode(node, ctx.graph) && nodeHasVisibleStroke(node),
canMakeBooleanSourceNode(node, ctx.graph) && hasVisibleStrokeSourceNode(node, ctx.graph),
vectorPropsFactory: outlineStrokeNodesToVectorProps
})
}

View file

@ -1,6 +1,6 @@
import { computed } from 'vue'
import { canMakeBooleanSourceNode, nodeHasVisibleStroke } from '@open-pencil/core/canvas'
import { canMakeBooleanSourceNode, hasVisibleStrokeSourceNode } from '@open-pencil/core/canvas'
import { useSelectionState } from '#vue/editor/selection-state/use'
import { useSceneComputed } from '#vue/internal/scene-computed/use'
@ -56,7 +56,9 @@ export function useSelectionCapabilities() {
return (
nodes.length > 0 &&
nodes.every(
(node) => nodeHasVisibleStroke(node) && canMakeBooleanSourceNode(node, editor.graph)
(node) =>
hasVisibleStrokeSourceNode(node, editor.graph) &&
canMakeBooleanSourceNode(node, editor.graph)
)
)
}),

View file

@ -94,6 +94,30 @@ describe('flattenSelected', () => {
surface.delete()
})
test('outlines stroked descendants inside groups', async () => {
const { editor, surface } = await createEditorWithRenderer()
const pageId = editor.state.currentPageId
const rect = editor.graph.createNode('RECTANGLE', pageId, {
x: 20,
y: 30,
width: 40,
height: 30,
fills: [TRANSPARENT],
strokes: [{ type: 'SOLID', color: BLACK, weight: 8, opacity: 1, visible: true }]
})
editor.select([rect.id])
editor.groupSelected()
editor.outlineStrokeSelected()
const [vectorId] = [...editor.state.selectedIds]
const vector = editor.graph.getNode(vectorId)
expect(vector?.type).toBe('VECTOR')
expect(vector?.name).toBe('Outline stroke')
expect(vector?.vectorNetwork?.vertices.length).toBeGreaterThan(0)
surface.delete()
})
test('does not outline fill-only shapes as strokes', async () => {
const { editor, surface } = await createEditorWithRenderer()
const pageId = editor.state.currentPageId