1.2.1: strokes scale with the layer (incl. inside SVG-asset placements)
- strokeCfg now defaults to strokeScaleEnabled:true so stroked rect / circle / text outlines grow proportionally when the layer is scaled (Konva default; previously hard-pinned to false). - polygonConfig and clipPolygonConfig had inline stroke blocks that also forced strokeScaleEnabled:false. Flipped both to true so freeform / polygon outlines scale alongside the shape. - SVG-asset placements use a dedicated svgChildConfig wrapper + pre-multiplied strokeWidth in svgStackForAsset: each child's stroke is bumped by the placement's geometric-mean scale and rendered with strokeScaleEnabled:false (Konva resets the ctx to identity before stroking) so the on-screen width lands at the right pixel count regardless of how Konva propagates parent transforms to nested stroke rendering. Konva-mounted tests cover outer-scale 1×, 3× and non-uniform 4×/1×. UI chrome (transformer borders, vertex / tangent handles, hit-shape outlines, animation-path ghosts, ghost-locked layer indicators) keeps strokeScaleEnabled:false intentionally — those should stay at constant pixel width regardless of zoom.
This commit is contained in:
1 parent
d03fbe4ecb
commit
3bc329813c
4 files changed
+213
-13
No files matched your search
@@ -13956,6 +13956,154 @@ export function registerAllTests(): void {
|
||||
expect(restoredAsset.canvas.height).toBe(80)
|
||||
expect(restoredAsset.layers.length).toBe(1)
|
||||
})
|
||||
|
||||
/** Konva-mounted helpers for the stroke-scaling tests below. */
|
||||
async function mountCanvasWithKonva() {
|
||||
const VueKonva = (await import('vue-konva')).default
|
||||
const host = document.createElement('div')
|
||||
host.setAttribute('data-test-host', '')
|
||||
host.style.width = '600px'
|
||||
host.style.height = '400px'
|
||||
document.body.appendChild(host)
|
||||
const app = createApp({ render() { return h(CanvasStage, {}) } })
|
||||
app.use(VueKonva)
|
||||
app.directive('tooltip', vTooltip)
|
||||
app.mount(host)
|
||||
return { host, app, unmount() { app.unmount(); host.remove() } }
|
||||
}
|
||||
async function getKonvaStage() {
|
||||
const Konva = (await import('konva')).default
|
||||
return Konva.stages[Konva.stages.length - 1]
|
||||
}
|
||||
|
||||
test('strokes inside an SVG asset placement scale with the outer scale', async () => {
|
||||
// Regression: Konva's strokeScaleEnabled flag honours only the
|
||||
// node's own scale, so an inner v-rect with scaleX=1 inside a
|
||||
// v-group with scaleX=3 used to render a constant-pixel stroke.
|
||||
// svgStackForAsset now pre-multiplies child strokeWidth by the
|
||||
// outer scale, and svgChildConfig forces strokeScaleEnabled:false
|
||||
// so the multiplied width lands as the right number of on-screen
|
||||
// pixels regardless of how Konva propagates parent transforms.
|
||||
const s = makeFreshStore()
|
||||
s.setDuration(1)
|
||||
s.scene.canvas = { width: 200, height: 200, background: '#ffffff' }
|
||||
const svg = s.addSvgAsset('Logo', 100, 100)
|
||||
s.enterSvgAssetEdit(svg.id)
|
||||
const rect = s.addAsset({
|
||||
kind: 'rect', name: 'R', width: 60, height: 60,
|
||||
fill: '#22cc88', stroke: '#000000', strokeWidth: 4,
|
||||
})
|
||||
s.addLayer(rect.id)
|
||||
s.exitSvgAssetEdit()
|
||||
const placement = s.addLayer(svg.id)!
|
||||
// Bake a 3× scale on the placement.
|
||||
s.upsertKeyframe(placement.id, 0, { scaleX: 3, scaleY: 3 })
|
||||
const handle = await mountCanvasWithKonva()
|
||||
try {
|
||||
await nextTick()
|
||||
await tick(40)
|
||||
const stage = await getKonvaStage() as unknown as {
|
||||
findOne: (sel: string) => unknown
|
||||
}
|
||||
// Outer v-group carries the placement layer's id.
|
||||
const group = stage.findOne(`#${placement.id}`) as unknown as {
|
||||
find: (sel: string) => Array<{
|
||||
stroke: () => string | undefined
|
||||
strokeWidth: () => number
|
||||
strokeScaleEnabled: () => boolean
|
||||
}>
|
||||
} | null
|
||||
expect(group).toBeTruthy()
|
||||
// The svg-hit-catcher rect has no stroke colour set
|
||||
// (Konva defaults `strokeWidth` to 2 even with no stroke).
|
||||
// Distinguish the actual child rect by `stroke()` being a
|
||||
// real colour.
|
||||
const rects = group!.find('Rect')
|
||||
const stroked = rects.find((r) => !!r.stroke())
|
||||
expect(stroked).toBeTruthy()
|
||||
// Pre-multiplied: base 4 × outer scale 3 = 12.
|
||||
expect(stroked!.strokeWidth()).toBeCloseTo(12, 5)
|
||||
// strokeScaleEnabled stays off so Konva resets the ctx
|
||||
// before stroking — the multiplied width then maps 1:1 to
|
||||
// on-screen pixels.
|
||||
expect(stroked!.strokeScaleEnabled()).toBe(false)
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('outer scale = 1: stroke width passes through unchanged', async () => {
|
||||
// Sanity: with no scaling on the placement we don't multiply
|
||||
// anything (the 1× factor in svgStackForAsset short-circuits).
|
||||
const s = makeFreshStore()
|
||||
s.setDuration(1)
|
||||
s.scene.canvas = { width: 200, height: 200, background: '#ffffff' }
|
||||
const svg = s.addSvgAsset('Logo', 100, 100)
|
||||
s.enterSvgAssetEdit(svg.id)
|
||||
const rect = s.addAsset({
|
||||
kind: 'rect', name: 'R', width: 60, height: 60,
|
||||
fill: '#22cc88', stroke: '#000000', strokeWidth: 5,
|
||||
})
|
||||
s.addLayer(rect.id)
|
||||
s.exitSvgAssetEdit()
|
||||
const placement = s.addLayer(svg.id)!
|
||||
const handle = await mountCanvasWithKonva()
|
||||
try {
|
||||
await nextTick()
|
||||
await tick(40)
|
||||
const stage = await getKonvaStage() as unknown as {
|
||||
findOne: (sel: string) => unknown
|
||||
}
|
||||
const group = stage.findOne(`#${placement.id}`) as unknown as {
|
||||
find: (sel: string) => Array<{
|
||||
stroke: () => string | undefined
|
||||
strokeWidth: () => number
|
||||
}>
|
||||
}
|
||||
const stroked = group.find('Rect').find((r) => !!r.stroke())
|
||||
expect(stroked).toBeTruthy()
|
||||
expect(stroked!.strokeWidth()).toBeCloseTo(5, 5)
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('non-uniform outer scale: strokeWidth uses the geometric mean', async () => {
|
||||
// For scaleX != scaleY the cleanest single-thickness number is
|
||||
// sqrt(|sx| * |sy|) — the geometric mean. With sx=4, sy=1 the
|
||||
// factor is 2, so a base 3px stroke → 6px on screen.
|
||||
const s = makeFreshStore()
|
||||
s.setDuration(1)
|
||||
const svg = s.addSvgAsset('Logo', 100, 100)
|
||||
s.enterSvgAssetEdit(svg.id)
|
||||
const rect = s.addAsset({
|
||||
kind: 'rect', name: 'R', width: 60, height: 60,
|
||||
fill: '#22cc88', stroke: '#000000', strokeWidth: 3,
|
||||
})
|
||||
s.addLayer(rect.id)
|
||||
s.exitSvgAssetEdit()
|
||||
const placement = s.addLayer(svg.id)!
|
||||
s.upsertKeyframe(placement.id, 0, { scaleX: 4, scaleY: 1 })
|
||||
const handle = await mountCanvasWithKonva()
|
||||
try {
|
||||
await nextTick()
|
||||
await tick(40)
|
||||
const stage = await getKonvaStage() as unknown as {
|
||||
findOne: (sel: string) => unknown
|
||||
}
|
||||
const group = stage.findOne(`#${placement.id}`) as unknown as {
|
||||
find: (sel: string) => Array<{
|
||||
stroke: () => string | undefined
|
||||
strokeWidth: () => number
|
||||
}>
|
||||
}
|
||||
const stroked = group.find('Rect').find((r) => !!r.stroke())
|
||||
expect(stroked).toBeTruthy()
|
||||
expect(stroked!.strokeWidth()).toBeCloseTo(6, 4)
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('svg asset — thumbnail rendering', () => {
|
||||
|
||||
Reference in new issue
Block a user