/** * 교대 3D 철근 (③, 2026-11-04): 저판 B1–B10, 벽체 W1–W11, 흉벽 · 브라켓 P1–P11, 날개벽 WR1–WR20 (좌 · 우), 접속슬래브 R1–R8. Positions come from `abutFootingRebar` · * `abutWallRebar` · `abutParapetRebar` (no new rules here; the 벽체 · 흉벽 bars are their paths). The 저판 shapes follow the * 배근 삽도 (abut-footing-rebar-svg.ts): B1 ⊓ / B2 ⊔ / B3 · B4 직선, B5 ⊓ / B6 ⊔ / B7 · B8 직선, B9 연직, B10 측면, B1-S · B2-S. * Hooks and the bend radii are not drawn. Each bar is named by its 철근표 number, so the table, the 삽도 and the model * share one selection. Local frame = the abutment group's (x = s along the axis, y = EL, z = t toward the front). */ import * as THREE from 'three'; import type { AbutFootingGeom, AbutFootingRebar } from '../core/abut/rebar/footing'; import { NOMINAL_DIAMETER } from '../core/rebar/common'; import { memberOfBar } from '../core/rebar/color'; import { ABUT_WALL_SIZES, type AbutWallRebar } from '../core/abut/rebar/wall'; import { ABUT_PARAPET_SIZES, type AbutParapetRebar } from '../core/abut/rebar/parapet'; import { ABUT_PARAPET_COLORS } from './abut-parapet-rebar-svg'; import { ABUT_WING_SIZES, type AbutWingRebar } from '../core/abut/rebar/wing'; import { ABUT_WING_COLORS } from './abut-wing-rebar-svg'; import { ABUT_SLAB_SIZES, type AbutSlabRebar } from '../core/abut/rebar/slab'; import { ABUT_BLOCK_SIZES, type AbutBlockRebar } from '../core/abut/rebar/block'; import { ABUT_BLOCK_COLORS } from './abut-block-rebar-svg'; import { ABUT_SLAB_COLORS } from './abut-slab-rebar-svg'; import { ABUT_FOOTING_COLORS } from './abut-footing-rebar-svg'; import { ABUT_WALL_COLORS } from './abut-wall-rebar-svg'; import { rebarColor, rebarControls, type RebarScene } from './rebar3d'; type P3 = [number, number, number]; /** * @param skewK plan skew (s per m of t, `abutSkewK`), @param backM 저판 배면 t (m), @param bottomEl 기초 하단 EL (m), * @param wall 벽체 배근, @param parapet 흉벽 배근, @param wings 날개벽 배근 (측마다, 경로는 교대 s · t · z) — 없으면 그 부재는 그리지 않음. */ export function buildAbutRebar3D( g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, backM: number, bottomEl: number, wall?: AbutWallRebar, parapet?: AbutParapetRebar, wings: AbutWingRebar[] = [], slab?: AbutSlabRebar, blocks?: AbutBlockRebar, ): RebarScene { const group = new THREE.Group(); group.name = 'abut-rebar'; const byName = new Map(); const C = Object.fromEntries(Object.entries(ABUT_FOOTING_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; const sizeOf = new Map([...r.bars.map((b) => [b.mark, b.size] as const), ...Object.entries(ABUT_WALL_SIZES), ...Object.entries(ABUT_PARAPET_SIZES), ...Object.entries(ABUT_WING_SIZES), ...Object.entries(ABUT_SLAB_SIZES), ...Object.entries(ABUT_BLOCK_SIZES)]); // (s, x, z) in mm: s along the skew grid (끝면과 나란히), x from the 배면, z from the 저판 하면. const P = (s: number, x: number, z: number): P3 => [s / 1000 + skewK * (backM + x / 1000), bottomEl + z / 1000, backM + x / 1000]; // Option ②: square to the axis, s in the 삽도's frame (abutFootingRebarPlanSvg SQ), the same for every x. const shiftMax = Math.abs(skewK) * g.W; const Q = (s: number, x: number, z: number): P3 => { const u = (skewK >= 0 ? s : s - shiftMax) / 1000 + skewK * backM; return [u, bottomEl + z / 1000, backM + x / 1000]; }; const main = r.dir === 'skew' ? P : Q; const add = (name: string, pts: P3[], color: number) => { const size = sizeOf.get(name); if (!size) return; const dMm = NOMINAL_DIAMETER[size]; for (let i = 1; i < pts.length; i++) { const a = new THREE.Vector3(...pts[i - 1]); const b = new THREE.Vector3(...pts[i]); const dir = b.clone().sub(a); const len = dir.length(); if (len < 1e-6) continue; const geo = new THREE.CylinderGeometry(dMm / 2000, dMm / 2000, len, 6); geo.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize())); geo.translate((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: rebarColor(color), roughness: 0.45, metalness: 0.1 })); mesh.userData = { name, roleColor: color, dMm, member: memberOfBar(name) }; group.add(mesh); const list = byName.get(name) ?? []; list.push(mesh); byName.set(name, list); } }; const c = r.settings.cSide; const { B13, B24, B57, B68 } = r.z; // B1–B4: across the width. B1 sits beside B2 along the axis (옆붙임). r.main.positions.forEach((s, i) => { if (i % 2 === 0) { const s1 = s + r.sideShift.B1; add('B1', [main(s1, c, B24), main(s1, c, B13), main(s1, g.W - c, B13), main(s1, g.W - c, B24)], C.main); add('B2', [main(s, c, B13), main(s, c, B24), main(s, g.W - c, B24), main(s, g.W - c, B13)], C.main); } else { add('B3', [main(s, c, B13), main(s, g.xf - c, B13)], C.main); add('B4', [main(s, c, B24), main(s, g.W - c, B24)], C.main); } }); for (const off of r.skewEnd?.offsets ?? []) { for (const s of [c + off, g.L - c - off]) { add('B1-S', [P(s, c, B13), P(s, g.W - c, B13)], C.main); add('B2-S', [P(s, c, B24), P(s, g.W - c, B24)], C.main); } } // B5–B8: along the axis, cut at the end cover (c / sin θ). Top bars only where the 삽도 keeps them. const e = c / g.angle.sin; r.dist.positions.forEach((x, i) => { const one = i % 2 === 0; if (r.dist.topKept[i]) { if (one) { const x5 = x + r.sideShift.B5; add('B5', [P(e, x5, B68), P(e, x5, B57), P(g.L - e, x5, B57), P(g.L - e, x5, B68)], C.dist); } else add('B7', [P(e, x, B57), P(g.L - e, x, B57)], C.dist); } if (one) add('B6', [P(e, x, B57), P(e, x, B68), P(g.L - e, x, B68), P(g.L - e, x, B57)], C.dist); else add('B8', [P(e, x, B68), P(g.L - e, x, B68)], C.dist); }); // B9: vertical, B2 → B1 level. for (const row of r.shear.rows) for (const s of row.s) add('B9', [main(s, row.x, B24), main(s, row.x, B13)], C.shear); // B10-1 on the front and back faces (one bar above the level, as in the 단면), B10-2 on the two skew end faces. const { depth1, depth2, levels } = r.side; const e2 = depth2 / g.angle.sin; for (const z of levels) { const z1 = z + NOMINAL_DIAMETER[sizeOf.get('B10-1') ?? 'H19']; for (const x of [depth1, g.W - depth1]) add('B10-1', [P(e2, x, z1), P(g.L - e2, x, z1)], C.side); for (const s of [e2, g.L - e2]) add('B10-2', [P(s, depth1, z), P(s, g.W - depth1, z)], C.side); } // 벽체 · 흉벽: the rule paths in (u, t, z) mm — u on the skew grid, t from the axis, z from the 저판 하면. const toScene = ([u, t, z]: [number, number, number]): P3 => [u / 1000 + (skewK * t) / 1000, bottomEl + z / 1000, t / 1000]; if (wall) { const W = Object.fromEntries(Object.entries(ABUT_WALL_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; const colorOf = (mark: string) => ['W1', 'W3', 'W4', 'W11'].includes(mark) ? W.vertical : mark === 'W7' ? W.tie : mark === 'W9' || mark === 'W10' ? W.step : W.horizontal; for (const p of wall.paths) add(p.mark, p.pts.map(toScene), colorOf(p.mark)); } if (parapet) { const P = Object.fromEntries(Object.entries(ABUT_PARAPET_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; const colorOf = (mark: string) => ['P1', 'P2', 'P3', 'P11'].includes(mark) ? P.vertical : mark === 'P6' || mark === 'P7' ? P.tie : ['P8', 'P9', 'P10'].includes(mark) ? P.side : P.horizontal; for (const p of parapet.paths) add(p.mark, p.pts.map(toScene), colorOf(p.mark)); } const G = Object.fromEntries(Object.entries(ABUT_WING_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; const wingColor = (mark: string) => ['WR1', 'WR2', 'WR11', 'WR12'].includes(mark) ? G.vertical : /^WR(3|4|5|13|14|15)$/.test(mark) ? G.horizontal : mark === 'WR6' || mark === 'WR20' ? G.other : G.haunch; for (const w of wings) for (const p of w.paths) add(p.mark, p.pts.map(([s, t, z]): P3 => [s / 1000, bottomEl + z / 1000, t / 1000]), wingColor(p.mark)); if (slab) { const S = Object.fromEntries(Object.entries(ABUT_SLAB_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; const slabColor = (mark: string) => (mark === 'R1' || mark === 'R3' ? S.main : mark === 'R7' ? S.corner : mark === 'R8' ? S.shear : S.dist); for (const p of slab.paths) add(p.mark, p.pts.map(([s, t, z]): P3 => [s / 1000, bottomEl + z / 1000, t / 1000]), slabColor(p.mark)); } if (blocks) { const B = Object.fromEntries(Object.entries(ABUT_BLOCK_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record<'s1' | 's2', number>; for (const p of blocks.paths) add(p.mark, p.pts.map(([s, t, z]): P3 => [s / 1000, bottomEl + z / 1000, t / 1000]), p.mark === 'S1' ? B.s1 : B.s2); } return { group, byName, records: [], ...rebarControls(byName) }; }