openpencil/tests/engine/render/canvas/measurement.test.ts
Danila Poyarkov c2103ef722
feat(canvas): measure distances between layers (#515)
* feat(canvas): measure distances between layers

- Show temporary Figma-style guides while Option or Alt is held

- Support deep nested targets with Command or Control plus Alt

- Measure transformed world bounds and container padding

* fix(canvas): anchor diagonal measurement guides

- Join diagonal distances at the nearest object corners

- Separate horizontal and vertical value badges

- Crop visual coverage tightly around the measurement overlay

* fix(canvas): cover measurement edge cases

- Draw transformed target outlines and center value pills on guides

- Suppress conflicting hover overlays during measurement mode

- Cover containment, overlap, multi-selection, rotation, and fractional gaps

* fix(canvas): suppress conflicting measurement overlays

- Hide auto-layout hover visuals while distance measurements render

- Assert fractional measurement geometry with numeric tolerance
2026-08-14 18:18:55 +03:00

60 lines
2.4 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { computeMeasurementSegments } from '#core/canvas/overlays/measurement'
const rect = (x: number, y: number, width = 20, height = 20) => ({ x, y, width, height })
describe('distance measurement geometry', () => {
test('measures horizontal and vertical gaps between separated bounds', () => {
expect(computeMeasurementSegments(rect(0, 0), rect(30, 40))).toEqual([
{ axis: 'x', from: 20, to: 30, cross: 20, value: 10 },
{ axis: 'y', from: 20, to: 40, cross: 30, value: 20 }
])
})
test('anchors diagonal guides to the nearest corners', () => {
expect(computeMeasurementSegments(rect(120, 140, 100, 80), rect(300, 290, 120, 90))).toEqual([
{ axis: 'x', from: 220, to: 300, cross: 220, value: 80 },
{ axis: 'y', from: 220, to: 290, cross: 300, value: 70 }
])
})
test('measures from facing edges in either direction', () => {
expect(computeMeasurementSegments(rect(40, 50), rect(0, 10))).toEqual([
{ axis: 'x', from: 20, to: 40, cross: 50, value: 20 },
{ axis: 'y', from: 30, to: 50, cross: 20, value: 20 }
])
})
test('only measures the separated axis when bounds overlap', () => {
expect(computeMeasurementSegments(rect(0, 0), rect(30, 10))).toEqual([
{ axis: 'x', from: 20, to: 30, cross: 15, value: 10 }
])
})
test('measures all four edges for a child inside a container', () => {
expect(computeMeasurementSegments(rect(20, 30, 40, 50), rect(0, 0, 100, 120))).toEqual([
{ axis: 'x', from: 0, to: 20, cross: 55, value: 20 },
{ axis: 'x', from: 60, to: 100, cross: 55, value: 40 },
{ axis: 'y', from: 0, to: 30, cross: 40, value: 30 },
{ axis: 'y', from: 80, to: 120, cross: 40, value: 40 }
])
})
test('does not measure overlap on either axis', () => {
expect(computeMeasurementSegments(rect(0, 0, 40, 40), rect(20, 20, 40, 40))).toEqual([])
})
test('preserves fractional geometry for display rounding', () => {
const [segment] = computeMeasurementSegments(rect(0.2, 0, 10.1, 10), rect(20.8, 0, 10, 10))
expect(segment).toMatchObject({ axis: 'x', to: 20.8, cross: 5 })
expect(segment?.from).toBeCloseTo(10.3)
expect(segment?.value).toBeCloseTo(10.5)
})
test('omits zero gaps for touching and identical bounds', () => {
expect(computeMeasurementSegments(rect(0, 0), rect(20, 0))).toEqual([])
expect(computeMeasurementSegments(rect(0, 0), rect(0, 0))).toEqual([])
})
})