import { describe, expect, it } from 'vitest'; import { graphTextScreenBounds, graphTextTopLeftAnchor, moveGraphTextAnchor, resizeGraphTextFromBottomRight, setGraphTextAnchor, type GraphTextElement } from './text-elements'; import { mathToPixel, pixelToMath } from './viewport'; const viewport = { x_min: -10, x_max: 10, y_min: -10, y_max: 10, width: 800, height: 600 }; const text: GraphTextElement = { id: 'label-1', source: 'A point', position: { x: 0, y: 0 }, anchor: { horizontal: 'center', vertical: 'middle' }, widthPx: 240, heightPx: 96, fontSizePx: 16, color: null, align: 'left' }; describe('graph text geometry', () => { it('keeps the display box in CSS pixels as the camera zooms', () => { const wide = graphTextScreenBounds(text, viewport); const zoomed = graphTextScreenBounds(text, { ...viewport, x_min: -2, x_max: 2, y_min: -2, y_max: 2 }); expect([wide.width, wide.height]).toEqual([240, 96]); expect([zoomed.width, zoomed.height]).toEqual([240, 96]); expect([zoomed.left, zoomed.top]).toEqual([280, 252]); }); it('moves the graph-coordinate anchor under the grabbed screen point', () => { const initialAnchor = mathToPixel(viewport, text.position.x, text.position.y); const pointerStart = { x: initialAnchor.x + 30, y: initialAnchor.y - 8 }; const anchorOffset = { x: initialAnchor.x - pointerStart.x, y: initialAnchor.y - pointerStart.y }; const pointerEnd = { x: pointerStart.x + 160, y: pointerStart.y + 75 }; const movedAnchor = moveGraphTextAnchor(viewport, pointerEnd, anchorOffset); expect(movedAnchor).toEqual(pixelToMath(viewport, initialAnchor.x + 160, initialAnchor.y + 75)); }); it('keeps the screen box fixed when switching to another anchor point', () => { const before = graphTextScreenBounds(text, viewport); for (const vertical of ['top', 'middle', 'bottom'] as const) { for (const horizontal of ['left', 'center', 'right'] as const) { const reanchored = setGraphTextAnchor(text, { horizontal, vertical }, viewport); const after = graphTextScreenBounds(reanchored, viewport); const xFraction = horizontal === 'left' ? 0 : horizontal === 'right' ? 1 : 0.5; const yFraction = vertical === 'top' ? 0 : vertical === 'bottom' ? 1 : 0.5; const zoomed = { ...viewport, x_min: -2, x_max: 2, y_min: -2, y_max: 2 }; const zoomedBounds = graphTextScreenBounds(reanchored, zoomed); const zoomedAnchor = mathToPixel(zoomed, reanchored.position.x, reanchored.position.y); expect(after.left).toBeCloseTo(before.left); expect(after.top).toBeCloseTo(before.top); expect(reanchored.position).toEqual( pixelToMath(viewport, before.left + before.width * xFraction, before.top + before.height * yFraction) ); expect(zoomedBounds.left + zoomedBounds.width * xFraction).toBeCloseTo(zoomedAnchor.x); expect(zoomedBounds.top + zoomedBounds.height * yFraction).toBeCloseTo(zoomedAnchor.y); } } }); it('keeps the existing anchor when a change would exceed the supported coordinate range', () => { const wideViewport = { ...viewport, x_min: -1e12, x_max: 1e12, y_min: -1e12, y_max: 1e12 }; const nearLimit: GraphTextElement = { ...text, position: { x: 8e11, y: 0 }, anchor: { horizontal: 'left', vertical: 'middle' } }; expect(setGraphTextAnchor( nearLimit, { horizontal: 'right', vertical: 'middle' }, wideViewport )).toBe(nearLimit); }); it('resizes from the bottom-right while preserving the graph-coordinate top-left for each anchor', () => { for (const vertical of ['top', 'middle', 'bottom'] as const) { for (const horizontal of ['left', 'center', 'right'] as const) { const anchored = setGraphTextAnchor(text, { horizontal, vertical }, viewport); const fixedTopLeft = graphTextTopLeftAnchor(anchored, viewport); const startBounds = graphTextScreenBounds(anchored, viewport); const resized = resizeGraphTextFromBottomRight( anchored, fixedTopLeft, { x: startBounds.left + 310, y: startBounds.top + 145 }, viewport ); expect(resized.widthPx).toBe(310); expect(resized.heightPx).toBeCloseTo(145); expect(graphTextTopLeftAnchor(resized, viewport).x).toBeCloseTo(fixedTopLeft.x); expect(graphTextTopLeftAnchor(resized, viewport).y).toBeCloseTo(fixedTopLeft.y); } } }); });