Unify preview-aware transforms across scene drawing, labels, selection and input. Give live property and creation edits explicit preview ownership, preserve cancellation and one-step undo, and reuse bounded text preparation resources. Include retained device-grid handling, worktree HMR coverage, and nested/reflected interaction regressions in this cohesive progress checkpoint. Validation: full check passes; 734 scoped unit tests and 53 targeted browser tests pass. This is NOT merge-ready: the unchanged exact nested filled-section held/released regression still fails with 63 differing pixels (maximum channel delta 5/255). The browser pass count excludes that separately run failure and the previously classified paint-field-width baseline. Raster-origin investigation and broader acceptance remain outstanding.
145 lines
5.9 KiB
TypeScript
145 lines
5.9 KiB
TypeScript
import { ROTATION_HANDLE_DISTANCE } from '@open-pencil/core/constants'
|
|
import type { Vector } from '@open-pencil/scene-graph/primitives'
|
|
|
|
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
import { applyFixtureTransform, nestedGeometryData } from '#tests/helpers/geometry'
|
|
|
|
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
|
|
|
|
for (const flipX of [false, true]) {
|
|
for (const flipY of [false, true]) {
|
|
test(`nested rotation, labels and cancellation (flipX=${flipX}, flipY=${flipY})`, async () => {
|
|
const data = nestedGeometryData(flipX, flipY)
|
|
const id = await editor.page.evaluate((fixture) => {
|
|
const store = window.openPencil?.getStore?.()
|
|
if (!store) throw new Error('Editor unavailable')
|
|
const s = fixture.section
|
|
const sectionId = store.createShape('SECTION', s.x, s.y, s.width, s.height)
|
|
store.updateNode(sectionId, s)
|
|
const frame = store.graph.createNode('FRAME', sectionId, {
|
|
...fixture.frame,
|
|
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }]
|
|
})
|
|
store.graph.createNode('RECTANGLE', frame.id, {
|
|
name: 'Asymmetric content',
|
|
x: 15,
|
|
y: 14,
|
|
width: 70,
|
|
height: 30,
|
|
cornerRadius: 6,
|
|
fills: [
|
|
{ type: 'SOLID', color: { r: 0.85, g: 0.25, b: 0.3, a: 1 }, opacity: 1, visible: true }
|
|
]
|
|
})
|
|
store.select([frame.id])
|
|
store.requestRender()
|
|
return frame.id
|
|
}, data)
|
|
await editor.canvas.waitForRender()
|
|
const bounds = expectDefined(
|
|
await editor.page.getByTestId('canvas-element').boundingBox(),
|
|
'canvas bounds'
|
|
)
|
|
const viewport = await editor.page.evaluate(() => {
|
|
const state = window.openPencil?.getStore?.().state
|
|
if (!state) throw new Error('Editor unavailable')
|
|
return { zoom: state.zoom, panX: state.panX, panY: state.panY }
|
|
})
|
|
const world = (point: Vector) =>
|
|
applyFixtureTransform(data.section, applyFixtureTransform(data.frame, point))
|
|
const center = world({ x: data.frame.width / 2, y: data.frame.height / 2 })
|
|
const stalkOffset = ROTATION_HANDLE_DISTANCE / viewport.zoom
|
|
const reflectedY = data.section.flipY !== data.frame.flipY
|
|
const start = world({
|
|
x: data.frame.width / 2,
|
|
y: reflectedY ? data.frame.height + stalkOffset : -stalkOffset
|
|
})
|
|
const radians = Math.PI / 6
|
|
const dx = start.x - center.x
|
|
const dy = start.y - center.y
|
|
const end = {
|
|
x: center.x + dx * Math.cos(radians) - dy * Math.sin(radians),
|
|
y: center.y + dx * Math.sin(radians) + dy * Math.cos(radians)
|
|
}
|
|
const move = async (point: Vector, steps = 1) =>
|
|
editor.page.mouse.move(
|
|
bounds.x + viewport.panX + point.x * viewport.zoom,
|
|
bounds.y + viewport.panY + point.y * viewport.zoom,
|
|
{ steps }
|
|
)
|
|
await move(start)
|
|
await editor.canvas.waitForRender()
|
|
const original = await editor.canvas.screenshotCanvasRegion()
|
|
await editor.page.mouse.down()
|
|
try {
|
|
await move(end, 8)
|
|
await editor.canvas.waitForRender()
|
|
const held = await editor.page.evaluate((nodeId) => {
|
|
const store = window.openPencil?.getStore?.()
|
|
return {
|
|
rotation: store?.graph.getNode(nodeId)?.rotation,
|
|
preview: store?.state.rotationPreview
|
|
}
|
|
}, id)
|
|
expect(held.rotation).toBe(data.frame.rotation)
|
|
const preview = expectDefined(held.preview, 'held rotation')
|
|
expect(preview.nodeId).toBe(id)
|
|
expect(Math.sign(preview.angle - data.frame.rotation)).toBe(flipX === flipY ? 1 : -1)
|
|
expect(await editor.canvas.screenshotCanvasRegion()).toMatchSnapshot(
|
|
`nested-transform-${flipX}-${flipY}-held.png`,
|
|
{ maxDiffPixelRatio: 0, threshold: 0 }
|
|
)
|
|
await editor.page.keyboard.press('Escape')
|
|
await editor.canvas.waitForRender()
|
|
expect(
|
|
await editor.page.evaluate(() => window.openPencil?.getStore?.().state.rotationPreview)
|
|
).toBeNull()
|
|
expect(
|
|
await editor.page.evaluate(() => [
|
|
...(window.openPencil?.getStore?.().state.selectedIds ?? [])
|
|
])
|
|
).toEqual([id])
|
|
await editor.page.mouse.up()
|
|
// Compare the restored scene with the pointer (and therefore hover) back in the same place.
|
|
await move(start)
|
|
await editor.canvas.waitForRender()
|
|
expect((await editor.canvas.screenshotCanvasRegion()).equals(original)).toBe(true)
|
|
await editor.page.mouse.down()
|
|
await move(end, 8)
|
|
const committedAngle = expectDefined(
|
|
await editor.page.evaluate(
|
|
() => window.openPencil?.getStore?.().state.rotationPreview?.angle
|
|
),
|
|
'preview angle'
|
|
)
|
|
await editor.page.mouse.up()
|
|
await editor.canvas.waitForRender()
|
|
expect(
|
|
await editor.page.evaluate(
|
|
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
|
|
id
|
|
)
|
|
).toBe(committedAngle)
|
|
await editor.page.evaluate(() => window.openPencil?.getStore?.().undoAction())
|
|
expect(
|
|
await editor.page.evaluate(
|
|
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
|
|
id
|
|
)
|
|
).toBe(data.frame.rotation)
|
|
await editor.page.evaluate(() => window.openPencil?.getStore?.().redoAction())
|
|
expect(
|
|
await editor.page.evaluate(
|
|
(nodeId) => window.openPencil?.getStore?.().graph.getNode(nodeId)?.rotation,
|
|
id
|
|
)
|
|
).toBe(committedAngle)
|
|
editor.canvas.assertNoErrors()
|
|
} finally {
|
|
await editor.page.mouse.up()
|
|
}
|
|
})
|
|
}
|
|
}
|