Each member can take its own fck on its ② card. A member without one uses the ③ 공통 fck, and fy stays common. The rebar table and the 3D model both read the resolved value. The I02 note stays until every fck member has its own value. The project file stores the values as optional fckByMember and refuses a bad value on open. Changing the fck is not a shape change, so it is not counted in the pending changes. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
441 lines
23 KiB
TypeScript
441 lines
23 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, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping';
|
||
import { columnRebar, COLUMN_P1 } from '../core/rebar/column';
|
||
import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block';
|
||
import { DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, 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,
|
||
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;
|
||
|
||
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 (코핑철근.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],
|
||
];
|
||
for (let s = 0; s + 1 < pts.length; s++) addBar(name, 0, pts[s], pts[s + 1], 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 - COPING_REBAR.levelWidth / 2000;
|
||
const under = (x: number) => undersideAt(L * 1000, x * 1000, m.endBottom, m.levelBottom);
|
||
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]) {
|
||
for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], 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]];
|
||
// 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) / 1000;
|
||
const bot = top - h;
|
||
const capBot = top - (h - CAP.cover / 1000);
|
||
const xc = xm + mm(NOMINAL_DIAMETER.S25);
|
||
for (let i = 0; i < 3; i++) {
|
||
const a = legN(i);
|
||
const b = legN(i + 3);
|
||
addBar(name, 0, [xm, bot, a], [xm, bot, b], dShear, COLOR.shear);
|
||
addBar(name, 0, [xm, bot, a], [xm, top, a], dShear, COLOR.shear);
|
||
addBar(name, 0, [xm, bot, b], [xm, 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 side = copingSideBars(L * 1000, 41.288, 42.538);
|
||
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))) {
|
||
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 col = columnRebar(COLUMN_P1, 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) => {
|
||
for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], 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) => {
|
||
for (let k = 0; k + 1 < pts.length; k++) addBar(name, 0, pts[k], pts[k + 1], 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;
|
||
for (const b of m.blocks) {
|
||
const z2 = b.top - BLOCK_REBAR.depthS2 / 1000;
|
||
const z1 = b.top - BLOCK_REBAR.depthS1 / 1000;
|
||
for (const off of offsets) {
|
||
const n2 = b.n + off;
|
||
addBar('S2', b.k, [b.s - half, z2, n2], [b.s + half, z2, n2], dBar, COLOR.block);
|
||
addBar('S2', b.k, [b.s - half, z2 - legM, n2], [b.s - half, z2, n2], dBar, COLOR.block);
|
||
addBar('S2', b.k, [b.s + half, z2, n2], [b.s + half, z2 - legM, n2], dBar, COLOR.block);
|
||
const s1 = b.s + off;
|
||
addBar('S1', b.k, [s1, z1, b.n - half], [s1, z1, b.n + half], dBar, COLOR.block);
|
||
addBar('S1', b.k, [s1, z1 - legM, b.n - half], [s1, z1, b.n - half], dBar, COLOR.block);
|
||
addBar('S1', b.k, [s1, z1, b.n + half], [s1, z1 - legM, b.n + half], dBar, 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 ||
|
||
(name === '커플러' && key.includes('커플러')) ||
|
||
(name === '주철근 커플러' && key === '주철근 커플러') ||
|
||
(name === '띠철근 커플러' && key === '띠철근 커플러');
|
||
mat.transparent = !on;
|
||
mat.opacity = on ? 1 : 0.18;
|
||
const match = name !== null && (key === name || (name === '커플러' && key.includes('커플러')));
|
||
mat.emissive.setHex(match ? 0x6b5a00 : 0x000000);
|
||
}
|
||
}
|
||
};
|
||
return { group, byName, highlight };
|
||
}
|