diff --git a/src/main.ts b/src/main.ts index aca0082..b66fe3b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,7 +73,7 @@ import { pileGeometry } from './core/geometry'; import { rebarSummary, type RebarRow } from './core/rebar/summary'; import { shearSegments } from './core/rebar/coping'; import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar3d'; -import { buildAbutFootingRebar3D } from './view/abut-rebar3d'; +import { buildAbutRebar3D } from './view/abut-rebar3d'; import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, isAssumedMaterial, materialFor, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common'; import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity'; import { groundElFor, parseGroundModel, strataRowsFor, type GroundMapLayer, type GroundModelFile } from './core/ground-model'; @@ -682,12 +682,13 @@ function setupScene(): void { state.scene = scene; setView(state.view); } - // ③ 교대 (2026-11-04): with the piers hidden, the selected abutment's 저판 rebar is the ③ rebar (표 · 3D 선택 공통). + // ③ 교대 (2026-11-04): with the piers hidden, the selected abutment's 저판 · 벽체 rebar is the ③ rebar (표 · 3D 선택 공통). const abutRec = piersShown(state.stage, state.abutActive) ? undefined : state.abutments.find((a) => a.name === state.abutActive); const abutGroup = abutRec ? abutGroupByName.get(abutRec.name) : undefined; const footing = abutRec ? abutFootingBuilt(abutRec) : null; - if (abutGroup && footing && footing.bottomEl !== undefined) { - rebar3d = buildAbutFootingRebar3D(footing.geom, footing.result, footing.skewK, footing.backM, footing.bottomEl); + if (abutRec && abutGroup && footing && footing.bottomEl !== undefined) { + const wall = abutWallBuilt(abutRec, footing.result); + rebar3d = buildAbutRebar3D(footing.geom, footing.result, footing.skewK, footing.backM, footing.bottomEl, wall?.result); rebar3d.group.applyMatrix4(abutGroup.matrix); rebar3d.group.visible = state.stage === 'rebar'; rebar3d.setColorMode(state.rebarColor); diff --git a/src/view/abut-rebar3d.ts b/src/view/abut-rebar3d.ts index 12db77c..e96ea71 100644 --- a/src/view/abut-rebar3d.ts +++ b/src/view/abut-rebar3d.ts @@ -1,5 +1,6 @@ /** - * 교대 저판 3D 철근 (③, 2026-11-04). Positions come from `abutFootingRebar` (no new rules here); the shapes follow the + * 교대 3D 철근 (③, 2026-11-04): 저판 B1–B10과 벽체 W1–W11. Positions come from `abutFootingRebar` · `abutWallRebar` + * (no new rules here; the 벽체 bars are its 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). @@ -8,20 +9,23 @@ 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_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 skewK plan skew (s per m of t, `abutSkewK`), @param backM 저판 배면 t (m), @param bottomEl 기초 하단 EL (m), + * @param wall 벽체 배근 (없으면 저판만). */ -export function buildAbutFootingRebar3D(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, backM: number, bottomEl: number): RebarScene { +export function buildAbutRebar3D(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, backM: number, bottomEl: number, wall?: AbutWallRebar): 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])); + const sizeOf = new Map([...r.bars.map((b) => [b.mark, b.size] as const), ...Object.entries(ABUT_WALL_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. @@ -96,5 +100,12 @@ export function buildAbutFootingRebar3D(g: AbutFootingGeom, r: AbutFootingRebar, 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 저판 하면. + 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(([u, t, z]): P3 => [u / 1000 + (skewK * t) / 1000, bottomEl + z / 1000, t / 1000]), colorOf(p.mark)); + } return { group, byName, records: [], ...rebarControls(byName) }; } diff --git a/tests/abut-rebar3d.test.ts b/tests/abut-rebar3d.test.ts index 2702577..e204740 100644 --- a/tests/abut-rebar3d.test.ts +++ b/tests/abut-rebar3d.test.ts @@ -9,7 +9,8 @@ import { buildMembers } from '../src/core/abut/members'; import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing'; import { skewAngle } from '../src/core/abut/rebar/skew-layout'; import { abutSkewK } from '../src/view/abut-scene'; -import { buildAbutFootingRebar3D } from '../src/view/abut-rebar3d'; +import { buildAbutRebar3D } from '../src/view/abut-rebar3d'; +import { abutWallGeom, abutWallRebar } from '../src/core/abut/rebar/wall'; // 예제 A1 (교대확대기초철근.md §13). const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); @@ -22,7 +23,7 @@ const back = members.shape.footingBackM; const bottom = members.footingBottomEl!; const build = (dir: 'skew' | 'perp') => { const r = abutFootingRebar(geom, { dir }); - return { r, scene: buildAbutFootingRebar3D(geom, r, k, back, bottom) }; + return { r, scene: buildAbutRebar3D(geom, r, k, back, bottom) }; }; // Segments per bar: ⊓ / ⊔ have 3, the others 1. const PIECES: Record = { B1: 3, B2: 3, B5: 3, B6: 3 }; @@ -64,3 +65,36 @@ describe('교대 저판 3D 철근 (③, 2026-11-04)', () => { expect(scene.byName.get('B1')!.every((m) => m.visible)).toBe(true); }); }); + +describe('교대 벽체 3D 철근 (③, 2026-11-04)', () => { + const footing = abutFootingRebar(geom); + const wGeom = abutWallGeom(model.length, members, skewAngle(rec.skewDeg)); + const wall = abutWallRebar(wGeom, footing); + const scene = buildAbutRebar3D(geom, footing, k, back, bottom, wall); + + it('draws every wall bar of the 철근표 (one tube per straight piece)', () => { + for (const b of wall.bars) { + const pieces = wall.paths.filter((p) => p.mark === b.mark).reduce((a, p) => a + p.pts.length - 1, 0); + expect(scene.byName.get(b.mark)?.length, b.mark).toBe(pieces); + } + expect(scene.byName.get('B1')?.length).toBeGreaterThan(0); // the 저판 bars stay + }); + + it('keeps the wall bars between the wall faces and the end faces, above the 저판 하면 철근망', () => { + const top = bottom + (wGeom.HF + Math.max(...wGeom.segments.map((s) => s.H)) + wall.ld.H19) / 1000; + const box = new THREE.Box3(); + for (const b of wall.bars) { + for (const mesh of scene.byName.get(b.mark)!) { + mesh.geometry.computeBoundingBox(); + box.copy(mesh.geometry.boundingBox!); + expect(box.min.z, b.mark).toBeGreaterThan(wGeom.xb / 1000 - 0.3); // 갈고리 296 바깥쪽까지 + expect(box.max.z, b.mark).toBeLessThan(wGeom.xf / 1000 + 0.3); + expect(box.min.y, b.mark).toBeGreaterThan(bottom + footing.z.B68 / 1000); + expect(box.max.y, b.mark).toBeLessThan(top + 0.01); + const t = (box.min.z + box.max.z) / 2; + expect(box.min.x, b.mark).toBeGreaterThan(k * t - 0.05 - Math.abs(k) * 0.3); + expect(box.max.x, b.mark).toBeLessThan(k * t + geom.L / 1000 + 0.05 + Math.abs(k) * 0.3); + } + } + }); +});