// Frame resolution and transform chains for Imhotep geometry world import { GeometryWorld, Frames, FrameKind, Matrices, WorldIndex, Rects, } from './world.js' import { IDENTITY_4X4, multiply4x4, transformPoint, invert4x4, buildTransformChain, readMatrix4x4, translateMatrix, } from './transforms.js' // --------------------------------------------------------------------------- // Frame tree navigation // --------------------------------------------------------------------------- /** * Get the parent frame ID for a frame at the given index. * Returns 0 if the frame has no parent (root). */ export function getParentFrameId(frames: Frames, index: number): number { return frames.parentFrameId[index] } /** * Walk from a frame up to the root, collecting frame indices in order. * The returned array is [frameIndex, parentIndex, grandparentIndex, ..., rootIndex]. */ export function walkFrameAncestors( frames: Frames, startIndex: number ): number[] { const path: number[] = [] let current = startIndex while (current >= 0 && current < frames.frameId.length) { path.push(current) const parentId = frames.parentFrameId[current] if (parentId === 0) break // Find parent index by linear scan (small N in practice, or use index) let found = -1 for (let i = 0; i < frames.frameId.length; i++) { if (frames.frameId[i] === parentId) { found = i break } } if (found === -1) break current = found } return path } // --------------------------------------------------------------------------- // Frame resolution by kind // --------------------------------------------------------------------------- /** * Resolve the viewport frame index. Returns -1 if not found. */ export function resolveViewportFrame(frames: Frames): number { for (let i = 0; i < frames.frameId.length; i++) { if (frames.frameKind[i] === FrameKind.Viewport) { return i } } return -1 } /** * Resolve the containing-block frame for a subject. * Uses the topology.containingBlockOf relation. */ export function resolveContainingBlockFrame( world: GeometryWorld, subjectId: number, index: WorldIndex ): number { const containingBlockSubjectId = world.topology.containingBlockOf[subjectId] if (containingBlockSubjectId === 0) { return resolveViewportFrame(world.frames) } // Find frame owned by that subject for (let i = 0; i < world.frames.frameId.length; i++) { if ( world.frames.ownerSubjectId[i] === containingBlockSubjectId && world.frames.frameKind[i] === FrameKind.ContainingBlock ) { return i } } return resolveViewportFrame(world.frames) } /** * Resolve the nearest positioned ancestor frame for a subject. */ export function resolveNearestPositionedAncestorFrame( world: GeometryWorld, subjectId: number ): number { const ancestorId = world.topology.nearestPositionedAncestorOf[subjectId] if (ancestorId === 0) { return resolveViewportFrame(world.frames) } for (let i = 0; i < world.frames.frameId.length; i++) { if ( world.frames.ownerSubjectId[i] === ancestorId && world.frames.frameKind[i] === FrameKind.NearestPositionedAncestor ) { return i } } return resolveViewportFrame(world.frames) } /** * Resolve the scroll-container frame for a subject. */ export function resolveScrollContainerFrame( world: GeometryWorld, subjectId: number ): number { const containerId = world.topology.scrollContainerOf[subjectId] if (containerId === 0) { return -1 } for (let i = 0; i < world.frames.frameId.length; i++) { if ( world.frames.ownerSubjectId[i] === containerId && world.frames.frameKind[i] === FrameKind.ScrollContainer ) { return i } } return -1 } // --------------------------------------------------------------------------- // Transform chain construction // --------------------------------------------------------------------------- /** * Collect the transform matrices along a frame's ancestor chain, * from root to leaf, so that multiplying them gives the total transform * from the root frame to the target frame. */ export function collectFrameTransformChain( world: GeometryWorld, frameIndex: number, index: WorldIndex ): Float64Array[] { const ancestorIndices = walkFrameAncestors(world.frames, frameIndex) // ancestorIndices is [leaf, ..., root]; reverse to get root -> leaf ancestorIndices.reverse() const matrices: Float64Array[] = [] for (const idx of ancestorIndices) { // Origin translation: frame origin in parent coordinates const originX = world.frames.originX[idx] const originY = world.frames.originY[idx] if (originX !== 0 || originY !== 0) { matrices.push(translateMatrix(originX, originY)) } const matrixStart = world.frames.axisMatrixStart[idx] if (matrixStart >= 0 && matrixStart < world.matrices.values.length) { const m = readMatrix4x4( world.matrices.values, matrixStart, new Float64Array(16) ) matrices.push(m) } } return matrices } /** * Build the total 4x4 transform matrix for a frame relative to the root. */ export function buildFrameTransform( world: GeometryWorld, frameIndex: number, index: WorldIndex ): Float64Array { const chain = collectFrameTransformChain(world, frameIndex, index) return buildTransformChain(chain) } /** * Build the transform that maps coordinates from frame A to frame B. * Returns a 4x4 matrix such that: * point_in_B = M * point_in_A * The matrix is inverse(B_to_root) * A_to_root. */ export function buildFrameToFrameTransform( world: GeometryWorld, fromFrameIndex: number, toFrameIndex: number, index: WorldIndex ): Float64Array | null { const fromTransform = buildFrameTransform(world, fromFrameIndex, index) const toTransform = buildFrameTransform(world, toFrameIndex, index) const invTo = invert4x4(toTransform) if (!invTo) return null return multiply4x4(invTo, fromTransform) } // --------------------------------------------------------------------------- // Coordinate conversion // --------------------------------------------------------------------------- /** * Convert a point from one frame to another. * Returns null if the transform is singular. */ export function convertPoint( world: GeometryWorld, fromFrameIndex: number, toFrameIndex: number, x: number, y: number, index: WorldIndex ): { x: number; y: number } | null { const m = buildFrameToFrameTransform( world, fromFrameIndex, toFrameIndex, index ) if (!m) return null const out = transformPoint(m, x, y) return { x: out[0], y: out[1] } } /** * Convert a rect from one frame to another. * Transforms all four corners and takes the bounding box. */ export function convertRect( world: GeometryWorld, fromFrameIndex: number, toFrameIndex: number, left: number, top: number, right: number, bottom: number, index: WorldIndex ): { left: number; top: number; right: number; bottom: number } | null { const m = buildFrameToFrameTransform( world, fromFrameIndex, toFrameIndex, index ) if (!m) return null const corners = [ { x: left, y: top }, { x: right, y: top }, { x: right, y: bottom }, { x: left, y: bottom }, ] let minX = Infinity, minY = Infinity let maxX = -Infinity, maxY = -Infinity const out = new Float64Array(2) for (const c of corners) { transformPoint(m, c.x, c.y, out) minX = Math.min(minX, out[0]) minY = Math.min(minY, out[1]) maxX = Math.max(maxX, out[0]) maxY = Math.max(maxY, out[1]) } return { left: minX, top: minY, right: maxX, bottom: maxY } } // --------------------------------------------------------------------------- // Frame origin helpers // --------------------------------------------------------------------------- /** * Get the origin of a frame in its parent frame. */ export function getFrameOrigin( frames: Frames, index: number ): { x: number; y: number } { return { x: frames.originX[index], y: frames.originY[index] } } /** * Resolve the full origin of a frame in the root (viewport) coordinate space. */ export function resolveFrameOriginInRoot( world: GeometryWorld, frameIndex: number, index: WorldIndex ): { x: number; y: number } { const ancestorIndices = walkFrameAncestors(world.frames, frameIndex) ancestorIndices.reverse() // root -> leaf let x = 0 let y = 0 const tmp = new Float64Array(2) // Start at root origin, accumulate transforms and origins for (let i = 0; i < ancestorIndices.length; i++) { const idx = ancestorIndices[i] x += world.frames.originX[idx] y += world.frames.originY[idx] if (i > 0) { // Apply previous frame's transform to the accumulated offset const prevIdx = ancestorIndices[i - 1] const matrixStart = world.frames.axisMatrixStart[prevIdx] if (matrixStart >= 0 && matrixStart < world.matrices.values.length) { const m = readMatrix4x4( world.matrices.values, matrixStart, new Float64Array(16) ) transformPoint(m, x, y, tmp) x = tmp[0] y = tmp[1] } } } return { x, y } }