// Mirror of ../matlab/triangle_geometry.m: the open-triangle centerline as a // polyline in the x-y plane, apex up, gap at the bottom-left corner, running // counterclockwise from the free end on the base (arclength fraction 0) to // the free end on the left leg (fraction 1). Used by the 3D scene and the // mode-shape plot; the physics reads the same geometry from the MATLAB side. export interface OutlinePoint { x: number; y: number; tx: number; // unit tangent along increasing arclength ty: number; } export interface Outline { pts: { x: number; y: number }[]; // A, V3, V1, B segLens: number[]; totalLen: number; at(frac: number): OutlinePoint; } export function triangleOutline(sideLength: number, gapM: number): Outline { const L = sideLength; const g = Math.min(gapM, 0.8 * L); const V1 = { x: 0, y: (L * Math.sqrt(3)) / 2 }; const V2 = { x: -L / 2, y: 0 }; const V3 = { x: L / 2, y: 0 }; const e1 = { x: 1, y: 0 }; const e3 = { x: (V2.x - V1.x) / L, y: (V2.y - V1.y) / L }; const A = { x: V2.x + (e1.x * g) / 2, y: V2.y + (e1.y * g) / 2 }; const B = { x: V2.x - (e3.x * g) / 2, y: V2.y - (e3.y * g) / 2 }; const pts = [A, V3, V1, B]; const segLens = [L - g / 2, L, L - g / 2]; const totalLen = segLens[0] + segLens[1] + segLens[2]; return { pts, segLens, totalLen, at(frac: number): OutlinePoint { let s = Math.min(0.999, Math.max(0.001, frac)) * totalLen; for (let k = 0; k < 3; k++) { if (s <= segLens[k] || k === 2) { const a = pts[k]; const b = pts[k + 1]; const t = Math.min(1, s / segLens[k]); const dx = b.x - a.x; const dy = b.y - a.y; const dl = Math.hypot(dx, dy); return { x: a.x + t * dx, y: a.y + t * dy, tx: dx / dl, ty: dy / dl, }; } s -= segLens[k]; } throw new Error('unreachable'); }, }; }