346 lines
9.1 KiB
TypeScript
346 lines
9.1 KiB
TypeScript
|
|
// 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 }
|
||
|
|
}
|