feat(rebar): 꺾인 코핑 철근을 꺾임에 맞춰 접는다 (교축 꺾은 철근 · 미터 전단 1조)

편 좌표(g)로 계산한 다주형 코핑 철근을 꺾인 코핑에 맞춘다 (coping-fold.ts):
- 교축 방향 철근은 그 n에서 미터면과 만나는 점에서 꺾은 한 개, 길이는 실제 경로 (바깥 길고 안쪽 짧음)
- 교축 직각 철근(전단·크로스타이)은 미터 쐐기에 걸리면 빼고, 꺾임점에 가장 가까운 전단 위치 1조를 미터 방향으로
- 상면 1단 이음은 꺾임점에서 l_s/2 + 미터 퍼짐 밖으로
조각(pieces)은 구간별 국부 좌표로 담아 3D가 그대로 그릴 수 있게 한다. 곧은 코핑은 바뀌지 않는다.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-10 17:09:27 +09:00
1 parent 51209cccf9
commit 6a15a13fc7
3 files changed
+145 -2

No files matched your search

+41
View File
@@ -7,6 +7,7 @@ import { buildPier, DIM, defaultLevelWidthMm } from '../src/core/geometry';
import type { PierInput } from '../src/core/types';
import { rebarSummary } from '../src/core/rebar/summary';
import { multicolCopingInfo } from '../src/core/rebar/multicol-coping';
import { copingFold, inWedge, isTransverse } from '../src/core/rebar/coping-fold';
// 산외교 P11 (샘플01) with the coping bent 8.767° at IP4 (2026-11-10 검토): back ⑥ g 11.64, ⑦ g 18.49, kink g 15.14.
const SAMPLE = fileURLToPath(new URL('../project_data/01_엑셀/샘플01_산외교_교각입력데이터.xlsx', import.meta.url));
@@ -145,3 +146,43 @@ describe('꺾인 다주형 코핑의 철근표 (A, 2026-11-10)', () => {
expect(rows.every((r) => r.note.includes('다주형'))).toBe(true);
});
});
describe('꺾인 코핑 철근 접기 (B·C, 2026-11-10)', () => {
const m = buildPier(p11());
const f = copingFold(m)!;
const bars = multicolCopingInfo(m).bars;
const unfolded = (pts: [number, number, number][]) => pts.slice(1).reduce((s, p, i) => s + Math.hypot(p[0] - pts[i][0], p[1] - pts[i][1], p[2] - pts[i][2]), 0) * 1000;
it('a run across the kink turns at its mitre point: length = unfolded + a0(n) − a1(n), outer longer, inner shorter', () => {
const crossing = bars.filter((b) => b.name === 'C7' && b.pieces!.length === 2);
expect(crossing.length).toBeGreaterThan(0);
for (const b of crossing) {
const n = b.pts[1][2];
expect(b.length).toBeCloseTo(unfolded(b.pts) + (f.a0(n) - f.a1(n)) * 1000, 6);
}
const byN = [...crossing].sort((a, b) => a.pts[1][2] - b.pts[1][2]);
const d = (b: (typeof crossing)[number]) => b.length - unfolded(b.pts);
expect(Math.sign(d(byN[0]))).toBe(-Math.sign(d(byN.at(-1)!)));
expect(Math.abs(d(byN[0]))).toBeGreaterThan(100);
});
it('no bar across the width cuts the mitre, except the one shear set laid on it', () => {
const transverse = bars.filter((b) => isTransverse(b.pts));
const onIt = transverse.filter((b) => b.pieces!.length === 1 && b.pieces![0].pts.every((p) => Math.abs(p[0] - (f.kinkG + f.a0(p[2]))) < 0.2));
expect(new Set(onIt.map((b) => b.name)).size).toBeGreaterThan(0);
for (const b of transverse.filter((x) => !onIt.includes(x))) expect(inWedge(f, b.pts)).toBe(false);
});
it('the top-bar laps keep l_s/2 + the wedge away from the kink', () => {
const c1 = bars.filter((b) => b.name === 'C1');
const ends = c1.flatMap((b) => [Math.min(...b.pts.map((p) => p[0])), Math.max(...b.pts.map((p) => p[0]))]);
const inner = ends.filter((x) => x > 0.2 && x < m.copingL - 0.2);
expect(inner.length).toBeGreaterThan(0);
for (const x of inner) expect(Math.abs(x - f.kinkG)).toBeGreaterThan(f.wedge - 1e-9);
});
it('a straight coping is unchanged (no pieces)', () => {
const straight = buildPier({ ...p11(), ips: p11().ips.map((q) => (q.name === 'IP4' ? { ...q, angleDeg: 0 } : q)) });
expect(multicolCopingInfo(straight).bars.every((b) => b.pieces === undefined)).toBe(true);
});
});