feat(view): 꺾인 코핑의 코핑 철근을 3D로 그린다

꺾인 코핑은 코핑 철근을 그리지 않았다. 다주형은 접힌 조각(pieces)을 각 구간 좌표로 그리고, T형은 같은
접기(coping-fold.ts)로 편 좌표 철근을 미터점에서 꺾어 그린다.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-10 17:12:55 +09:00
1 parent 6a15a13fc7
commit 8f808f453e
2 files changed
+59 -25

No files matched your search

+38 -25
View File
@@ -7,6 +7,7 @@ import { sideRowLevels } from '../core/coping-geom';
import { columnRebar, columnInputFor } from '../core/rebar/column';
import { castRebarLayout } from '../core/rebar/cast-layout';
import { multicolCopingBars } from '../core/rebar/multicol-coping';
import { copingFold, foldBar } from '../core/rebar/coping-fold';
import { barDepths, barsAcross } from '../core/rebar/bearing-block';
import { blockFramePoint } from '../core/geometry';
import { INTEGRATED_BLOCK } from '../core/integrated-block';
@@ -98,7 +99,6 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const Ms = m.stretches.map((_, k) => stretchMatrix(m, k));
const L = m.copingL;
const B = m.copingB;
const straight = m.kink === null;
/**
* A bar through several points as one tube, each bend rounded to its centre-line radius (수정 2026-10-23).
@@ -163,14 +163,27 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
byName.set(name, list);
};
// Coping (straight coping only; a bent coping is not drawn in 3D yet).
if (straight && m.columns.length > 1) {
// Coping. A bent coping draws each bar's pieces in their own stretch (coping-fold.ts, 2026-11-10).
if (m.columns.length > 1) {
for (const b of multicolCopingBars(m, mCoping)) {
if (/^C[1-8](-\d+)?$/.test(b.name)) addPath(b.name, 0, b.pts, b.diameterMm, COLOR.coping);
else for (let i = 1; i < b.pts.length; i++) addBar(b.name, 0, b.pts[i - 1], b.pts[i], b.diameterMm, COLOR.shear);
for (const p of b.pieces ?? [{ k: 0 as const, pts: b.pts }]) {
if (/^C[1-8](-\d+)?$/.test(b.name)) addPath(b.name, p.k, p.pts, b.diameterMm, COLOR.coping);
else for (let i = 1; i < p.pts.length; i++) addBar(b.name, p.k, p.pts[i - 1], p.pts[i], b.diameterMm, COLOR.shear);
}
}
}
if (straight && m.columns.length <= 1) {
if (m.columns.length <= 1) {
// T형 on a bent coping: the unfolded bars turn at their mitre point (the mitre shear set and the wedge are multi-column only).
const fold = copingFold(m);
const tBar = (name: string, k: number, a: [number, number, number], b: [number, number, number], dMm: number, color: number) => {
if (!fold) return addBar(name, k, a, b, dMm, color);
for (const p of foldBar(fold, [a, b]).pieces) for (let i = 1; i < p.pts.length; i++) addBar(name, p.k, p.pts[i - 1], p.pts[i], dMm, color);
};
const tPath = (name: string, k: number, pts: [number, number, number][], dMm: number, color: number) => {
if (!fold) return addPath(name, k, pts, dMm, color);
for (const p of foldBar(fold, pts).pieces) addPath(name, p.k, p.pts, dMm, color);
};
const w = widthLayout(B * 1000, m.copingRebar);
const xs = [w.yE, ...Array.from({ length: w.n + 1 }, (_, i) => w.yE + w.g + w.spacing * i), B * 1000 - w.yE].map((v) => mm(v) - B / 2);
const cover = 0.1;
@@ -189,8 +202,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
xs.forEach((n, j) => {
const name = `C${k + (j % 2 === 1 ? 0 : 3)}`;
if (sp) {
addPath(name, 0, [[xk, z - hookTop, n], [xk, z, n], [splitX + half, z, n]], NOMINAL_DIAMETER.H29, COLOR.coping);
addPath(name, 0, [[splitX - half, z, n], [L - xk, z, n], [L - xk, z - hookTop, n]], NOMINAL_DIAMETER.H29, COLOR.coping);
tPath(name, 0, [[xk, z - hookTop, n], [xk, z, n], [splitX + half, z, n]], NOMINAL_DIAMETER.H29, COLOR.coping);
tPath(name, 0, [[splitX - half, z, n], [L - xk, z, n], [L - xk, z - hookTop, n]], NOMINAL_DIAMETER.H29, COLOR.coping);
return;
}
const pts: [number, number, number][] = [
@@ -199,7 +212,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
[L - xk, z, n],
[L - xk, z - hookTop, n],
];
addPath(name, 0, pts, NOMINAL_DIAMETER.H29, COLOR.coping);
tPath(name, 0, pts, NOMINAL_DIAMETER.H29, COLOR.coping);
});
}
// Bottom bars C7 / C8 (코핑철근.md §5.2–5.3): they follow the sloped underside, 127 above it, with a vertical leg up
@@ -218,7 +231,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const right = left.map(([x, y, zz]): [number, number, number] => [L - x, y, zz]);
const name = j % 2 === 1 ? 'C7' : 'C8';
for (const pts of [left, right]) {
addPath(name, 0, pts, NOMINAL_DIAMETER.H29, COLOR.coping);
tPath(name, 0, pts, NOMINAL_DIAMETER.H29, COLOR.coping);
}
});
// Shear loops C9 / C10 / C11 and the gaps between columns: a closed U at each position.
@@ -244,12 +257,12 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
// The three 폐합 stack to the right, so no U meets another loop's U or 삿갓 (SHEAR_LOOP_STEP).
const xu = xm + mm(i * SHEAR_LOOP_STEP);
const xc = xu + mm(dShear);
addBar(name, 0, [xu, bot, a], [xu, bot, b], dShear, COLOR.shear);
addBar(name, 0, [xu, bot, a], [xu, top, a], dShear, COLOR.shear);
addBar(name, 0, [xu, bot, b], [xu, top, b], dShear, COLOR.shear);
addBar(name, 0, [xc, top, a], [xc, top, b], dShear, COLOR.shear);
addBar(name, 0, [xc, top, a], [xc, capBot, a], dShear, COLOR.shear);
addBar(name, 0, [xc, top, b], [xc, capBot, b], dShear, COLOR.shear);
tBar(name, 0, [xu, bot, a], [xu, bot, b], dShear, COLOR.shear);
tBar(name, 0, [xu, bot, a], [xu, top, a], dShear, COLOR.shear);
tBar(name, 0, [xu, bot, b], [xu, top, b], dShear, COLOR.shear);
tBar(name, 0, [xc, top, a], [xc, top, b], dShear, COLOR.shear);
tBar(name, 0, [xc, top, a], [xc, capBot, a], dShear, COLOR.shear);
tBar(name, 0, [xc, top, b], [xc, capBot, b], dShear, COLOR.shear);
}
}
}
@@ -262,14 +275,14 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
for (const [name, rows] of [['C12', side.c12], ['C13', side.c13]] as const) {
for (const r of rows) {
const xw = mm(r.xw);
addBar(name, 0, [xw, r.z, -sideFace], [L - xw, r.z, -sideFace], dSide, COLOR.shear);
addBar(name, 0, [xw, r.z, sideFace], [L - xw, r.z, sideFace], dSide, COLOR.shear);
tBar(name, 0, [xw, r.z, -sideFace], [L - xw, r.z, -sideFace], dSide, COLOR.shear);
tBar(name, 0, [xw, r.z, sideFace], [L - xw, r.z, sideFace], dSide, COLOR.shear);
const zh = r.z + mm(dSide);
for (const x0 of [xw, L - xw]) {
const leg = mm(SIDE_HOOK_LEG) * Math.sign(L / 2 - x0);
addBar(name, 0, [x0, zh, -sideFace], [x0, zh, sideFace], dSide, COLOR.shear);
addBar(name, 0, [x0, zh, -sideFace], [x0 + leg, zh, -sideFace], dSide, COLOR.shear);
addBar(name, 0, [x0, zh, sideFace], [x0 + leg, zh, sideFace], dSide, COLOR.shear);
tBar(name, 0, [x0, zh, -sideFace], [x0, zh, sideFace], dSide, COLOR.shear);
tBar(name, 0, [x0, zh, -sideFace], [x0 + leg, zh, -sideFace], dSide, COLOR.shear);
tBar(name, 0, [x0, zh, sideFace], [x0 + leg, zh, sideFace], dSide, COLOR.shear);
}
}
}
@@ -280,18 +293,18 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const c14Nb = B / 2 - mm(C14_INSET);
for (const b of c14Bars(L * 1000, topEl, [...side.c13, ...side.c12], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth), m.copingRebar.mainLayerGap / 1000)) {
const xm = mm(b.x);
addBar('C14', 0, [xm, b.z, c14Na], [xm, b.z, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
tBar('C14', 0, [xm, b.z, c14Na], [xm, b.z, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
// 연직 발 below the coping underside is left out (코핑철근.md §8).
if (b.legs) {
addBar('C14', 0, [xm, b.z - c14Hook, c14Na], [xm, b.z, c14Na], NOMINAL_DIAMETER.H19, COLOR.coping);
addBar('C14', 0, [xm, b.z, c14Nb], [xm, b.z - c14Hook, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
tBar('C14', 0, [xm, b.z - c14Hook, c14Na], [xm, b.z, c14Na], NOMINAL_DIAMETER.H19, COLOR.coping);
tBar('C14', 0, [xm, b.z, c14Nb], [xm, b.z - c14Hook, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
}
}
// C15 (⊓) and C16 (L형) on the model's own steps (결정 2026-11-10, src/core/rebar/step-bars.ts).
for (const b of tStepBars(m, mCoping).bars) {
const n = b.n;
for (let i = 1; i < b.pts.length; i++) {
addBar(b.name, 0, [mm(b.pts[i - 1][0]), b.pts[i - 1][1], n], [mm(b.pts[i][0]), b.pts[i][1], n], NOMINAL_DIAMETER.H19, COLOR.coping);
tBar(b.name, 0, [mm(b.pts[i - 1][0]), b.pts[i - 1][1], n], [mm(b.pts[i][0]), b.pts[i][1], n], NOMINAL_DIAMETER.H19, COLOR.coping);
}
}
}
+21
View File
@@ -8,6 +8,7 @@ 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';
import { buildRebar3D } from '../src/view/rebar3d';
// 산외교 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));
@@ -186,3 +187,23 @@ describe('꺾인 코핑 철근 접기 (B·C, 2026-11-10)', () => {
expect(multicolCopingInfo(straight).bars.every((b) => b.pieces === undefined)).toBe(true);
});
});
describe('꺾인 코핑 3D 철근 (E, 2026-11-10)', () => {
const straightOf = (input: PierInput) => ({ ...input, ips: input.ips.map((q) => (q.name === 'IP4' ? { ...q, angleDeg: 0 } : q)) });
const count = (rb: ReturnType<typeof buildRebar3D>, name: string) => rb.byName.get(name)?.length ?? 0;
it('a bent multi-column coping now draws its coping bars, runs across the kink in two pieces', () => {
const bent = buildRebar3D(buildPier(p11()));
for (const name of ['C1', 'C7', 'C9', 'C10', 'C12', 'C13', 'C15', 'C16']) expect(count(bent, name), name).toBeGreaterThan(0);
const straight = buildRebar3D(buildPier(straightOf(p11())));
expect(count(bent, 'C7')).toBeGreaterThan(count(straight, 'C7'));
}, 60000);
it('a bent T형 coping draws its coping bars through the same fold', () => {
const t = { ...p11(), type: 'T형' as const, columnCount: 1 };
const bent = buildRebar3D(buildPier(t));
const straight = buildRebar3D(buildPier(straightOf(t)));
expect(count(bent, 'C1')).toBeGreaterThan(0);
expect(count(bent, 'C1')).toBeGreaterThan(count(straight, 'C1'));
}, 60000);
});