diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index dec36ba..0d03e89 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -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); } } } diff --git a/tests/kink-steps.test.ts b/tests/kink-steps.test.ts index 3c2275e..109d882 100644 --- a/tests/kink-steps.test.ts +++ b/tests/kink-steps.test.ts @@ -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, 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); +});