Files
Imhotep/packages/imhotep-geometry/src/frames.ts
T

346 lines
9.1 KiB
TypeScript
Raw Normal View History

// 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 }
}