feat(abut-rebar): ③ 교대 저판 3D 철근 (B1–B10)
- 배근 결과(abutFootingRebar)로 저판 철근을 교대 좌표에 그림: B1 ⊓ · B2 ⊔ · B3 · B4, B5 ⊓ · B6 ⊔ · B7 · B8, B9, B10-1 · B10-2, 옵션 ② B1-S · B2-S - 교대 선택 시 ③ 철근(rebar3d)으로 사용: 철근표 · 삽도 선택, 선택만 보기, 색 기준, 3D 클릭 선택 공통 - 교각 rebar3d의 강조 · 색 · 범례를 rebarControls로 공용화 - 테스트: 철근표 개수 = 3D 개수, 모든 철근이 저판 안 (skew · 교대축 직각) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7714f7ef2
commit
8361487b6b
4 files changed
+188
-4
No files matched your search
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 교대 저판 3D 철근 (③, 2026-11-04). Positions come from `abutFootingRebar` (no new rules here); 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_FOOTING_COLORS } from './abut-footing-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).
|
||||
*/
|
||||
export function buildAbutFootingRebar3D(g: AbutFootingGeom, r: AbutFootingRebar, skewK: number, backM: number, bottomEl: number): 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(r.bars.map((b) => [b.mark, b.size]));
|
||||
// (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);
|
||||
}
|
||||
return { group, byName, records: [], ...rebarControls(byName) };
|
||||
}
|
||||
+7
-2
@@ -49,7 +49,7 @@ const MEMBER_LABELS: Record<RebarMemberKey, string> = { bearing: '받침블럭',
|
||||
const hex = (v: number) => `#${v.toString(16).padStart(6, '0')}`;
|
||||
/** Rebar is drawn a little deeper than the 6.7 colours so it reads through the half-transparent members (승인 2026-10-23). */
|
||||
const REBAR_SHADE = 0.8;
|
||||
const rebarColor = (v: number) => new THREE.Color(v).multiplyScalar(REBAR_SHADE);
|
||||
export const rebarColor = (v: number) => new THREE.Color(v).multiplyScalar(REBAR_SHADE);
|
||||
|
||||
const COLOR = {
|
||||
coping: 0xb4531f,
|
||||
@@ -555,6 +555,11 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
}
|
||||
}
|
||||
|
||||
return { group, byName, records, ...rebarControls(byName) };
|
||||
}
|
||||
|
||||
/** Highlight, colour and legend over bars grouped by 철근표 number (교각 · 교대 3D 철근 공통). */
|
||||
export function rebarControls(byName: Map<string, THREE.Mesh[]>): Pick<RebarScene, 'show' | 'setColorMode' | 'legend'> {
|
||||
const show = (names: ReadonlySet<string> | null, isolate: boolean) => {
|
||||
// 커플러 as a name selects every coupler bar.
|
||||
const isOn = (key: string) => names === null || names.has(key) || (names.has('커플러') && key.includes('커플러'));
|
||||
@@ -592,5 +597,5 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
const items = DIAMETER_GROUPS.map((g, i) => ({ label: g.label, color: hex(DIAMETER_COLORS[i]) })).filter((_, i) => present.has(i));
|
||||
return present.has(-1) ? [...items, { label: '기타', color: hex(OTHER_COLOR) }] : items;
|
||||
};
|
||||
return { group, byName, records, show, setColorMode, legend };
|
||||
return { show, setColorMode, legend };
|
||||
}
|
||||
Reference in new issue
Block a user