// MADE BY HENRYHUY AKA NGUYEN HIEU HUY // Scene references const isosceles_triangle_instance = Scene.getItem("XZrpUSuh") as Frustum4; // Shared geometry helpers /** Tolerance used for all floating point side / area comparisons. */ const EPS = 1e-4; /** Euclidean distance between two points. */ const dist = (p1: Vector3, p2: Vector3): number => Math.hypot(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z); /** Midpoint between two points. */ const midpoint = (p1: Vector3, p2: Vector3): Vector3 => new Vector3((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, (p1.z + p2.z) / 2); /** Random RGB color, used whenever the caller doesn't supply one. */ const randomColor = (): Color => new Color(Math.random() * 255, Math.random() * 255, Math.random() * 255); /** * Builds one flat, zero-width triangular wedge at `pos` with the given * base length and height. This is the piece every *_create function stamps * out one or more of before orienting it. */ function createFlatTriangle(pos: Vector3, height: number, base: number, color?: Color): Frustum4 { const tri = isosceles_triangle_instance.copy(); tri.transform.position = pos; tri.height = height; tri.bottomLength = base; tri.bottomWidth = tri.topWidth = tri.topLength = 0; tri.color = color ?? randomColor(); return tri; } /** * Orients a flat triangle piece so its height axis points from `pos` toward * `target`. This is the exact math every *_create function repeated inline: * * 1. lookAt(to) - initial alignment along the base edge * 2. extract local +Z (height) axis - from the resulting quaternion * 3. angle between that and target - via dot product / acos * 4. rotateLocal around local Y - swings the height axis onto target * * `side` is the signed value that decides which way step 4 rotates - * positive swings one way, negative the other. Callers compute it * differently depending on the shape (see sideOf() below vs. obtuse_create). */ function orientTowardTarget(tri: Frustum4, pos: Vector3, to: Vector3, target: Vector3, height: number, side: number): void { tri.transform.lookAt(to); const { x: qx, y: qy, z: qz, w: qw } = tri.transform.rotation; const mp = new Vector3( pos.x + 2 * (qx * qz + qw * qy) * height, pos.y + 2 * (qy * qz - qw * qx) * height, pos.z + (1 - 2 * (qx * qx + qy * qy)) * height ); const ax = target.x - pos.x, ay = target.y - pos.y, az = target.z - pos.z; const cx = mp.x - pos.x, cy = mp.y - pos.y, cz = mp.z - pos.z; const denom = (Math.hypot(ax, ay, az) * Math.hypot(cx, cy, cz)) || 1; const angle = Math.acos(Math.max(-1, Math.min(1, (ax * cx + ay * cy + az * cz) / denom))); tri.transform.rotateLocal(new Vector3(0, 1, 0), side > 0 ? angle : -angle); } /** Signed 2D (XY) cross product used by isosceles/right/acute to pick rotation direction. */ const sideOf = (target: Vector3, to: Vector3, pos: Vector3): number => (target.x - pos.x) * (to.y - pos.y) - (target.y - pos.y) * (to.x - pos.x); /** Builds + orients one flat triangle piece in a single call. */ function placeTrianglePiece(pos: Vector3, to: Vector3, target: Vector3, base: number, height: number, color?: Color): Frustum4 { const tri = createFlatTriangle(pos, height, base, color); orientTowardTarget(tri, pos, to, target, height, sideOf(target, to, pos)); return tri; } // Triangle builders export function isosceles_create(A: Vector3, B: Vector3, C: Vector3, color?: Color): Frustum4 { const a = dist(B, C), b = dist(A, C), c = dist(A, B); // Identify the apex (vertex between the two equal sides) and the base. let base1: Vector3, base2: Vector3, apex: Vector3, baseLength: number; if (Math.abs(a - b) < EPS) { [base1, base2, apex, baseLength] = [A, B, C, c]; } else if (Math.abs(a - c) < EPS) { [base1, base2, apex, baseLength] = [A, C, B, b]; } else { [base1, base2, apex, baseLength] = [B, C, A, a]; } const pos = midpoint(base1, base2); return [placeTrianglePiece(pos, base1, apex, baseLength, dist(pos, apex), color)]; } export function right_create(A: Vector3, B: Vector3, C: Vector3, color?: Color): Frustum4[] { const a = dist(B, C), b = dist(A, C), c = dist(A, B); // Longest side is the hypotenuse; split along the median from each leg's // midpoint to the hypotenuse's midpoint. let m: Vector3, l: Vector3, r: Vector3; let lenl: number, lenr: number; let lo: Vector3, ro: Vector3; if (a >= b && a >= c) { m = midpoint(B, C); r = midpoint(A, C); l = midpoint(A, B); lenl = c; lenr = b; lo = A; ro = C; } else if (b >= a && b >= c) { r = midpoint(B, C); m = midpoint(A, C); l = midpoint(A, B); lenl = c; lenr = a; lo = A; ro = B; } else { l = midpoint(B, C); r = midpoint(A, C); m = midpoint(A, B); lenl = a; lenr = b; lo = B; ro = A; } return [ placeTrianglePiece(l, lo, m, lenl, dist(l, m), color), placeTrianglePiece(r, ro, m, lenr, dist(r, m), color), ]; } export function acute_create(A: Vector3, B: Vector3, C: Vector3, color?: Color): Frustum4[] { const a = dist(B, C), b = dist(A, C), c = dist(A, B); const a2 = a * a, b2 = b * b, c2 = c * c; // Circumcenter via barycentric weights (only valid while the triangle // stays acute - a weight goes negative once an angle passes 90°). const wA = a2 * (b2 + c2 - a2); const wB = b2 * (a2 + c2 - b2); const wC = c2 * (a2 + b2 - c2); const totalWeight = wA + wB + wC || 1; // guard divide-by-zero on degenerate input const o = new Vector3( (wA * A.x + wB * B.x + wC * C.x) / totalWeight, (wA * A.y + wB * B.y + wC * C.y) / totalWeight, (wA * A.z + wB * B.z + wC * C.z) / totalWeight ); const midAB = midpoint(A, B), midBC = midpoint(B, C), midAC = midpoint(A, C); return [ placeTrianglePiece(midAB, A, o, c, dist(midAB, o), color), placeTrianglePiece(midBC, B, o, a, dist(midBC, o), color), placeTrianglePiece(midAC, C, o, b, dist(midAC, o), color), ]; } export function obtuse_create(A: Vector3, B: Vector3, C: Vector3, color?: Color): Frustum4[] { // Find the vertex/opposite-side pairing with the smallest foot-to-peak // distance - this identifies the obtuse angle and its opposite side. const best = ([[A, B, C], [C, A, B], [B, C, A]] as [Vector3, Vector3, Vector3][]) .map(([p1, p2, peak]) => { const dx = p2.x - p1.x, dy = p2.y - p1.y, dz = p2.z - p1.z; const lenSq = dx * dx + dy * dy + dz * dz; const t = lenSq ? ((peak.x - p1.x) * dx + (peak.y - p1.y) * dy + (peak.z - p1.z) * dz) / lenSq : 0; const hp = new Vector3(p1.x + t * dx, p1.y + t * dy, p1.z + t * dz); return { hp, p1, p2, peak }; }) .reduce((min, cur) => dist(cur.hp, cur.peak) < dist(min.hp, min.peak) ? cur : min); const bp = best.hp, up = best.peak; // Which side of the base line the (arbitrary) p1 reference sits on - // used only to pick a consistent left/right split. const crossY = (up.x - bp.x) * (best.p1.z - bp.z) - (up.z - bp.z) * (best.p1.x - bp.x); const lp = crossY > 0 ? best.p2 : best.p1; const rp = crossY > 0 ? best.p1 : best.p2; const mub = midpoint(up, bp), mru = midpoint(rp, up), mlu = midpoint(lp, up); const mrb = midpoint(rp, bp), mlb = midpoint(lp, bp); const pieces: [Vector3, Vector3, Vector3, number, number][] = [ [mub, up, mru, dist(up, bp), dist(mub, mru)], [mub, up, mlu, dist(up, bp), dist(mub, mlu)], [mrb, bp, mru, dist(rp, bp), dist(mrb, mru)], [mlb, bp, mlu, dist(lp, bp), dist(mlb, mlu)], ]; return pieces.map(([pos, to, target, base, height]) => { const tri = createFlatTriangle(pos, height, base, color); // Obtuse pieces reference the fixed up/bp axis (rather than each // piece's own pos/to) to keep the split consistent, so the sign // convention is inverted relative to the other *_create functions. const side = -((up.x - bp.x) * (target.y - bp.y) - (up.y - bp.y) * (target.x - bp.x)); orientTowardTarget(tri, pos, to, target, height, side); return tri; }); } // Classification + single entry-point selector export type TriangleClassification = "isosceles" | "right" | "acute" | "obtuse" | "degenerate"; /** * Classifies a triangle from 3 points as isosceles (2+ equal sides), right, * acute, obtuse, or degenerate (collinear / coincident points). * * Isosceles is checked first: it also covers equilateral triangles and any * right/acute/obtuse triangle that happens to have two equal sides, matching * the fact that isosceles_create only needs two equal sides to work, and * produces a single (cheaper) piece rather than 2-4. */ export function classifyTriangle(A: Vector3, B: Vector3, C: Vector3): TriangleClassification { const a = dist(B, C), b = dist(A, C), c = dist(A, B); if (a < EPS || b < EPS || c < EPS) return "degenerate"; // Collinearity check via twice the triangle's area (cross product magnitude). const ux = B.x - A.x, uy = B.y - A.y, uz = B.z - A.z; const vx = C.x - A.x, vy = C.y - A.y, vz = C.z - A.z; const area2 = Math.hypot(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx); if (area2 < EPS) return "degenerate"; if (Math.abs(a - b) < EPS || Math.abs(b - c) < EPS || Math.abs(a - c) < EPS) return "isosceles"; const [s1, s2, s3] = [a, b, c].sort((x, y) => x - y); // s3 = longest side (candidate hypotenuse) const lhs = s3 * s3, rhs = s1 * s1 + s2 * s2; const tolerance = Math.max(EPS, EPS * rhs); // scale tolerance to the triangle's size if (Math.abs(lhs - rhs) < tolerance) return "right"; return lhs < rhs ? "acute" : "obtuse"; } /** * Single entry point: given any 3 points (+ optional color), figures out * what kind of triangle they form and calls the matching builder. */ export function triangle_create(A: Vector3, B: Vector3, C: Vector3, color?: Color): Frustum4 | Frustum4[] | null { switch (classifyTriangle(A, B, C)) { case "degenerate": Debug.log("triangle_create: points are collinear or coincident - skipped."); return null; case "isosceles": return isosceles_create(A, B, C, color); case "right": return right_create(A, B, C, color); case "acute": return acute_create(A, B, C, color); case "obtuse": return obtuse_create(A, B, C, color); } }