/** * 꺾인 코핑의 철근 접기 (결정 2026-11-10). Pure. * * 코핑 철근은 코핑을 편 좌표 (g = 코핑 시작에서 축을 따라 잰 거리, EL, n)로 계산한다. 꺾인 코핑에서는: * - 교축 방향 철근은 꺾임점에서 꺾은 한 개: 그 철근의 n에서 미터면과 만나는 점에서 꺾는다. 길이는 실제 경로로 다시 잰다 * (편 길이 + a0(n) − a1(n), 바깥쪽이 길고 안쪽이 짧다). * - 교축 직각 철근 (전단 · 크로스타이)은 각 구간 축에 직각이다. 미터 쐐기에 걸리는 것은 빼고, 꺾임점에 전단철근 * 1조를 미터 방향으로 둔다 (가장 가까운 전단 위치의 모양 그대로). * 조각 좌표는 각 구간의 국부 좌표 (s, EL, n)이다: 구간 0은 s = g, 구간 1은 s = g − 꺾임점 g. */ import type { PierModel } from '../geometry'; export type FoldPoint = [number, number, number]; export interface FoldPiece { k: 0 | 1; pts: FoldPoint[] } export interface CopingFold { /** g of the kink (m). */ kinkG: number; /** Local s of the mitre at offset n, in stretch 0 (from the kink, + ahead) and stretch 1 (from its base). */ a0: (n: number) => number; a1: (n: number) => number; /** Largest |a| over the coping width (m): half the wedge the mitre sweeps. */ wedge: number; } export function copingFold(model: PierModel): CopingFold | null { if (!model.kink || !model.mitre || model.stretches.length < 2) return null; const m = model.mitre; const dot = (a: { e: number; n: number }, b: { e: number; n: number }) => a.e * b.e + a.n * b.n; const [s0, s1] = model.stretches; // A point (s, n) of stretch k is on the mitre when (s − s_kink)·(u·m) + n·(nrm·m) = 0. const r0 = dot(s0.nrm, m) / dot(s0.u, m); const r1 = dot(s1.nrm, m) / dot(s1.u, m); return { kinkG: s1.base, a0: (n) => -n * r0, a1: (n) => -n * r1, wedge: (model.copingB / 2) * Math.max(Math.abs(r0), Math.abs(r1)) }; } const pathLength = (pts: FoldPoint[]) => pts.slice(1).reduce((sum, p, i) => sum + Math.hypot(p[0] - pts[i][0], p[1] - pts[i][1], p[2] - pts[i][2]), 0); /** One bar from the unfolded g into stretch pieces; a run across the kink turns at its own mitre point. Length in mm. */ export function foldBar(f: CopingFold, pts: FoldPoint[]): { pieces: FoldPiece[]; length: number } { const side = (x: number): 0 | 1 => (x <= f.kinkG + 1e-12 ? 0 : 1); const local = (p: FoldPoint, k: 0 | 1): FoldPoint => [k === 0 ? p[0] : p[0] - f.kinkG, p[1], p[2]]; const pieces: FoldPiece[] = [{ k: side(pts[0][0]), pts: [local(pts[0], side(pts[0][0]))] }]; for (let i = 1; i < pts.length; i++) { const a = pts[i - 1], b = pts[i]; const ka = side(a[0]), kb = side(b[0]); if (ka !== kb) { const t = (f.kinkG - a[0]) / (b[0] - a[0]); const z = a[1] + t * (b[1] - a[1]), n = a[2] + t * (b[2] - a[2]); const m0: FoldPoint = [f.kinkG + f.a0(n), z, n], m1: FoldPoint = [f.a1(n), z, n]; // Both are the same world point on the mitre, seen from each stretch. if (ka === 0) { pieces[pieces.length - 1].pts.push(m0); pieces.push({ k: 1, pts: [m1] }); } else { pieces[pieces.length - 1].pts.push(m1); pieces.push({ k: 0, pts: [m0] }); } } pieces[pieces.length - 1].pts.push(local(b, kb)); } return { pieces, length: pieces.reduce((sum, p) => sum + pathLength(p.pts), 0) * 1000 }; } /** True for a bar across the width: every point at one g. */ export const isTransverse = (pts: FoldPoint[]) => pts.every((p) => Math.abs(p[0] - pts[0][0]) < 1e-9) && pts.some((p) => Math.abs(p[2] - pts[0][2]) > 1e-9); /** True when a transverse bar at g would cut the mitre plane somewhere over its width. */ export function inWedge(f: CopingFold, pts: FoldPoint[], margin = 0): boolean { const reach = Math.max(...pts.map((p) => Math.max(Math.abs(f.a0(p[2])), Math.abs(f.a1(p[2]))))); return Math.abs(pts[0][0] - f.kinkG) <= reach + margin + 1e-9; } /** A transverse bar laid on the mitre (stretch 0 coordinates): each point moves to the mitre at its n, plus `dx` along the axis. */ export function onMitre(f: CopingFold, pts: FoldPoint[], dx = 0): { pieces: FoldPiece[]; length: number } { const moved = pts.map((p): FoldPoint => [f.kinkG + f.a0(p[2]) + dx, p[1], p[2]]); return { pieces: [{ k: 0, pts: moved }], length: pathLength(moved) * 1000 }; }