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.
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
import { expect, test } from 'bun:test'
|
|
|
|
import type { Font } from 'canvaskit-wasm'
|
|
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
import Matrix from '@open-pencil/scene-graph/matrix'
|
|
|
|
import { hitTestComponentLabel, hitTestFrameTitle } from '#core/canvas/labels/hit-test'
|
|
import { labelLayout } from '#core/canvas/labels/layout'
|
|
import { labelLocalPoint, labelScreenMatrix, labelTransform } from '#core/canvas/labels/transform'
|
|
import { createSceneGeometry } from '#core/geometry'
|
|
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
import { nestedGeometryFixture } from '#tests/helpers/geometry'
|
|
|
|
const font = {
|
|
getGlyphIDs(text: string) {
|
|
return Uint16Array.from([...text].map((_, i) => i))
|
|
},
|
|
getGlyphWidths(ids: Uint16Array) {
|
|
return new Float32Array(ids.length).fill(8)
|
|
}
|
|
} as Font
|
|
|
|
test('layout bounds include clipped text, padding and the entire component icon', () => {
|
|
const section = expectDefined(
|
|
labelLayout('section', 60, false, { width: 100, height: 14 }),
|
|
'section layout'
|
|
)
|
|
expect(section.bounds).toEqual({ x: 0, y: -30, width: 60, height: 24 })
|
|
expect(section.text).toEqual({ x: 8, y: -25 })
|
|
expect(section.maxTextWidth).toBe(44)
|
|
const component = expectDefined(
|
|
labelLayout('component', 80, false, { width: 100, height: 11 }),
|
|
'component layout'
|
|
)
|
|
const icon = expectDefined(component.icon, 'icon')
|
|
expect(component.bounds.width).toBe(80)
|
|
expect(component.bounds.y + component.bounds.height).toBeGreaterThanOrEqual(icon.y + icon.height)
|
|
expect(labelLayout('component', 10)).toBeNull()
|
|
})
|
|
|
|
for (const flipX of [false, true]) {
|
|
for (const flipY of [false, true]) {
|
|
test(`label placement stays outside reflected bounds and shares its inverse (flipX=${flipX}, flipY=${flipY})`, () => {
|
|
const graph = new SceneGraph()
|
|
const page = expectDefined(graph.getPages()[0], 'page')
|
|
const { section, frame } = nestedGeometryFixture(graph, page.id, flipX, flipY)
|
|
const preview = { nodeId: section.id, angle: 70 }
|
|
const transform = labelTransform(frame, graph, preview)
|
|
const screen = Matrix.mapPoint(labelScreenMatrix(transform, { panX: 0, panY: 0, zoom: 2 }), {
|
|
x: 4,
|
|
y: -10
|
|
})
|
|
const world = { x: screen.x / 2, y: screen.y / 2 }
|
|
const local = expectDefined(labelLocalPoint(transform, 2, world), 'label-local point')
|
|
expect(local.x).toBeCloseTo(4, 9)
|
|
expect(local.y).toBeCloseTo(-10, 9)
|
|
const nodeLocal = expectDefined(
|
|
createSceneGeometry(graph, preview).toLocal(frame, world),
|
|
'node-local point'
|
|
)
|
|
expect(nodeLocal.y < 0 || nodeLocal.y > frame.height).toBe(true)
|
|
expect(
|
|
hitTestFrameTitle(graph, world.x, world.y, 2, new Set([frame.id]), font, { preview })?.id
|
|
).toBe(frame.id)
|
|
})
|
|
}
|
|
}
|
|
|
|
test('hidden component labels and the clipped-away part of long titles are not hit targets', () => {
|
|
const graph = new SceneGraph()
|
|
const page = expectDefined(graph.getPages()[0], 'page')
|
|
const frame = graph.createNode('FRAME', page.id, {
|
|
name: 'A very long title that is clipped',
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 100
|
|
})
|
|
expect(hitTestFrameTitle(graph, 140, 90, 1, new Set([frame.id]), font)?.id).toBe(frame.id)
|
|
expect(hitTestFrameTitle(graph, 170, 90, 1, new Set([frame.id]), font)).toBeNull()
|
|
graph.createNode('COMPONENT', frame.id, { name: 'Not drawn here', width: 80, height: 40 })
|
|
expect(hitTestComponentLabel(graph, 104, 90, 1, page.id, font)).toBeNull()
|
|
})
|