Files
Pier_v1/src/view/abut-rebar3d.ts
T
ypshimandClaude Haiku 5.5 91c4a4ad63 feat(abut-rebar): ③ 교대 받침블럭 3D 철근 (S1 · S2)
- 받침블럭 배근 경로를 교대 3D 철근에 추가 (선택 · 색 기준 공통), 방위각 회전 반영
- 콘크리트 안 검사에 받침블럭 포함 (다리 묻힘 포함 모두 안)
- 테스트: 블럭 5개 × 8본씩 3조각, 콘크리트 안

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-05 09:40:30 +09:00

151 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 교대 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) };
}