openpencil/tests/engine/render/canvas/labels/transform.test.ts

117 lines
4.3 KiB
TypeScript
Raw Normal View History

import { expect, test } from 'bun:test'
import type { Font } from 'canvaskit-wasm'
import { SceneGraph } from '@open-pencil/scene-graph'
import { LabelCache } from '#core/canvas/labels/cache'
import { hitTestSectionTitle, hitTestFrameTitle } from '#core/canvas/labels/hit-test'
import { frameLabelPlacement, labelTransform } from '#core/canvas/labels/transform'
import { createSceneGeometry } from '#core/geometry'
import { expectDefined } from '#tests/helpers/assert'
function fixture() {
const graph = new SceneGraph()
const page = expectDefined(graph.getPages()[0], 'page')
const section = graph.createNode('SECTION', page.id, {
name: 'Section',
x: 100,
y: 100,
width: 200,
height: 100
})
const frame = graph.createNode('FRAME', section.id, {
name: 'Frame',
x: 20,
y: 30,
width: 80,
height: 40
})
return { graph, page, section, frame }
}
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('label origins and edge anchors include a live ancestor rotation without mutating nodes', () => {
const { graph, section, frame } = fixture()
const preview = { nodeId: section.id, angle: 90 }
const title = labelTransform(frame, graph, preview)
expect(title.x).toBeCloseTo(220)
expect(title.y).toBeCloseTo(70)
expect(title.rotation).toBe(90)
const bottom = labelTransform(frame, graph, preview, { x: frame.width / 2, y: frame.height })
expect(bottom.x).toBeCloseTo(180)
expect(bottom.y).toBeCloseTo(110)
expect(section.rotation).toBe(0)
expect(frame.rotation).toBe(0)
const bounds = createSceneGeometry(graph, preview).bounds(frame)
graph.updateNode(section.id, { rotation: 90 })
expect(labelTransform(frame, graph, null)).toEqual(title)
expect(createSceneGeometry(graph, null).bounds(frame)).toEqual(bounds)
})
for (const [angle, rotation, width, height] of [
[40, 40, 80, 40],
[50, -40, 40, 80],
[140, -40, 80, 40],
[-145, 35, 80, 40],
[180, 0, 80, 40]
] as const) {
test(`frame labels choose readable opposite edges at ${angle} degrees`, () => {
const { graph, frame } = fixture()
graph.updateNode(frame.id, { rotation: angle })
const title = frameLabelPlacement(frame, graph, null)
const size = frameLabelPlacement(frame, graph, null, { x: 0.5, y: 1 })
expect(title.rotation).toBeCloseTo(rotation, 9)
expect(title.width).toBe(width)
expect(title.height).toBe(height)
const radians = (rotation * Math.PI) / 180
const dx = size.x - title.x
const dy = size.y - title.y
expect(dx * Math.cos(radians) + dy * Math.sin(radians)).toBeCloseTo(width / 2, 9)
expect(-dx * Math.sin(radians) + dy * Math.cos(radians)).toBeCloseTo(height, 9)
})
}
test('readability thresholds use identical edges during preview and after commit', () => {
for (const angle of [-135, -45, 45, 135]) {
const { graph, frame } = fixture()
const preview = frameLabelPlacement(frame, graph, { nodeId: frame.id, angle })
graph.updateNode(frame.id, { rotation: angle })
expect(frameLabelPlacement(frame, graph, null)).toEqual(preview)
}
})
test('section culling uses preview world bounds rather than cached unrotated coordinates', () => {
const { graph, page, section } = fixture()
const cache = new LabelCache()
cache.update(graph, page.id, 1)
const viewport = { x: 215, y: 65, w: 10, h: 10 }
expect(cache.getSections(graph, viewport)).toHaveLength(0)
expect(
cache.getSections(graph, viewport, { nodeId: section.id, angle: 90 }).map(({ node }) => node.id)
).toEqual([section.id])
})
test('section and nested frame hit targets follow their rendered title transforms', () => {
const { graph, page, section, frame } = fixture()
graph.updateNode(section.id, { rotation: 90 })
const cache = new LabelCache()
cache.update(graph, page.id, 1)
// Section title local point (4, -10), transformed through its top-left (250, 50).
for (const catalog of [undefined, cache]) {
expect(hitTestSectionTitle(graph, 260, 54, 1, page.id, font, catalog)?.id).toBe(section.id)
}
// The readable frame title moves to the horizontal edge at (180, 70).
expect(hitTestFrameTitle(graph, 184, 60, 1, new Set([frame.id]), font)?.id).toBe(frame.id)
expect(hitTestFrameTitle(graph, 230, 74, 1, new Set([frame.id]), font)).toBeNull()
})