import * as THREE from 'three'; import type { PierModel } from '../core/geometry'; import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, FOOTING_DEFAULTS, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing'; import { bottomPiece, MAIN, widthLayout } from '../core/rebar/coping-main'; import { C14_INSET, c14Bars, CAP, COPING_REBAR, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping'; import { columnRebar, COLUMN_P1 } from '../core/rebar/column'; import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block'; import { DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common'; import { stretchMatrix } from './scene'; /** * 3D rebar of the ③ screen. Positions come from the rebar modules (no new rules here). Each bar is a cylinder * named by its 철근표 number (C1, P3, B5, S2 …), so the table and the model can point at each other. * Local frame: x = along the axis s, y = absolute EL (m), z = across n. Stretch matrices map it to the scene. */ export interface RebarScene { group: THREE.Group; byName: Map; /** Highlight one bar name (or none): the others are dimmed. */ highlight(name: string | null): void; } const COLOR = { coping: 0xb4531f, shear: 0x6b3fa0, column: 0x0f5c66, tie: 0x6b3fa0, couplerMain: 0x0b3f46, couplerTie: 0x553088, footing: 0x0f5c66, footingBottom: 0x2e7d32, block: 0xa07a10, } as const; const mm = (v: number) => v / 1000; export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, byMember: FckByMember = {}): RebarScene { const mCoping = materialFor(common, byMember, 'coping'); const mColumn = materialFor(common, byMember, 'column'); const group = new THREE.Group(); const byName = new Map(); const Ms = m.stretches.map((_, k) => stretchMatrix(m, k)); const L = m.copingL; const B = m.copingB; const straight = m.kink === null; const addBar = (name: string, k: number, a: [number, number, number], b: [number, number, number], dMm: number, color: number) => { const dir = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]); const len = dir.length(); if (len < 1e-6) return; const geo = new THREE.CylinderGeometry(mm(dMm) / 2, mm(dMm) / 2, len, 6); geo.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize())); geo.translate((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2); geo.applyMatrix4(Ms[k]); const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness: 0.1 }); const mesh = new THREE.Mesh(geo, mat); mesh.userData.name = name; group.add(mesh); const list = byName.get(name) ?? []; list.push(mesh); byName.set(name, list); }; // Coping (straight coping only; a bent coping is not drawn in 3D yet). if (straight) { const w = widthLayout(B * 1000); const xs = [w.yE, ...Array.from({ length: w.n + 1 }, (_, i) => w.yE + w.g + 125 * i), B * 1000 - w.yE].map((v) => mm(v) - B / 2); const cover = 0.1; // Top rows C1–C6 (코핑철근.md §5.1, §5.3): counted from the left, the even (1-based) positions are cycle 1 (C1–C3), // the odd cycle 2 (C4–C6). In 0-based j that is odd j → cycle 1. Each row turns down 472 at both ends, and the // k-th row's ends sit (k − 1)·57.2 further in. const topEl = m.copingRefTop; const hookTop = MAIN.hook / 1000; for (let k = 1; k <= 3; k++) { const z = topEl - 0.127 - (k - 1) * 0.057; const xk = cover + ((k - 1) * MAIN.layHorizontal) / 1000; xs.forEach((n, j) => { const name = `C${k + (j % 2 === 1 ? 0 : 3)}`; const pts: [number, number, number][] = [ [xk, z - hookTop, n], [xk, z, n], [L - xk, z, n], [L - xk, z - hookTop, n], ]; for (let s = 0; s + 1 < pts.length; s++) addBar(name, 0, pts[s], pts[s + 1], 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 // at each end to the C1 level. Each bar is two pieces spliced at the column centre (overlap l_s). The slope runs x_L0 from each end (T형). const xL0 = L / 2 - COPING_REBAR.levelWidth / 2000; const under = (x: number) => undersideAt(L * 1000, x * 1000, m.endBottom, m.levelBottom); const lap = bottomPiece(mCoping).ls / 2000; xs.forEach((n, j) => { const z = (x: number) => under(x) + 0.127; const left: [number, number, number][] = [ [cover, topEl - 0.127, n], [cover, z(cover), n], [xL0, z(xL0), n], [L / 2 + lap, z(L / 2 + lap), n], ]; 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]) { for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], NOMINAL_DIAMETER.H29, COLOR.coping); } }); // Shear loops C9 / C10 / C11 and the gaps between columns: a closed U at each position. const colS = m.columns.map((c) => c.s * 1000); const sh = colS.length > 1 ? shearBarsMulti(L * 1000, colS) : { ...shearBars(L * 1000), between: [] as number[] }; const loops: [string, number[]][] = [['C9', sh.c9], ['C10', sh.c10], ['C11', sh.c11], ['C9·C11 사이', sh.between]]; // Each position has three 폐합 (코핑철근.md §4.1): legs at y_i = c + i·s_leg (i = 0 … 5), 폐합 i = legs i and i + 3, // 1740 wide. Each 폐합 is a 하부 U (base at the bottom, legs up to the top) and a 상부 삿갓 (top bar, legs down // l_cap = h − c) that sits 25.4 beside the U along the coping. const top = topEl - 0.1; const dShear = NOMINAL_DIAMETER.H25; const sLeg = (B * 1000 - 2 * CAP.cover) / 5 / 1000; const legN = (i: number) => -B / 2 + 0.1 + i * sLeg; for (const [name, list] of loops) { for (const x of list) { const xm = mm(x); const h = shearUHeight(x, L * 1000) / 1000; const bot = top - h; const capBot = top - (h - CAP.cover / 1000); const xc = xm + mm(NOMINAL_DIAMETER.S25); for (let i = 0; i < 3; i++) { const a = legN(i); const b = legN(i + 3); addBar(name, 0, [xm, bot, a], [xm, bot, b], dShear, COLOR.shear); addBar(name, 0, [xm, bot, a], [xm, top, a], dShear, COLOR.shear); addBar(name, 0, [xm, bot, b], [xm, 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); } } } // Side horizontal bars C12 / C13 (코핑철근.md §6.2): per row, two 일자 at the side faces from x_w to L − x_w, // and two ㄷ bars: a body across the width at x_w, 25.4 above the row, with legs 1525 inward along the faces. const sideFace = B / 2 - 0.127; const dSide = NOMINAL_DIAMETER.H25; const side = copingSideBars(L * 1000, 41.288, 42.538); 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); 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); } } } // C14 ⊓ (코핑철근.md §8): across the width at each x, both ends down 15.5·d_b. const c14Hook = mm(hookLength('main90', NOMINAL_DIAMETER.H19)); const c14Na = -B / 2 + mm(C14_INSET); const c14Nb = B / 2 - mm(C14_INSET); for (const b of c14Bars(L * 1000, topEl, [...side.c12, ...side.c13], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom))) { const xm = mm(b.x); addBar('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); } } // C15 (거꾸로 된 U) and C16 (L형) on the stair (코핑철근.md §7): 11 bars each across the width. const stairs = stairBars(L * 1000, B * 1000, topEl, mCoping); for (const s of stairs.c15) { const n = mm(s.n); const ld = mm(s.leg); const x0 = mm(s.x0); const x1 = mm(s.x1); addBar('C15', 0, [x0, s.z - ld, n], [x0, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping); addBar('C15', 0, [x0, s.z, n], [x1, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping); addBar('C15', 0, [x1, s.z, n], [x1, s.z - ld, n], NOMINAL_DIAMETER.H19, COLOR.coping); } for (const s of stairs.c16) { const n = mm(s.n); const ld = mm(s.leg); const x0 = mm(s.x0); const x1 = mm(s.x1); addBar('C16', 0, [x0, s.z - ld, n], [x0, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping); addBar('C16', 0, [x0, s.z, n], [x1, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping); } } // Columns: P1 / P2 main bars on their radii, ties P3 / P5 / P4 as rings, P6 crossties across the column. const col = columnRebar(COLUMN_P1, mColumn); const zBot = col.zBot; // mm const ring = (name: string, k: number, sc: number, y: number, r: number, d: number) => { const N = 36; for (let i = 0; i < N; i++) { const a0 = (2 * Math.PI * i) / N; const a1 = (2 * Math.PI * (i + 1)) / N; addBar(name, k, [sc + r * Math.cos(a0), y, r * Math.sin(a0)], [sc + r * Math.cos(a1), y, r * Math.sin(a1)], d, COLOR.tie); } }; for (const c of m.columns) { const sc = c.s; const base = c.bottom; const rt = mm(col.tieRadius); for (let i = 0; i < COLUMN_P1.nDiv; i++) { const th = (2 * Math.PI * i) / COLUMN_P1.nDiv; const r1 = mm(col.r1); addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + r1 * Math.cos(th), base + mm(col.zTop1), r1 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column); // 90° bottom hook of P1, outward (기둥철근.md §3.2, Q-C5): the 12·d tail is drawn, the bend is approximated by the tail's start. const tail = mm(12 * NOMINAL_DIAMETER.S32); const rOut = r1 + tail; addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + rOut * Math.cos(th), base + mm(zBot), rOut * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column); const r2 = mm(col.r2); addBar('P2', c.k, [sc + r2 * Math.cos(th), base + mm(zBot), r2 * Math.sin(th)], [sc + r2 * Math.cos(th), base + mm(col.zTop2), r2 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column); } for (const z of col.p3) ring('P3', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19); for (const z of col.p5) ring('P5', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19); for (const z of col.p4) ring('P4', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19); // Main bar couplers (D32, L150 Ø60, 4단 × 46줄 × 2열 = 368개): P1 at col.couplerP1, P2 at col.couplerP2 (+150 mm) const r1 = mm(col.r1); const r2 = mm(col.r2); for (let i = 0; i < COLUMN_P1.nDiv; i++) { const th = (2 * Math.PI * i) / COLUMN_P1.nDiv; const cosTh = Math.cos(th); const sinTh = Math.sin(th); // P1 couplers (184) for (const z of col.couplerP1) { const cx = sc + r1 * cosTh; const cz = r1 * sinTh; const cy = base + mm(z); addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain); } // P2 couplers (184, shifted +150 mm) for (const z of col.couplerP2) { const cx = sc + r2 * cosTh; const cz = r2 * sinTh; const cy = base + mm(z); addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain); } } // Tie couplers (D19, L100 Ø40, 총 35개): P5 1개 (19개) · P4 2개 (16개) col.p5.forEach((z, j) => { const th = (2 * Math.PI * (j * 7 + 0.5)) / COLUMN_P1.nDiv; const cx = sc + rt * Math.cos(th); const cz = rt * Math.sin(th); const cy = base + mm(z); const dx = 0.05 * -Math.sin(th); const dz = 0.05 * Math.cos(th); addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie); }); col.p4.forEach((z) => { const angles = [(2 * Math.PI * 0.5) / COLUMN_P1.nDiv, (2 * Math.PI * 0.5) / COLUMN_P1.nDiv + Math.PI]; angles.forEach((th) => { const cx = sc + rt * Math.cos(th); const cz = rt * Math.sin(th); const cy = base + mm(z); const dx = 0.05 * -Math.sin(th); const dz = 0.05 * Math.cos(th); addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie); }); }); const hookP6 = mm(Math.round(hookLength('tie90', NOMINAL_DIAMETER.H16))); // 135 mm const dP6 = NOMINAL_DIAMETER.H16; // 15.9 mm const p6Angles = [0, 46.96, 90, 133.04]; for (const z of col.p6Levels) { p6Angles.forEach((deg, i) => { const a = (deg * Math.PI) / 180; const xA = sc - rt * Math.cos(a); const zA = -rt * Math.sin(a); const xB = sc + rt * Math.cos(a); const zB = rt * Math.sin(a); // Vertical stacking offset: ±1.5 db, ±0.5 db to avoid interference at the column centre (Q-C15) const zOffset = (i - 1.5) * dP6; const yBar = base + mm(z + zOffset); // Main crosstie across the diameter addBar('P6', c.k, [xA, yBar, zA], [xB, yBar, zB], dP6, COLOR.tie); // 90° hooks at both ends turning downward (135 mm, wrapping the tie) addBar('P6', c.k, [xA, yBar, zA], [xA, yBar - hookP6, zA], dP6, COLOR.tie); addBar('P6', c.k, [xB, yBar, zB], [xB, yBar - hookP6, zB], dP6, COLOR.tie); }); } } // Footing bars B1–B8 (교각축 · 교각축 직각), the grids from §3 and §6 of 확대기초철근 (1).md. for (const f of m.footings) { const Lf = f.length; const Wf = f.width; const Hf = (f.top - f.bottom) * 1000; const z = layers(Hf); const gW = grid(Wf * 1000); const gL = grid(Lf * 1000); const posW = (j: number) => -Wf / 2 + mm(100 + gW.e + j * 125); const posL = (i: number) => -Lf / 2 + mm(100 + gL.e + i * 125); const sA = -Lf / 2 + 0.1; const sB = Lf / 2 - 0.1; const nA = -Wf / 2 + 0.1; const nB = Wf / 2 - 0.1; const bottom = f.bottom; const off = legOffsets(); // One name, several cylinders: each segment of the path is one bar piece. const path = (name: string, pts: [number, number, number][], dMm: number) => { for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], dMm, COLOR.footing); }; // B1–B4: along s, at W position n. legTo (absolute EL, m) is where both ends turn up or down (§5.1); undefined = straight. const along = (name: string, n: number, y: number, legTo: number | undefined, dMm: number) => { const at = (t: number, h: number): [number, number, number] => [f.s + t, h, n]; path(name, legTo === undefined ? [at(sA, y), at(sB, y)] : [at(sA, legTo), at(sA, y), at(sB, y), at(sB, legTo)], dMm); }; // B5–B8: across, at L position s. Same rule, with the ends on the W faces (§6.2). const across = (name: string, s: number, y: number, legTo: number | undefined, dMm: number) => { const at = (t: number, h: number): [number, number, number] => [f.s + s, h, t]; path(name, legTo === undefined ? [at(nA, y), at(nB, y)] : [at(nA, legTo), at(nA, y), at(nB, y), at(nB, legTo)], dMm); }; for (let j = 0; j <= gW.n; j++) { const n = posW(j); if (j % 2 === 1) { // B1 ⊓ down, B2 ⊔ up to the B1 height, B3 ⊔ up. B1·B3 sit beside the B2 leg (+25.4 / +22.25, §5.3). along('B1', n + mm(off.B1), bottom + mm(z.B1), bottom + mm(z.B1) - mm(legLength('S25')), NOMINAL_DIAMETER.S25); along('B2', n, bottom + mm(z.B24), bottom + mm(z.B1), NOMINAL_DIAMETER.S25); along('B3', n + mm(off.B3), bottom + mm(z.B3), bottom + mm(z.B3) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); } else { along('B4', n, bottom + mm(z.B24), undefined, NOMINAL_DIAMETER.S25); } } for (let i = 0; i <= gL.n; i++) { const s = posL(i); if (i % 2 === 1) { // B5 ⊓ down, B6 ⊔ up to the B5 height, B7 ⊔ up. B5·B7 sit beside the B6 leg (+28.6 / +23.85, §6.3). across('B5', s + mm(off.B5), bottom + mm(z.B5), bottom + mm(z.B5) - mm(legLength('S29')), NOMINAL_DIAMETER.S29); across('B6', s, bottom + mm(z.B68), bottom + mm(z.B5), NOMINAL_DIAMETER.S29); across('B7', s + mm(off.B7), bottom + mm(z.B7), bottom + mm(z.B7) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); } else { across('B8', s, bottom + mm(z.B68), undefined, NOMINAL_DIAMETER.S29); } } // B9 (확대기초철근 (1).md §9, rule applied): vertical H16 bars on the grid, outside the column zones, with 135° hooks. // Each hook tail (6·d) turns toward the footing centre along the pier axis (Q-B19). const dB9 = NOMINAL_DIAMETER.H16; const tail9 = (6 * dB9) / 1000; const colX = m.columns.filter((c) => Math.abs(c.s - f.s) <= Lf / 2).map((c) => (c.s - f.s) * 1000); const yB9Bot = bottom + mm(z.B24); const yB9Top = bottom + mm(z.B5); for (const b of b9Bars(Lf * 1000, Wf * 1000, colX).bars) { const x = mm(b.x); const n = mm(b.n); const tailDir = x >= 0 ? -1 : 1; addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x, yB9Top, n], dB9, COLOR.footing); addBar('B9', 0, [f.s + x, yB9Top, n], [f.s + x + tailDir * tail9, yB9Top, n], dB9, COLOR.footing); addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x + tailDir * tail9, yB9Bot, n], dB9, COLOR.footing); } // B10-1 / B10-2 (§7) at each B10 level, B11 / B12 (§8) between them. Ends turn 90° inward (B10) or down (B11, B12). const polyline = (name: string, pts: [number, number, number][], dMm: number) => { for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], dMm, COLOR.footing); }; const hookB10 = mm(hookLength('main90', NOMINAL_DIAMETER.S19)); // 296 const hookAsm = mm(Math.round(hookLength('main90', NOMINAL_DIAMETER.S16))); // 246 const d1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19); // B10-1 depth (mm), on the W faces const d2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19); // B10-2 depth (mm), on the L faces const sEdge = Lf / 2 - mm(d2); const nEdge = Wf / 2 - mm(d1); const dB10 = mm(NOMINAL_DIAMETER.S19); for (let j = 0; j < b10Levels(Hf); j++) { const zj = bottom + mm(FOOTING_DEFAULTS.cBottom + j * FOOTING_DEFAULTS.sV); for (const sg of [-1, 1]) { // B10-2: across W at the L faces; its ends turn inward along s. const sp = f.s + sg * sEdge; polyline('B10-2', [[sp - sg * hookB10, zj, -nEdge], [sp, zj, -nEdge], [sp, zj, nEdge], [sp - sg * hookB10, zj, nEdge]], NOMINAL_DIAMETER.S19); // B10-1: along s at the W faces, 19.1 above B10-2; its ends turn inward along n. const np = sg * nEdge; const zb = zj + dB10; polyline('B10-1', [[f.s - sEdge, zb, np - sg * hookB10], [f.s - sEdge, zb, np], [f.s + sEdge, zb, np], [f.s + sEdge, zb, np - sg * hookB10]], NOMINAL_DIAMETER.S19); } } // B11 (across W, spread along L) and B12 (along L, spread across W) at the assembly levels, 4s apart, 2s stagger per level. const xsL = gridPositions(Lf * 1000); const ysW = gridPositions(Wf * 1000); const dAsm = assemblyEndOffset(); const nAsm = Wf / 2 - mm(d1 + dAsm); const sAsm = Lf / 2 - mm(d2 + dAsm); assemblyJs(Hf).forEach((jA, k) => { const zA = bottom + mm(FOOTING_DEFAULTS.cBottom + jA * FOOTING_DEFAULTS.sV); const zB = zA + dB10; const i0 = k % 2 === 0 ? 0 : 2; for (let iL = i0; iL < xsL.length; iL += 4) { const x = f.s + mm(xsL[iL]); polyline('B11', [[x, zA - hookAsm, -nAsm], [x, zA, -nAsm], [x, zA, nAsm], [x, zA - hookAsm, nAsm]], NOMINAL_DIAMETER.S16); } for (let iW = i0; iW < ysW.length; iW += 4) { const n = mm(ysW[iW]); polyline('B12', [[f.s - sAsm, zB - hookAsm, n], [f.s - sAsm, zB, n], [f.s + sAsm, zB, n], [f.s + sAsm, zB - hookAsm, n]], NOMINAL_DIAMETER.S16); } }); } // Bearing blocks: S2 (upper, along s) and S1 (lower, along n), 8 bars each across the 1200 block. // Positions from barsAcross (162.5 from the block edge, @125), bars a − 2·c_s long, legs 700 down at both ends (§3–5). const across = barsAcross(); const half = (BLOCK_REBAR.dim - 2 * BLOCK_REBAR.cover) / 2000; const legM = BLOCK_REBAR.legLength / 1000; const offsets = Array.from({ length: across.count }, (_, i) => (across.firstEnd + i * across.spacing) / 1000 - BLOCK_REBAR.dim / 2000); const dBar = NOMINAL_DIAMETER.H19; for (const b of m.blocks) { const z2 = b.top - BLOCK_REBAR.depthS2 / 1000; const z1 = b.top - BLOCK_REBAR.depthS1 / 1000; for (const off of offsets) { const n2 = b.n + off; addBar('S2', b.k, [b.s - half, z2, n2], [b.s + half, z2, n2], dBar, COLOR.block); addBar('S2', b.k, [b.s - half, z2 - legM, n2], [b.s - half, z2, n2], dBar, COLOR.block); addBar('S2', b.k, [b.s + half, z2, n2], [b.s + half, z2 - legM, n2], dBar, COLOR.block); const s1 = b.s + off; addBar('S1', b.k, [s1, z1, b.n - half], [s1, z1, b.n + half], dBar, COLOR.block); addBar('S1', b.k, [s1, z1 - legM, b.n - half], [s1, z1, b.n - half], dBar, COLOR.block); addBar('S1', b.k, [s1, z1, b.n + half], [s1, z1 - legM, b.n + half], dBar, COLOR.block); } } const highlight = (name: string | null) => { for (const [key, meshes] of byName) { for (const mesh of meshes) { const mat = mesh.material as THREE.MeshStandardMaterial; const on = name === null || key === name || (name === '커플러' && key.includes('커플러')) || (name === '주철근 커플러' && key === '주철근 커플러') || (name === '띠철근 커플러' && key === '띠철근 커플러'); mat.transparent = !on; mat.opacity = on ? 1 : 0.18; const match = name !== null && (key === name || (name === '커플러' && key.includes('커플러'))); mat.emissive.setHex(match ? 0x6b5a00 : 0x000000); } } }; return { group, byName, highlight }; }