feat(view): 철근표 and the 3D rebar model point at each other (③)
- The 3D model of ③ draws each bar family from the rebar modules (coping top and bottom bars, shear loops, side bars, column bars and ties, footing B1–B8, bearing S1/S2) and names every bar as its 철근표 number. - A table row click highlights the bar in 3D (the others dim); a click on a 3D bar selects its row in the table and sets the member tab. Drags still rotate. - The rebar is shown only on ③. Not drawn yet: C14, C15/C16, B9–B12, the couplers, and the bars of a bent coping. - Tests: family names and counts (P1 46), highlight behaviour. - Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c96ab76f5
commit
a692f1f507
3 files changed
+277
-1
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import * as THREE from 'three';
|
||||
import type { PierModel } from '../core/geometry';
|
||||
import { grid, layers } from '../core/rebar/footing';
|
||||
import { widthLayout } from '../core/rebar/coping-main';
|
||||
import { shearBars, shearBarsMulti, shearUHeight, sideRows } from '../core/rebar/coping';
|
||||
import { columnRebar, COLUMN_P1 } from '../core/rebar/column';
|
||||
import { NOMINAL_DIAMETER } from '../core/rebar/common';
|
||||
import { stretchMatrix } from './scene';
|
||||
|
||||
/**
|
||||
* 3D rebar of the ③ screen. Positions come from the rebar modules (no new rules here). Each bar is a cylinder
|
||||
* named by its 철근표 number (C1, P3, B5, S2 …), so the table and the model can point at each other.
|
||||
* Local frame: x = along the axis s, y = absolute EL (m), z = across n. Stretch matrices map it to the scene.
|
||||
*/
|
||||
export interface RebarScene {
|
||||
group: THREE.Group;
|
||||
byName: Map<string, THREE.Mesh[]>;
|
||||
/** Highlight one bar name (or none): the others are dimmed. */
|
||||
highlight(name: string | null): void;
|
||||
}
|
||||
|
||||
const COLOR = {
|
||||
coping: 0xb4531f,
|
||||
shear: 0x6b3fa0,
|
||||
column: 0x0f5c66,
|
||||
tie: 0x6b3fa0,
|
||||
footing: 0x0f5c66,
|
||||
footingBottom: 0x2e7d32,
|
||||
block: 0xa07a10,
|
||||
} as const;
|
||||
|
||||
const mm = (v: number) => v / 1000;
|
||||
|
||||
export function buildRebar3D(m: PierModel): RebarScene {
|
||||
const group = new THREE.Group();
|
||||
const byName = new Map<string, THREE.Mesh[]>();
|
||||
const Ms = m.stretches.map((_, k) => stretchMatrix(m, k));
|
||||
const L = m.copingL;
|
||||
const B = m.copingB;
|
||||
const straight = m.kink === null;
|
||||
|
||||
const addBar = (name: string, k: number, a: [number, number, number], b: [number, number, number], dMm: number, color: number) => {
|
||||
const dir = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
||||
const len = dir.length();
|
||||
if (len < 1e-6) return;
|
||||
const geo = new THREE.CylinderGeometry(mm(dMm) / 2, mm(dMm) / 2, len, 6);
|
||||
geo.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()));
|
||||
geo.translate((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
|
||||
geo.applyMatrix4(Ms[k]);
|
||||
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness: 0.1 });
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.userData.name = name;
|
||||
group.add(mesh);
|
||||
const list = byName.get(name) ?? [];
|
||||
list.push(mesh);
|
||||
byName.set(name, list);
|
||||
};
|
||||
|
||||
// Coping (straight coping only; a bent coping is not drawn in 3D yet).
|
||||
if (straight) {
|
||||
const w = widthLayout(B * 1000);
|
||||
const xs = [w.yE, ...Array.from({ length: w.n + 1 }, (_, i) => w.yE + w.g + 125 * i), B * 1000 - w.yE].map((v) => mm(v) - B / 2);
|
||||
const cover = 0.1;
|
||||
// Top rows C1–C6: row k is 3 bars per cycle; the even positions are cycle 1 (C1–C3), the odd cycle 2 (C4–C6).
|
||||
const topEl = m.copingRefTop;
|
||||
for (let k = 1; k <= 3; k++) {
|
||||
const z = topEl - 0.127 - (k - 1) * 0.057;
|
||||
xs.forEach((n, j) => {
|
||||
const name = `C${k + (j % 2 === 1 ? 3 : 0)}`;
|
||||
addBar(name, 0, [cover, z, n], [L - cover, z, n], NOMINAL_DIAMETER.H29, COLOR.coping);
|
||||
});
|
||||
}
|
||||
// Bottom bars C7 / C8 along the Level height.
|
||||
xs.forEach((n, j) => {
|
||||
const z = m.levelBottom + 0.127;
|
||||
addBar(j % 2 === 0 ? 'C7' : 'C8', 0, [cover, z, n], [L - cover, z, n], NOMINAL_DIAMETER.H29, COLOR.coping);
|
||||
});
|
||||
// Shear loops C9 / C10 / C11 and the gaps between columns: a closed U at each position.
|
||||
const colS = m.columns.map((c) => c.s * 1000);
|
||||
const sh = colS.length > 1 ? shearBarsMulti(L * 1000, colS) : { ...shearBars(L * 1000), between: [] as number[] };
|
||||
const loops: [string, number[]][] = [['C9', sh.c9], ['C10', sh.c10], ['C11', sh.c11], ['C9·C11 사이', sh.between]];
|
||||
const ne = B / 2 - 0.1;
|
||||
const top = topEl - 0.1;
|
||||
for (const [name, list] of loops) {
|
||||
for (const x of list) {
|
||||
const xm = mm(x);
|
||||
const bot = top - shearUHeight(x, L * 1000) / 1000;
|
||||
const d = NOMINAL_DIAMETER.H25;
|
||||
addBar(name, 0, [xm, top, -ne], [xm, top, ne], d, COLOR.shear);
|
||||
addBar(name, 0, [xm, bot, -ne], [xm, bot, ne], d, COLOR.shear);
|
||||
addBar(name, 0, [xm, top, -ne], [xm, bot, -ne], d, COLOR.shear);
|
||||
addBar(name, 0, [xm, top, ne], [xm, bot, ne], d, COLOR.shear);
|
||||
}
|
||||
}
|
||||
// Side horizontal bars C12 / C13 at both side faces.
|
||||
const side = sideRows(41.288, 42.538);
|
||||
const sideFace = B / 2 - 0.127;
|
||||
for (const [name, rows] of [['C12', side.c12], ['C13', side.c13]] as const) {
|
||||
for (const z of rows) {
|
||||
addBar(name, 0, [cover, z, -sideFace], [L - cover, z, -sideFace], NOMINAL_DIAMETER.H25, COLOR.shear);
|
||||
addBar(name, 0, [cover, z, sideFace], [L - cover, z, sideFace], NOMINAL_DIAMETER.H25, COLOR.shear);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Columns: P1 / P2 main bars on their radii, ties P3 / P5 / P4 as rings, P6 crossties across the column.
|
||||
const col = columnRebar(COLUMN_P1);
|
||||
const zBot = -1697.5;
|
||||
const ring = (name: string, k: number, sc: number, y: number, r: number, d: number) => {
|
||||
const N = 36;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a0 = (2 * Math.PI * i) / N;
|
||||
const a1 = (2 * Math.PI * (i + 1)) / N;
|
||||
addBar(name, k, [sc + r * Math.cos(a0), y, r * Math.sin(a0)], [sc + r * Math.cos(a1), y, r * Math.sin(a1)], d, COLOR.tie);
|
||||
}
|
||||
};
|
||||
for (const c of m.columns) {
|
||||
const sc = c.s;
|
||||
const base = c.bottom;
|
||||
const rt = mm(col.tieRadius);
|
||||
for (let i = 0; i < COLUMN_P1.nDiv; i++) {
|
||||
const th = (2 * Math.PI * i) / COLUMN_P1.nDiv;
|
||||
const r1 = mm(col.r1);
|
||||
addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + r1 * Math.cos(th), base + mm(col.zTop1), r1 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column);
|
||||
const r2 = mm(col.r2);
|
||||
addBar('P2', c.k, [sc + r2 * Math.cos(th), base + mm(zBot), r2 * Math.sin(th)], [sc + r2 * Math.cos(th), base + mm(col.zTop2), r2 * Math.sin(th)], NOMINAL_DIAMETER.S32, COLOR.column);
|
||||
}
|
||||
for (const z of col.p3) ring('P3', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
|
||||
for (const z of col.p5) ring('P5', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
|
||||
for (const z of col.p4) ring('P4', c.k, sc, base + mm(z), rt, NOMINAL_DIAMETER.H19);
|
||||
for (const z of col.p6Levels) {
|
||||
for (const deg of [0, 46.96, 90, 133.04]) {
|
||||
const a = (deg * Math.PI) / 180;
|
||||
addBar('P6', c.k, [sc - rt * Math.cos(a), base + mm(z), -rt * Math.sin(a)], [sc + rt * Math.cos(a), base + mm(z), rt * Math.sin(a)], NOMINAL_DIAMETER.H16, COLOR.tie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footing bars B1–B8 (교각축 · 교각축 직각), the grids from §3 and §6 of 확대기초철근 (1).md.
|
||||
for (const f of m.footings) {
|
||||
const Lf = f.length;
|
||||
const Wf = f.width;
|
||||
const Hf = (f.top - f.bottom) * 1000;
|
||||
const z = layers(Hf);
|
||||
const gW = grid(Wf * 1000);
|
||||
const gL = grid(Lf * 1000);
|
||||
const posW = (j: number) => -Wf / 2 + mm(100 + gW.e + j * 125);
|
||||
const posL = (i: number) => -Lf / 2 + mm(100 + gL.e + i * 125);
|
||||
const sA = -Lf / 2 + 0.1;
|
||||
const sB = Lf / 2 - 0.1;
|
||||
const nA = -Wf / 2 + 0.1;
|
||||
const nB = Wf / 2 - 0.1;
|
||||
const bottom = f.bottom;
|
||||
const along = (name: string, n: number, y: number, dMm: number, color: number) =>
|
||||
addBar(name, 0, [f.s + sA, y, n], [f.s + sB, y, n], dMm, color);
|
||||
const acrossN = (name: string, s: number, y: number, dMm: number, color: number) =>
|
||||
addBar(name, 0, [f.s + s, y, nA], [f.s + s, y, nB], dMm, color);
|
||||
for (let j = 0; j <= gW.n; j++) {
|
||||
const n = posW(j);
|
||||
if (j % 2 === 1) {
|
||||
along('B1', n, bottom + mm(z.B1), NOMINAL_DIAMETER.S25, COLOR.footing);
|
||||
along('B3', n, bottom + mm(z.B3), NOMINAL_DIAMETER.S19, COLOR.footing);
|
||||
} else {
|
||||
along('B4', n, bottom + mm(z.B24), NOMINAL_DIAMETER.S25, COLOR.footing);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i <= gL.n; i++) {
|
||||
const s = posL(i);
|
||||
if (i % 2 === 1) {
|
||||
acrossN('B5', s, bottom + mm(z.B5), NOMINAL_DIAMETER.S29, COLOR.footing);
|
||||
acrossN('B6', s, bottom + mm(z.B68), NOMINAL_DIAMETER.S29, COLOR.footing);
|
||||
acrossN('B7', s, bottom + mm(z.B7), NOMINAL_DIAMETER.S19, COLOR.footing);
|
||||
} else {
|
||||
acrossN('B8', s, bottom + mm(z.B68), NOMINAL_DIAMETER.S29, COLOR.footing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bearing blocks: S2 (upper, along s) and S1 (lower, along n), 8 bars each across the 1200 block.
|
||||
for (const b of m.blocks) {
|
||||
for (let j = 0; j < 8; j++) {
|
||||
const off = (j - 3.5) * 0.125;
|
||||
addBar('S2', b.k, [b.s - 0.5, b.top - 0.05, b.n + off], [b.s + 0.5, b.top - 0.05, b.n + off], NOMINAL_DIAMETER.H19, COLOR.block);
|
||||
addBar('S1', b.k, [b.s + off, b.top - 0.069, b.n - 0.5], [b.s + off, b.top - 0.069, b.n + 0.5], NOMINAL_DIAMETER.H19, COLOR.block);
|
||||
}
|
||||
}
|
||||
|
||||
const highlight = (name: string | null) => {
|
||||
for (const [key, meshes] of byName) {
|
||||
for (const mesh of meshes) {
|
||||
const mat = mesh.material as THREE.MeshStandardMaterial;
|
||||
const on = name === null || key === name;
|
||||
mat.transparent = !on;
|
||||
mat.opacity = on ? 1 : 0.18;
|
||||
mat.emissive.setHex(name !== null && key === name ? 0x6b5a00 : 0x000000);
|
||||
}
|
||||
}
|
||||
};
|
||||
return { group, byName, highlight };
|
||||
}
|
||||
Reference in new issue
Block a user