편 좌표(g)로 계산한 다주형 코핑 철근을 꺾인 코핑에 맞춘다 (coping-fold.ts): - 교축 방향 철근은 그 n에서 미터면과 만나는 점에서 꺾은 한 개, 길이는 실제 경로 (바깥 길고 안쪽 짧음) - 교축 직각 철근(전단·크로스타이)은 미터 쐐기에 걸리면 빼고, 꺾임점에 가장 가까운 전단 위치 1조를 미터 방향으로 - 상면 1단 이음은 꺾임점에서 l_s/2 + 미터 퍼짐 밖으로 조각(pieces)은 구간별 국부 좌표로 담아 3D가 그대로 그릴 수 있게 한다. 곧은 코핑은 바뀌지 않는다. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
74 lines
4.2 KiB
TypeScript
74 lines
4.2 KiB
TypeScript
/**
|
||
* 꺾인 코핑의 철근 접기 (결정 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 };
|
||
}
|