Choose readable opposite edges for rotated frame titles and size badges, sharing placement with hit testing. Render section titles as compact inset badges with contrast-aware borders and hover feedback.
105 lines
4 KiB
TypeScript
105 lines
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 {
|
|
frameLabelPlacement,
|
|
labelLocalPoint,
|
|
labelScreenMatrix
|
|
} 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: 6, y: -25 })
|
|
expect(section.maxTextWidth).toBe(48)
|
|
const nested = expectDefined(
|
|
labelLayout('section', 60, true, { width: 100, height: 14 }),
|
|
'nested section layout'
|
|
)
|
|
expect(nested.bounds).toEqual({ x: 6, y: 6, width: 54, height: 24 })
|
|
expect(nested.text).toEqual({ x: 12, y: 11 })
|
|
expect(nested.maxTextWidth).toBe(42)
|
|
expect(nested.fontWeight).toBe(600)
|
|
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 = frameLabelPlacement(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.x < 0 ||
|
|
nodeLocal.x > frame.width ||
|
|
nodeLocal.y < 0 ||
|
|
nodeLocal.y > frame.height
|
|
).toBe(true)
|
|
expect(transform.rotation).toBeGreaterThanOrEqual(-45)
|
|
expect(transform.rotation).toBeLessThan(45)
|
|
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()
|
|
})
|