'use client' import { useAlignmentGuides } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { memo } from 'react' import { formatMeasurement } from '../editor/measurement-pill' import { useFloorplanRender } from './floorplan-render-context' /** * Figma-style alignment guides for the 2D floor plan. * * Subscribes to `useAlignmentGuides` — populated by * `FloorplanRegistryMoveOverlay` (Path 2) during a generic free-translate * drag. Each guide renders as a red line between the moving and matched * candidate anchors with small `×` end-caps. A distance pill is drawn at * the line's midpoint when the perpendicular gap is non-zero. * * Stroke widths and handle radii are scaled by `unitsPerPixel` so they * stay a constant size on screen no matter the zoom. Text labels are * counter-rotated by `sceneRotationDeg` so they read upright even when * the building rotation rotates the scene ``. * * Mounted inside the `data-floorplan-scene` group so coordinates match * world meters 1:1 with the rest of the floor plan. */ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() { const guides = useAlignmentGuides((s) => s.guides) const unit = useViewer((s) => s.unit) const ctx = useFloorplanRender() if (guides.length === 0) return null const upp = ctx?.unitsPerPixel ?? 0.01 const sceneRot = ctx?.sceneRotationDeg ?? 0 // Pixel-budgeted sizes converted to world meters so visuals stay // constant across zoom. Numbers picked to mirror Figma's snap chrome. const stroke = 1 * upp const xCapSize = 4 * upp const pillFontSize = 11 * upp const pillPadX = 5 * upp const pillPadY = 3 * upp const pillRadius = 3 * upp const pillOffset = 8 * upp const color = '#ef4444' // tailwind red-500 — matches Figma's snap red return ( {guides.map((guide, i) => { const { from, to, axis } = guide const midX = (from.x + to.x) / 2 const midZ = (from.z + to.z) / 2 const distMeters = guide.distance // Pill placed offset perpendicular to the guide's axis so it // doesn't sit on top of the line itself. For an X-axis guide // (vertical line) we offset along Z; for a Z-axis guide we // offset along X. const pillX = axis === 'x' ? midX + pillOffset : midX const pillZ = axis === 'z' ? midZ + pillOffset : midZ const distLabel = formatMeasurement(distMeters, unit) const charWidth = pillFontSize * 0.55 const pillWidth = distLabel.length * charWidth + pillPadX * 2 const pillHeight = pillFontSize + pillPadY * 2 return ( {distMeters > 1e-4 && ( // Counter-rotate the pill so it stays upright when the // scene `` is rotated by building rotation. SVG's // `transform` runs in the local coord system, so the // rotation pivots around the pill's center. {distLabel} )} ) })} ) }) function XCap({ color, size, stroke, x, y, }: { color: string size: number stroke: number x: number y: number }) { return ( ) }