Hc = Level underside − footing top, and h_cop = step top at the column centre − Level underside. P1 gives 12703 and 2644, unchanged. With taper h2 = 3000 the Level underside moves down, so Hc = 11203 and the P2 top (Level underside + l_d) moves to 13039, while the P1 top stays at the step top (15147). The 3D and the 철근표 use the same heights. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
532 lines
28 KiB
TypeScript
532 lines
28 KiB
TypeScript
import * as THREE from 'three';
|
||
import type { PierModel } from '../core/geometry';
|
||
import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, FOOTING_DEFAULTS, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing';
|
||
import { bottomPiece, MAIN, widthLayout } from '../core/rebar/coping-main';
|
||
import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping';
|
||
import { sideRowLevels } from '../core/coping-geom';
|
||
import { columnRebar, columnInputFor, COLUMN_P1 } from '../core/rebar/column';
|
||
import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block';
|
||
import { blockFramePoint } from '../core/geometry';
|
||
import { bendRadius, DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common';
|
||
import { diameterGroupIndex, DIAMETER_GROUPS, memberOfBar, type RebarMemberKey } from '../core/rebar/color';
|
||
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. */
|
||
/** Emphasise the bars in `names` (all when null). The others are dimmed, or hidden when `isolate` is on (선택만 보기). */
|
||
show(names: ReadonlySet<string> | null, isolate: boolean): void;
|
||
/** Colour the bars by role (default, spec §6.7), by nominal diameter, or by member. */
|
||
setColorMode(mode: RebarColorMode): void;
|
||
/** Legend entries of the mode, only for the items this model has. Role mode has none (the role colours are in §6.7). */
|
||
legend(mode: RebarColorMode): { label: string; color: string }[];
|
||
}
|
||
|
||
export type RebarColorMode = 'role' | 'diameter' | 'member';
|
||
|
||
/** Okabe–Ito colours (colour-blind safe), one per diameter group in order; grey for anything else. */
|
||
const DIAMETER_COLORS = [0xe69f00, 0x56b4e9, 0x009e73, 0x0072b2, 0xd55e00, 0xcc79a7, 0x000000, 0x8c8c8c];
|
||
const OTHER_COLOR = 0x8c8c8c;
|
||
const MEMBER_COLORS: Record<RebarMemberKey, number> = { bearing: 0xe69f00, coping: 0x0072b2, column: 0x009e73, footing: 0xcc79a7, other: OTHER_COLOR };
|
||
const MEMBER_LABELS: Record<RebarMemberKey, string> = { bearing: '받침블럭', coping: '코핑', column: '기둥', footing: '확대기초', other: '기타' };
|
||
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);
|
||
|
||
const COLOR = {
|
||
coping: 0xb4531f,
|
||
shear: 0x6b3fa0,
|
||
column: 0x0f5c66,
|
||
tie: 0x6b3fa0,
|
||
couplerMain: 0x0b3f46,
|
||
couplerTie: 0x553088,
|
||
footing: 0x0f5c66,
|
||
footingBottom: 0x2e7d32,
|
||
block: 0xa07a10,
|
||
} as const;
|
||
|
||
const mm = (v: number) => v / 1000;
|
||
|
||
export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, byMember: FckByMember = {}): RebarScene {
|
||
const mCoping = materialFor(common, byMember, 'coping');
|
||
const mColumn = materialFor(common, byMember, 'column');
|
||
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;
|
||
|
||
/**
|
||
* A bar through several points as one tube, each bend rounded to its centre-line radius (수정 2026-10-23).
|
||
* A bend of deflection φ is cut back by t = R·tan(φ/2) on both sides and joined by a quadratic curve through the corner.
|
||
*/
|
||
const addPath = (name: string, k: number, pts: [number, number, number][], dMm: number, color: number) => {
|
||
if (pts.length < 2) return;
|
||
const R = mm(bendRadius(dMm, common.bend));
|
||
const pv = pts.map((p) => new THREE.Vector3(p[0], p[1], p[2]));
|
||
const path = new THREE.CurvePath<THREE.Vector3>();
|
||
let cur = pv[0].clone();
|
||
for (let i = 1; i < pv.length - 1; i++) {
|
||
const prev = pv[i - 1];
|
||
const corner = pv[i];
|
||
const next = pv[i + 1];
|
||
const u = corner.clone().sub(prev);
|
||
const v = next.clone().sub(corner);
|
||
const lenIn = u.length();
|
||
const lenOut = v.length();
|
||
if (lenIn < 1e-9 || lenOut < 1e-9) continue;
|
||
u.normalize();
|
||
v.normalize();
|
||
const phi = Math.acos(Math.min(1, Math.max(-1, u.dot(v))));
|
||
// Cut back by the tangent length, never more than half of either adjacent piece.
|
||
const t = Math.min(R * Math.tan(phi / 2), lenIn / 2, lenOut / 2);
|
||
const a = corner.clone().addScaledVector(u, -t);
|
||
const b = corner.clone().addScaledVector(v, t);
|
||
if (a.distanceTo(cur) > 1e-9) path.add(new THREE.LineCurve3(cur, a));
|
||
path.add(new THREE.QuadraticBezierCurve3(a, corner, b));
|
||
cur = b;
|
||
}
|
||
const last = pv[pv.length - 1];
|
||
if (last.distanceTo(cur) > 1e-9) path.add(new THREE.LineCurve3(cur, last));
|
||
const segments = Math.min(400, Math.max(12, Math.round(path.getLength() * 100)));
|
||
const geo = new THREE.TubeGeometry(path, segments, mm(dMm) / 2, 6, false);
|
||
geo.applyMatrix4(Ms[k]);
|
||
const mat = new THREE.MeshStandardMaterial({ color: rebarColor(color), roughness: 0.45, metalness: 0.1 });
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
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 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: rebarColor(color), roughness: 0.45, metalness: 0.1 });
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.userData = { name, roleColor: color, dMm, member: memberOfBar(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 (코핑철근.md §5.1, §5.3): counted from the left, the even (1-based) positions are cycle 1 (C1–C3),
|
||
// the odd cycle 2 (C4–C6). In 0-based j that is odd j → cycle 1. Each row turns down 472 at both ends, and the
|
||
// k-th row's ends sit (k − 1)·57.2 further in.
|
||
const topEl = m.copingRefTop;
|
||
const hookTop = MAIN.hook / 1000;
|
||
for (let k = 1; k <= 3; k++) {
|
||
const z = topEl - 0.127 - (k - 1) * 0.057;
|
||
const xk = cover + ((k - 1) * MAIN.layHorizontal) / 1000;
|
||
xs.forEach((n, j) => {
|
||
const name = `C${k + (j % 2 === 1 ? 0 : 3)}`;
|
||
const pts: [number, number, number][] = [
|
||
[xk, z - hookTop, n],
|
||
[xk, z, n],
|
||
[L - xk, z, n],
|
||
[L - xk, z - hookTop, n],
|
||
];
|
||
addPath(name, 0, pts, NOMINAL_DIAMETER.H29, COLOR.coping);
|
||
});
|
||
}
|
||
// Bottom bars C7 / C8 (코핑철근.md §5.2–5.3): they follow the sloped underside, 127 above it, with a vertical leg up
|
||
// at each end to the C1 level. Each bar is two pieces spliced at the column centre (overlap l_s). The slope runs x_L0 from each end (T형).
|
||
const xL0 = L / 2 - m.copingGeom.levelWidth / 2000;
|
||
const under = (x: number) => undersideAt(L * 1000, x * 1000, m.endBottom, m.levelBottom, m.copingGeom.levelWidth);
|
||
const lap = bottomPiece(mCoping).ls / 2000;
|
||
xs.forEach((n, j) => {
|
||
const z = (x: number) => under(x) + 0.127;
|
||
const left: [number, number, number][] = [
|
||
[cover, topEl - 0.127, n],
|
||
[cover, z(cover), n],
|
||
[xL0, z(xL0), n],
|
||
[L / 2 + lap, z(L / 2 + lap), n],
|
||
];
|
||
const right = left.map(([x, y, zz]): [number, number, number] => [L - x, y, zz]);
|
||
const name = j % 2 === 1 ? 'C7' : 'C8';
|
||
for (const pts of [left, right]) {
|
||
addPath(name, 0, pts, 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, m.copingGeom) : { ...shearBars(L * 1000, undefined, m.copingGeom), between: [] as number[] };
|
||
const loops: [string, number[]][] = [['C9', sh.c9], ['C10', sh.c10], ['C11', sh.c11], ['C9·C11 사이', sh.between]];
|
||
// Each position has three 폐합 (코핑철근.md §4.1): legs at y_i = c + i·s_leg (i = 0 … 5), 폐합 i = legs i and i + 3,
|
||
// 1740 wide. Each 폐합 is a 하부 U (base at the bottom, legs up to the top) and a 상부 삿갓 (top bar, legs down
|
||
// l_cap = h − c) that sits 25.4 beside the U along the coping.
|
||
const top = topEl - 0.1;
|
||
const dShear = NOMINAL_DIAMETER.H25;
|
||
const sLeg = (B * 1000 - 2 * CAP.cover) / 5 / 1000;
|
||
const legN = (i: number) => -B / 2 + 0.1 + i * sLeg;
|
||
for (const [name, list] of loops) {
|
||
for (const x of list) {
|
||
const xm = mm(x);
|
||
const h = shearUHeight(x, L * 1000, m.copingGeom) / 1000;
|
||
const bot = top - h;
|
||
const capBot = top - (h - CAP.cover / 1000);
|
||
for (let i = 0; i < 3; i++) {
|
||
const a = legN(i);
|
||
const b = legN(i + 3);
|
||
// The three 폐합 stack to the right, so no U meets another loop's U or 삿갓 (SHEAR_LOOP_STEP).
|
||
const xu = xm + mm(i * SHEAR_LOOP_STEP);
|
||
const xc = xu + mm(dShear);
|
||
addBar(name, 0, [xu, bot, a], [xu, bot, b], dShear, COLOR.shear);
|
||
addBar(name, 0, [xu, bot, a], [xu, top, a], dShear, COLOR.shear);
|
||
addBar(name, 0, [xu, bot, b], [xu, top, b], dShear, COLOR.shear);
|
||
addBar(name, 0, [xc, top, a], [xc, top, b], dShear, COLOR.shear);
|
||
addBar(name, 0, [xc, top, a], [xc, capBot, a], dShear, COLOR.shear);
|
||
addBar(name, 0, [xc, top, b], [xc, capBot, b], dShear, COLOR.shear);
|
||
}
|
||
}
|
||
}
|
||
// Side horizontal bars C12 / C13 (코핑철근.md §6.2): per row, two 일자 at the side faces from x_w to L − x_w,
|
||
// and two ㄷ bars: a body across the width at x_w, 25.4 above the row, with legs 1525 inward along the faces.
|
||
const sideFace = B / 2 - 0.127;
|
||
const dSide = NOMINAL_DIAMETER.H25;
|
||
const levels = sideRowLevels(m.levelBottom, m.endBottom, m.copingRefTop);
|
||
const side = copingSideBars(L * 1000, levels.zBottom, levels.zBreak, m.copingGeom, levels.zTop);
|
||
for (const [name, rows] of [['C12', side.c12], ['C13', side.c13]] as const) {
|
||
for (const r of rows) {
|
||
const xw = mm(r.xw);
|
||
addBar(name, 0, [xw, r.z, -sideFace], [L - xw, r.z, -sideFace], dSide, COLOR.shear);
|
||
addBar(name, 0, [xw, r.z, sideFace], [L - xw, r.z, sideFace], dSide, COLOR.shear);
|
||
const zh = r.z + mm(dSide);
|
||
for (const x0 of [xw, L - xw]) {
|
||
const leg = mm(SIDE_HOOK_LEG) * Math.sign(L / 2 - x0);
|
||
addBar(name, 0, [x0, zh, -sideFace], [x0, zh, sideFace], dSide, COLOR.shear);
|
||
addBar(name, 0, [x0, zh, -sideFace], [x0 + leg, zh, -sideFace], dSide, COLOR.shear);
|
||
addBar(name, 0, [x0, zh, sideFace], [x0 + leg, zh, sideFace], dSide, COLOR.shear);
|
||
}
|
||
}
|
||
}
|
||
|
||
// C14 ⊓ (코핑철근.md §8): across the width at each x, both ends down 15.5·d_b.
|
||
const c14Hook = mm(hookLength('main90', NOMINAL_DIAMETER.H19));
|
||
const c14Na = -B / 2 + mm(C14_INSET);
|
||
const c14Nb = B / 2 - mm(C14_INSET);
|
||
for (const b of c14Bars(L * 1000, topEl, [...side.c12, ...side.c13], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth))) {
|
||
const xm = mm(b.x);
|
||
addBar('C14', 0, [xm, b.z, c14Na], [xm, b.z, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
// 연직 발 below the coping underside is left out (코핑철근.md §8).
|
||
if (b.legs) {
|
||
addBar('C14', 0, [xm, b.z - c14Hook, c14Na], [xm, b.z, c14Na], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
addBar('C14', 0, [xm, b.z, c14Nb], [xm, b.z - c14Hook, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
}
|
||
}
|
||
// C15 (거꾸로 된 U) and C16 (L형) on the stair (코핑철근.md §7): 11 bars each across the width.
|
||
const stairs = stairBars(L * 1000, B * 1000, topEl, mCoping);
|
||
for (const s of stairs.c15) {
|
||
const n = mm(s.n);
|
||
const ld = mm(s.leg);
|
||
const x0 = mm(s.x0);
|
||
const x1 = mm(s.x1);
|
||
addBar('C15', 0, [x0, s.z - ld, n], [x0, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
addBar('C15', 0, [x0, s.z, n], [x1, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
addBar('C15', 0, [x1, s.z, n], [x1, s.z - ld, n], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
}
|
||
for (const s of stairs.c16) {
|
||
const n = mm(s.n);
|
||
const ld = mm(s.leg);
|
||
const x0 = mm(s.x0);
|
||
const x1 = mm(s.x1);
|
||
addBar('C16', 0, [x0, s.z - ld, n], [x0, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
addBar('C16', 0, [x0, s.z, n], [x1, s.z, n], NOMINAL_DIAMETER.H19, COLOR.coping);
|
||
}
|
||
}
|
||
|
||
// Columns: P1 / P2 main bars on their radii, ties P3 / P5 / P4 as rings, P6 crossties across the column.
|
||
const colInput = columnInputFor(m);
|
||
const col = columnRebar(colInput, mColumn);
|
||
const zBot = col.zBot; // mm
|
||
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);
|
||
// 90° bottom hook of P1, outward (기둥철근.md §3.2, Q-C5): the 12·d tail is drawn, the bend is approximated by the tail's start.
|
||
const tail = mm(12 * NOMINAL_DIAMETER.S32);
|
||
const rOut = r1 + tail;
|
||
addBar('P1', c.k, [sc + r1 * Math.cos(th), base + mm(zBot), r1 * Math.sin(th)], [sc + rOut * Math.cos(th), base + mm(zBot), rOut * 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);
|
||
|
||
// Main bar couplers (D32, L150 Ø60, 4단 × 46줄 × 2열 = 368개): P1 at col.couplerP1, P2 at col.couplerP2 (+150 mm)
|
||
const r1 = mm(col.r1);
|
||
const r2 = mm(col.r2);
|
||
for (let i = 0; i < COLUMN_P1.nDiv; i++) {
|
||
const th = (2 * Math.PI * i) / COLUMN_P1.nDiv;
|
||
const cosTh = Math.cos(th);
|
||
const sinTh = Math.sin(th);
|
||
// P1 couplers (184)
|
||
for (const z of col.couplerP1) {
|
||
const cx = sc + r1 * cosTh;
|
||
const cz = r1 * sinTh;
|
||
const cy = base + mm(z);
|
||
addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain);
|
||
}
|
||
// P2 couplers (184, shifted +150 mm)
|
||
for (const z of col.couplerP2) {
|
||
const cx = sc + r2 * cosTh;
|
||
const cz = r2 * sinTh;
|
||
const cy = base + mm(z);
|
||
addBar('주철근 커플러', c.k, [cx, cy - 0.075, cz], [cx, cy + 0.075, cz], 60, COLOR.couplerMain);
|
||
}
|
||
}
|
||
|
||
// Tie couplers (D19, L100 Ø40, 총 35개): P5 1개 (19개) · P4 2개 (16개)
|
||
col.p5.forEach((z, j) => {
|
||
const th = (2 * Math.PI * (j * 7 + 0.5)) / COLUMN_P1.nDiv;
|
||
const cx = sc + rt * Math.cos(th);
|
||
const cz = rt * Math.sin(th);
|
||
const cy = base + mm(z);
|
||
const dx = 0.05 * -Math.sin(th);
|
||
const dz = 0.05 * Math.cos(th);
|
||
addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie);
|
||
});
|
||
col.p4.forEach((z) => {
|
||
const angles = [(2 * Math.PI * 0.5) / COLUMN_P1.nDiv, (2 * Math.PI * 0.5) / COLUMN_P1.nDiv + Math.PI];
|
||
angles.forEach((th) => {
|
||
const cx = sc + rt * Math.cos(th);
|
||
const cz = rt * Math.sin(th);
|
||
const cy = base + mm(z);
|
||
const dx = 0.05 * -Math.sin(th);
|
||
const dz = 0.05 * Math.cos(th);
|
||
addBar('띠철근 커플러', c.k, [cx - dx, cy, cz - dz], [cx + dx, cy, cz + dz], 40, COLOR.couplerTie);
|
||
});
|
||
});
|
||
|
||
const hookP6 = mm(Math.round(hookLength('tie90', NOMINAL_DIAMETER.H16))); // 135 mm
|
||
const dP6 = NOMINAL_DIAMETER.H16; // 15.9 mm
|
||
const p6Angles = [0, 46.96, 90, 133.04];
|
||
for (const z of col.p6Levels) {
|
||
p6Angles.forEach((deg, i) => {
|
||
const a = (deg * Math.PI) / 180;
|
||
const xA = sc - rt * Math.cos(a);
|
||
const zA = -rt * Math.sin(a);
|
||
const xB = sc + rt * Math.cos(a);
|
||
const zB = rt * Math.sin(a);
|
||
// Vertical stacking offset: ±1.5 db, ±0.5 db to avoid interference at the column centre (Q-C15)
|
||
const zOffset = (i - 1.5) * dP6;
|
||
const yBar = base + mm(z + zOffset);
|
||
// Main crosstie across the diameter
|
||
addBar('P6', c.k, [xA, yBar, zA], [xB, yBar, zB], dP6, COLOR.tie);
|
||
// 90° hooks at both ends turning downward (135 mm, wrapping the tie)
|
||
addBar('P6', c.k, [xA, yBar, zA], [xA, yBar - hookP6, zA], dP6, COLOR.tie);
|
||
addBar('P6', c.k, [xB, yBar, zB], [xB, yBar - hookP6, zB], dP6, 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 off = legOffsets();
|
||
// One name, several cylinders: each segment of the path is one bar piece.
|
||
const path = (name: string, pts: [number, number, number][], dMm: number) => {
|
||
addPath(name, 0, pts, dMm, COLOR.footing);
|
||
};
|
||
// B1–B4: along s, at W position n. legTo (absolute EL, m) is where both ends turn up or down (§5.1); undefined = straight.
|
||
const along = (name: string, n: number, y: number, legTo: number | undefined, dMm: number) => {
|
||
const at = (t: number, h: number): [number, number, number] => [f.s + t, h, n];
|
||
path(name, legTo === undefined ? [at(sA, y), at(sB, y)] : [at(sA, legTo), at(sA, y), at(sB, y), at(sB, legTo)], dMm);
|
||
};
|
||
// B5–B8: across, at L position s. Same rule, with the ends on the W faces (§6.2).
|
||
const across = (name: string, s: number, y: number, legTo: number | undefined, dMm: number) => {
|
||
const at = (t: number, h: number): [number, number, number] => [f.s + s, h, t];
|
||
path(name, legTo === undefined ? [at(nA, y), at(nB, y)] : [at(nA, legTo), at(nA, y), at(nB, y), at(nB, legTo)], dMm);
|
||
};
|
||
for (let j = 0; j <= gW.n; j++) {
|
||
const n = posW(j);
|
||
if (j % 2 === 1) {
|
||
// B1 ⊓ down, B2 ⊔ up to the B1 height, B3 ⊔ up. B1·B3 sit beside the B2 leg (+25.4 / +22.25, §5.3).
|
||
along('B1', n + mm(off.B1), bottom + mm(z.B1), bottom + mm(z.B1) - mm(legLength('S25')), NOMINAL_DIAMETER.S25);
|
||
along('B2', n, bottom + mm(z.B24), bottom + mm(z.B1), NOMINAL_DIAMETER.S25);
|
||
along('B3', n + mm(off.B3), bottom + mm(z.B3), bottom + mm(z.B3) + mm(legLength('S19')), NOMINAL_DIAMETER.S19);
|
||
} else {
|
||
along('B4', n, bottom + mm(z.B24), undefined, NOMINAL_DIAMETER.S25);
|
||
}
|
||
}
|
||
for (let i = 0; i <= gL.n; i++) {
|
||
const s = posL(i);
|
||
if (i % 2 === 1) {
|
||
// B5 ⊓ down, B6 ⊔ up to the B5 height, B7 ⊔ up. B5·B7 sit beside the B6 leg (+28.6 / +23.85, §6.3).
|
||
across('B5', s + mm(off.B5), bottom + mm(z.B5), bottom + mm(z.B5) - mm(legLength('S29')), NOMINAL_DIAMETER.S29);
|
||
across('B6', s, bottom + mm(z.B68), bottom + mm(z.B5), NOMINAL_DIAMETER.S29);
|
||
across('B7', s + mm(off.B7), bottom + mm(z.B7), bottom + mm(z.B7) + mm(legLength('S19')), NOMINAL_DIAMETER.S19);
|
||
} else {
|
||
across('B8', s, bottom + mm(z.B68), undefined, NOMINAL_DIAMETER.S29);
|
||
}
|
||
}
|
||
// B9 (확대기초철근 (1).md §9, rule applied): vertical H16 bars on the grid, outside the column zones, with 135° hooks.
|
||
// Each hook tail (6·d) turns toward the footing centre along the pier axis (Q-B19).
|
||
const dB9 = NOMINAL_DIAMETER.H16;
|
||
const tail9 = (6 * dB9) / 1000;
|
||
const colX = m.columns.filter((c) => Math.abs(c.s - f.s) <= Lf / 2).map((c) => (c.s - f.s) * 1000);
|
||
const yB9Bot = bottom + mm(z.B24);
|
||
const yB9Top = bottom + mm(z.B5);
|
||
for (const b of b9Bars(Lf * 1000, Wf * 1000, colX).bars) {
|
||
const x = mm(b.x);
|
||
const n = mm(b.n);
|
||
const tailDir = x >= 0 ? -1 : 1;
|
||
addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x, yB9Top, n], dB9, COLOR.footing);
|
||
addBar('B9', 0, [f.s + x, yB9Top, n], [f.s + x + tailDir * tail9, yB9Top, n], dB9, COLOR.footing);
|
||
addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x + tailDir * tail9, yB9Bot, n], dB9, COLOR.footing);
|
||
}
|
||
|
||
// B10-1 / B10-2 (§7) at each B10 level, B11 / B12 (§8) between them. Ends turn 90° inward (B10) or down (B11, B12).
|
||
const polyline = (name: string, pts: [number, number, number][], dMm: number) => {
|
||
addPath(name, 0, pts, dMm, COLOR.footing);
|
||
};
|
||
const hookB10 = mm(hookLength('main90', NOMINAL_DIAMETER.S19)); // 296
|
||
const hookAsm = mm(Math.round(hookLength('main90', NOMINAL_DIAMETER.S16))); // 246
|
||
const d1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19); // B10-1 depth (mm), on the W faces
|
||
const d2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19); // B10-2 depth (mm), on the L faces
|
||
const sEdge = Lf / 2 - mm(d2);
|
||
const nEdge = Wf / 2 - mm(d1);
|
||
const dB10 = mm(NOMINAL_DIAMETER.S19);
|
||
for (let j = 0; j < b10Levels(Hf); j++) {
|
||
const zj = bottom + mm(FOOTING_DEFAULTS.cBottom + j * FOOTING_DEFAULTS.sV);
|
||
for (const sg of [-1, 1]) {
|
||
// B10-2: across W at the L faces; its ends turn inward along s.
|
||
const sp = f.s + sg * sEdge;
|
||
polyline('B10-2', [[sp - sg * hookB10, zj, -nEdge], [sp, zj, -nEdge], [sp, zj, nEdge], [sp - sg * hookB10, zj, nEdge]], NOMINAL_DIAMETER.S19);
|
||
// B10-1: along s at the W faces, 19.1 above B10-2; its ends turn inward along n.
|
||
const np = sg * nEdge;
|
||
const zb = zj + dB10;
|
||
polyline('B10-1', [[f.s - sEdge, zb, np - sg * hookB10], [f.s - sEdge, zb, np], [f.s + sEdge, zb, np], [f.s + sEdge, zb, np - sg * hookB10]], NOMINAL_DIAMETER.S19);
|
||
}
|
||
}
|
||
// B11 (across W, spread along L) and B12 (along L, spread across W) at the assembly levels, 4s apart, 2s stagger per level.
|
||
const xsL = gridPositions(Lf * 1000);
|
||
const ysW = gridPositions(Wf * 1000);
|
||
const dAsm = assemblyEndOffset();
|
||
const nAsm = Wf / 2 - mm(d1 + dAsm);
|
||
const sAsm = Lf / 2 - mm(d2 + dAsm);
|
||
assemblyJs(Hf).forEach((jA, k) => {
|
||
const zA = bottom + mm(FOOTING_DEFAULTS.cBottom + jA * FOOTING_DEFAULTS.sV);
|
||
const zB = zA + dB10;
|
||
const i0 = k % 2 === 0 ? 0 : 2;
|
||
for (let iL = i0; iL < xsL.length; iL += 4) {
|
||
const x = f.s + mm(xsL[iL]);
|
||
polyline('B11', [[x, zA - hookAsm, -nAsm], [x, zA, -nAsm], [x, zA, nAsm], [x, zA - hookAsm, nAsm]], NOMINAL_DIAMETER.S16);
|
||
}
|
||
for (let iW = i0; iW < ysW.length; iW += 4) {
|
||
const n = mm(ysW[iW]);
|
||
polyline('B12', [[f.s - sAsm, zB - hookAsm, n], [f.s - sAsm, zB, n], [f.s + sAsm, zB, n], [f.s + sAsm, zB - hookAsm, n]], NOMINAL_DIAMETER.S16);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Bearing blocks: S2 (upper, along s) and S1 (lower, along n), 8 bars each across the 1200 block.
|
||
// Positions from barsAcross (162.5 from the block edge, @125), bars a − 2·c_s long, legs 700 down at both ends (§3–5).
|
||
const across = barsAcross();
|
||
const half = (BLOCK_REBAR.dim - 2 * BLOCK_REBAR.cover) / 2000;
|
||
const legM = BLOCK_REBAR.legLength / 1000;
|
||
const offsets = Array.from({ length: across.count }, (_, i) => (across.firstEnd + i * across.spacing) / 1000 - BLOCK_REBAR.dim / 2000);
|
||
const dBar = NOMINAL_DIAMETER.H19;
|
||
// The bars are drawn in the block's own frame (a along its s side, c along its n side) and turned with the block.
|
||
for (const b of m.blocks) {
|
||
const z2 = b.top - BLOCK_REBAR.depthS2 / 1000;
|
||
const z1 = b.top - BLOCK_REBAR.depthS1 / 1000;
|
||
const P = (a: number, c: number, y: number): [number, number, number] => {
|
||
const [s, n] = blockFramePoint(b, a, c);
|
||
return [s, y, n];
|
||
};
|
||
for (const off of offsets) {
|
||
addBar('S2', b.k, P(-half, off, z2), P(half, off, z2), dBar, COLOR.block);
|
||
addBar('S2', b.k, P(-half, off, z2 - legM), P(-half, off, z2), dBar, COLOR.block);
|
||
addBar('S2', b.k, P(half, off, z2), P(half, off, z2 - legM), dBar, COLOR.block);
|
||
addBar('S1', b.k, P(off, -half, z1), P(off, half, z1), dBar, COLOR.block);
|
||
addBar('S1', b.k, P(off, -half, z1 - legM), P(off, -half, z1), dBar, COLOR.block);
|
||
addBar('S1', b.k, P(off, half, z1), P(off, half, z1 - legM), dBar, COLOR.block);
|
||
}
|
||
}
|
||
|
||
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('커플러'));
|
||
for (const [key, meshes] of byName) {
|
||
const on = isOn(key);
|
||
for (const mesh of meshes) {
|
||
const mat = mesh.material as THREE.MeshStandardMaterial;
|
||
mat.transparent = !on;
|
||
mat.opacity = on ? 1 : 0.18;
|
||
mat.emissive.setHex(names !== null && on ? 0x6b5a00 : 0x000000);
|
||
mesh.visible = !isolate || on;
|
||
}
|
||
}
|
||
};
|
||
const colorOf = (mesh: THREE.Mesh, mode: RebarColorMode): number => {
|
||
const data = mesh.userData as { roleColor: number; dMm: number; member: RebarMemberKey };
|
||
if (mode === 'role') return data.roleColor;
|
||
if (mode === 'member') return MEMBER_COLORS[data.member];
|
||
const idx = diameterGroupIndex(data.dMm);
|
||
return idx < 0 ? OTHER_COLOR : DIAMETER_COLORS[idx];
|
||
};
|
||
const setColorMode = (mode: RebarColorMode) => {
|
||
for (const meshes of byName.values()) {
|
||
for (const mesh of meshes) (mesh.material as THREE.MeshStandardMaterial).color.copy(rebarColor(colorOf(mesh, mode)));
|
||
}
|
||
};
|
||
const legend = (mode: RebarColorMode) => {
|
||
const all = [...byName.values()].flat().map((mesh) => mesh.userData as { dMm: number; member: RebarMemberKey });
|
||
if (mode === 'role') return [];
|
||
if (mode === 'member') {
|
||
const present = new Set(all.map((d) => d.member));
|
||
return (Object.keys(MEMBER_LABELS) as RebarMemberKey[]).filter((k) => present.has(k)).map((k) => ({ label: MEMBER_LABELS[k], color: hex(MEMBER_COLORS[k]) }));
|
||
}
|
||
const present = new Set(all.map((d) => diameterGroupIndex(d.dMm)));
|
||
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, show, setColorMode, legend };
|
||
}
|