v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user