- 받침블럭 배근 경로를 교대 3D 철근에 추가 (선택 · 색 기준 공통), 방위각 회전 반영 - 콘크리트 안 검사에 받침블럭 포함 (다리 묻힘 포함 모두 안) - 테스트: 블럭 5개 × 8본씩 3조각, 콘크리트 안 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
151 lines
9.1 KiB
TypeScript
151 lines
9.1 KiB
TypeScript
/**
|
||
* 교대 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<string, THREE.Mesh[]>();
|
||
const C = Object.fromEntries(Object.entries(ABUT_FOOTING_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record<keyof typeof ABUT_FOOTING_COLORS, number>;
|
||
const sizeOf = new Map<string, keyof typeof NOMINAL_DIAMETER>([...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<keyof typeof ABUT_WALL_COLORS, number>;
|
||
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<keyof typeof ABUT_PARAPET_COLORS, number>;
|
||
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<keyof typeof ABUT_WING_COLORS, number>;
|
||
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<keyof typeof ABUT_SLAB_COLORS, number>;
|
||
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) };
|
||
}
|