feat(rebar): add downward 90° hooks (135 mm) to P6 crossties in 3D rebar model

This commit is contained in:
ypshim committed 2026-10-22 17:45:22 +09:00
1 parent 4aa8dbb3cb
commit a2317caf6c
15 files changed
+740 -133

No files matched your search

+11 -5
View File
@@ -10,8 +10,6 @@ export const BLOCK_REBAR = {
cover: 50,
firstBarGap: 75,
spacing: 125,
/** Straight part of the bar for the 1200 block (spec table: 1100 + 700 × 2). */
straight: 1100,
legLength: 700,
/** Depth of each bar centre below the block top (mm). */
depthS2: 50,
@@ -40,10 +38,15 @@ export function barsAcross(dim: number = BLOCK_REBAR.dim): { count: number; spac
return { count: gaps + 1, spacing: BLOCK_REBAR.spacing, firstEnd: BLOCK_REBAR.cover + BLOCK_REBAR.firstBarGap + leftover / 2 };
}
/** Length of one bar (mm): the straight part a − 2·c_s plus two legs of 700 (spec §5; 2500 for the 1200 block). */
export function barLength(dim: number = BLOCK_REBAR.dim): number {
return dim - 2 * BLOCK_REBAR.cover + 2 * BLOCK_REBAR.legLength;
}
/** Rebar of one block; blockHeight is the bearing block height in mm (h1 front or h2 back, pad included). */
export function blockBars(blockHeight: number, dim: number = BLOCK_REBAR.dim): BarRow[] {
const across = barsAcross(dim);
const length = BLOCK_REBAR.straight + 2 * BLOCK_REBAR.legLength;
const length = barLength(dim);
return [
{ name: 'S1', ...across, length, embedment: BLOCK_REBAR.legLength - (blockHeight - BLOCK_REBAR.depthS1) },
{ name: 'S2', ...across, length, embedment: BLOCK_REBAR.legLength - (blockHeight - BLOCK_REBAR.depthS2) },
@@ -56,12 +59,15 @@ export interface BlockRebar {
bars: BarRow[];
}
/** Rebar of every block of a pier, using the model's block heights (h1 front, h2 back; pad included, Q16 결정). */
/**
* Rebar of every block of a pier. The block height h_b is the model's own block (coping top → block top, pad excluded),
* since the bar depths are measured from the block top. h1 / h2 include the 50 pad and would count it twice.
*/
export function bearingBlockRebar(model: PierModel): BlockRebar[] {
return model.blocks.map((b) => ({
no: b.no,
row: b.row,
bars: blockBars(b.row === '전열' ? model.h1 * 1000 : model.h2 * 1000),
bars: blockBars((b.top - b.bottom) * 1000),
}));
}
+13 -1
View File
@@ -97,7 +97,15 @@ export function columnRebar(x: ColumnInput = COLUMN_P1, m: Material = DEFAULT_MA
const zTop1 = x.Hc + x.hcop - x.eP1;
const lD2 = developmentLength(dS32, m.fyS, m.fck);
const zTop2 = x.Hc + lD2;
// Bottom of P1 and P2: 확대기초 하면 2단 (B7) 위 + (d_B7 + d_P1)/2 (기둥철근.md §3.2, the same for both).
const zBot = -x.Hf + layers(x.Hf).B7 + (NOMINAL_DIAMETER.S19 + dS32) / 2;
const couplerLevels = Array.from({ length: Math.max(0, Math.floor(zTop1 / x.ctcCoupler) - 1) }, (_, i) => (i + 1) * x.ctcCoupler);
// Pieces of each main bar between its couplers (기둥철근.md §3.2): the bottom piece carries the 90° hook (16.5·d = 525).
const p1Hook = Math.round(hookLength('main90', dS32));
const p1Points = [zBot, ...couplerLevels, zTop1];
const p1Pieces = p1Points.slice(1).map((z, i) => round(z - p1Points[i] + (i === 0 ? p1Hook : 0)));
const p2Points = [zBot, ...couplerLevels.map((z) => z + x.dzCp2), zTop2];
const p2Pieces = p2Points.slice(1).map((z, i) => round(z - p2Points[i]));
const couplersP1 = couplerLevels.length;
const mainLines = x.nDiv;
const mainP1 = mainLines * (couplersP1 + 1);
@@ -126,7 +134,7 @@ export function columnRebar(x: ColumnInput = COLUMN_P1, m: Material = DEFAULT_MA
for (let z = z4first; z <= x.Hc; z += x.ctcP6) p6Levels.push(z);
const p6Straight = 2 * tieRadius;
const p6Hook = hookLength('tie90', dH16);
const p6Length = p6Straight + 2 * p6Hook;
const p6Length = p6Straight + 2 * Math.round(p6Hook); // 135 mm per hook, as in the document
// P3 lap: Q-C6 is open; recommended = 1.3·l_d of H19 with the material inputs.
const lapP3 = spliceLength(dH19, m.fyH, m.fck);
@@ -142,6 +150,10 @@ export function columnRebar(x: ColumnInput = COLUMN_P1, m: Material = DEFAULT_MA
angleStepDeg: 360 / x.nDiv,
zTop1: round(zTop1),
zTop2: round(zTop2),
zBot: round(zBot, 2),
p1Hook,
p1Pieces,
p2Pieces,
couplerP1: couplerLevels,
couplerP2: couplerLevels.map((z) => z + x.dzCp2),
mainP1,
+148 -4
View File
@@ -2,6 +2,7 @@
* 코핑 철근 (plan C3). Source: RAG/코핑철근.md, 2026-10-22 최종 정리 (spec §6.4 is older and is not used here).
* Positions are along the coping length from its start, in mm.
*/
import { DEFAULT_MATERIAL, developmentLength, hookLength, Material, NOMINAL_DIAMETER } from './common';
export const COPING_REBAR = {
/** 피복 (최외측 철근 중심까지). */
cover: 100,
@@ -15,6 +16,8 @@ export const COPING_REBAR = {
spacingS: 250,
/** C12·C13 측면 수평 간격. */
spacingSide: 250,
/** 테이퍼 높이 h2 (코핑철근.md §2). */
taperHeight: 1500,
/** C14 위 2줄 간격 / 아래 줄 수평 간격 / 아래 줄 연직 간격 / 끝 여유. */
c14: { upperSpacing: 1000, lowerSpacing: 2000, verticalSpacing: 500, endGap: 500 },
} as const;
@@ -39,7 +42,7 @@ export interface ShearBars {
*/
export function shearBars(coping: number, xc = coping / 2): ShearBars {
const { spacingL, spacingS, levelWidth } = COPING_REBAR;
const kMax = Math.floor(levelWidth / 2 / spacingL) + 1;
const kMax = Math.ceil(levelWidth / 2 / spacingL); // k_max = ceil((W_L/2)/s_L), spec §3.1
const c10 = Array.from({ length: 2 * kMax + 1 }, (_, i) => Math.round(xc + (i - kMax) * spacingL));
const left = c10[0];
const right = c10[c10.length - 1];
@@ -90,12 +93,153 @@ export function shearULength(x: number, coping: number): number {
return SHEAR_U.flat + 2 * shearUHeight(x, coping);
}
/** C12 straight lengths per row (코핑철근.md §6 table, mm) and C13. */
export const SIDE_LENGTHS = { C12: [3717, 5333, 6950, 8567, 10183], C13: 11746 } as const;
/** C13 sits 127 in from each end face (코핑철근.md §6.2). */
export const SIDE_C13_END = 127;
/** Leg of each ㄷ형 bar along the side, 1.3·l_d = 1525 (Q-K9). */
export const SIDE_HOOK_LEG = 1525;
export interface SideBar {
/** Row height (m, EL). */
z: number;
/** Distance from each end face to where the 일자 bar starts (mm). */
xw: number;
/** Length of the 일자 bar, L − 2·x_w (mm). */
straight: number;
}
/**
* C12 and C13 rows for a coping of length L (mm), T형 (코핑철근.md §6.2).
* C12 meets the sloped bottom at x_w = c + (z_break − z_k)·(x_L0 − c)/h2, with x_L0 = L/2 − W_L/2.
* C13 starts at x_w = 127. Each row has two 일자 bars (side faces) and two ㄷ bars (one at each end).
*/
export function copingSideBars(L: number, zBottom: number, zBreak: number): { c12: SideBar[]; c13: SideBar[] } {
const c = COPING_REBAR.cover;
const xL0 = L / 2 - COPING_REBAR.levelWidth / 2;
const bar = (z: number, xw: number): SideBar => ({ z, xw, straight: L - 2 * xw });
const rows = sideRows(zBottom, zBreak);
return {
c12: rows.c12.map((z) => bar(z, c + ((zBreak - z) * 1000 * (xL0 - c)) / COPING_REBAR.taperHeight)),
c13: rows.c13.map((z) => bar(z, SIDE_C13_END)),
};
}
/** C15 (거꾸로 된 U, 3852) and C16 (L형, 4070 / 4069) lengths, mm (코핑철근.md §7). */
export const STEP_LENGTHS = { C15: 3852, C16: [4070, 4069] as const, C14: 3447.5 } as const;
/**
* Stair zone of the P1 coping (코핑철근.md §7.1): each segment from x to x (mm from the coping start), and its top rise
* above the lowest top (mm). The stair positions are the document's; the model has no stair geometry yet.
*/
export const STAIR_SEGMENTS = [
{ from: 2196, to: 4701, rise: 72 }, // 2구간
{ from: 4701, to: 7207, rise: 144 }, // 3구간
{ from: 7207, to: 9712, rise: 215 }, // 4구간
{ from: 9712, to: 12000, rise: 287 }, // 5구간 (to = L in the document)
] as const;
/** C14 end gap e_14 (코핑철근.md §8.1) and the inset of its ends from the side faces: c + (d_shear + d_14)/2 = 122.25. */
const C14_END_GAP = 500;
export const C14_INSET = COPING_REBAR.cover + (NOMINAL_DIAMETER.H25 + NOMINAL_DIAMETER.H19) / 2;
/** Development length l_d of H19 (882 for fy 400, fck 27), rounded to mm as the document does (§7.2). */
export function stepDevLength(m: Material = DEFAULT_MATERIAL): number {
return Math.round(developmentLength(NOMINAL_DIAMETER.H19, m.fyH, m.fck));
}
export interface C14Bar {
/** 1–2: upper lines (C2, C3). 3–6: lower rows from the 7th, 5th, 3rd and 1st side rows. */
line: number;
/** Position along the coping (mm from its start). */
x: number;
/** EL of the bar (m). */
z: number;
/** false when the 연직 발 would end below the coping underside at x, so both legs are left out (§8). */
legs: boolean;
}
/** Underside of the coping (EL m) at x (mm from the start), T형: sloped from each end to the Level zone, flat in the middle. */
export function undersideAt(L: number, x: number, endBottom: number, levelBottom: number): number {
const xL0 = L / 2 - COPING_REBAR.levelWidth / 2;
return endBottom + (levelBottom - endBottom) * Math.min(Math.min(x, L - x) / xL0, 1);
}
/**
* C14 ⊓ bars across the width (코핑철근.md §8.1). Upper lines hang from C2 and C3 at @1000 from the column centre.
* Lower rows use the side rows 7, 5, 3, 1 from the bottom (k = m − 2(j − 1)): odd j at x_c ± 2000k, even j at x_c ± 1000 ± 2000k,
* only where that side row exists (x_w ≤ x ≤ L − x_w). `sideRows` are the side rows (z m, x_w mm), ascending.
*/
export function c14Bars(L: number, topEl: number, sideRows: { z: number; xw: number }[], under: (x: number) => number): C14Bar[] {
const leg = Math.round(hookLength('main90', NOMINAL_DIAMETER.H19)) / 1000; // 296 mm, EL m
const xc = L / 2;
const lo = COPING_REBAR.cover + C14_END_GAP;
const hi = L - lo;
const out: C14Bar[] = [];
const dUpper = (NOMINAL_DIAMETER.H29 + NOMINAL_DIAMETER.H19) / 2000; // m, over C2 / C3 (H29)
for (const k of [2, 3]) {
const z = topEl - 0.127 - (k - 1) * 0.057 + dUpper;
for (let q = -10; q <= 10; q++) {
const x = xc + q * 1000;
if (x >= lo && x <= hi) out.push({ line: k - 1, x, z, legs: z - leg >= under(x) });
}
}
const dLower = (NOMINAL_DIAMETER.H25 + NOMINAL_DIAMETER.H19) / 2000; // m, over a H25 side row
for (let j = 1; 7 - 2 * (j - 1) >= 1; j++) {
const row = sideRows[7 - 2 * (j - 1) - 1];
const a = Math.max(lo, row.xw);
const b = Math.min(hi, L - row.xw);
for (let q = -10; q <= 10; q++) {
const x = xc + (j % 2 === 1 ? 0 : 1000) + 2000 * q;
if (x >= a && x <= b) out.push({ line: 2 + j, x, z: row.z + dLower, legs: row.z + dLower - leg >= under(x) });
}
}
return out;
}
/** C14 length (mm): the straight run between the inset ends plus two 15.5·d_b legs (§8.2). 3447.5 in the document. */
export function c14Length(B: number): number {
return B - 2 * C14_INSET + 2 * Math.round(hookLength('main90', NOMINAL_DIAMETER.H19)); // legs rounded to mm (296)
}
export interface StepBar {
/** Position across the width, mm from the centre line. */
n: number;
/** Start and end along the coping (mm). */
x0: number;
x1: number;
/** EL of the horizontal run (m). */
z: number;
/** Legs: C15 hangs at both ends, C16 only at its start. */
legs: 'both' | 'start';
/** Leg length l_d (mm). */
leg: number;
/** Developed length (mm): the horizontal run plus the leg(s) at its ends. */
length: number;
}
/**
* C15 (거꾸로 된 U, on the highest segment) and C16 (L형, on segments 3 and 4), 11 bars each across the width (코핑철근.md §7).
* Across: 137 from the cover line, then @250 (피복선 기준). Along: from each segment start + c to the next end − c.
*/
export function stairBars(L: number, B: number, topEl: number, m: Material = DEFAULT_MATERIAL): { c15: StepBar[]; c16: StepBar[] } {
const ld = stepDevLength(m);
const c = COPING_REBAR.cover;
const ns = Array.from({ length: 11 }, (_, k) => -B / 2 + c + 137 + 250 * k);
const seg = STAIR_SEGMENTS;
const top = (i: number) => topEl + seg[i].rise / 1000;
const c15Z = top(3) - c / 1000;
const c16Z = (i: number) => top(i) - c / 1000 - NOMINAL_DIAMETER.H19 / 1000; // (d_C15 + d_C16)/2 = 19.1
const x0c15 = seg[3].from + c;
const x1c15 = L - c;
const c15: StepBar[] = ns.map((n) => ({ n, x0: x0c15, x1: x1c15, z: c15Z, legs: 'both', leg: ld, length: x1c15 - x0c15 + 2 * ld }));
const c16: StepBar[] = [1, 2].flatMap((i) => {
const x0 = seg[i].from + c;
const x1 = seg[i].to - c + ld; // the horizontal run runs on past the segment end by l_d
return ns.map((n): StepBar => ({ n, x0, x1, z: c16Z(i), legs: 'start', leg: ld, length: x1 - x0 + ld }));
});
return { c15, c16 };
}
/**
* 삿갓 (upper cap) of C9–C11 (코핑철근.md §4.3). l_cap is open (Q-K5); recommended here: l_cap = h − c (top cover 100),
* so that l_cap stays below h (RB-K04). Cap length = 1740 + 2·l_cap.
@@ -152,7 +296,7 @@ export function shearBarsMulti(coping: number, columns: number[]): ShearBars & {
return { ...one, between: [] };
}
const { spacingL, spacingS, levelWidth } = COPING_REBAR;
const kMax = Math.floor(levelWidth / 2 / spacingL) + 1;
const kMax = Math.ceil(levelWidth / 2 / spacingL); // k_max = ceil((W_L/2)/s_L), spec §3.1
const groups = columns.map((xc) =>
Array.from({ length: 2 * kMax + 1 }, (_, i) => Math.round(xc + (i - kMax) * spacingL)),
);
+86 -33
View File
@@ -18,11 +18,8 @@ export const FOOTING_DEFAULTS = {
/** B10 높이 간격 s_v and 조립 단 배수 n. */
sV: 200,
n: 2,
/** B1·B3 leg and B5·B7 leg (proposed values; Q-B5 open). */
legB1: 300,
legB3: 230,
legB5: 343,
legB7: 230,
/** 상단 최소 여유 e_top (B10 top level, §7.1). */
eTop: 150,
} as const;
export interface GridResult {
@@ -47,6 +44,9 @@ export function grid(side: number, s: number = FOOTING_DEFAULTS.s, cSide: number
const d = (size: BarSize) => NOMINAL_DIAMETER[size];
/** Leg of B1·B3·B5·B7: 12·d_b on each end (proposed rule, spec §1.2 · Q-B5). */
export const legLength = (size: BarSize) => 12 * d(size);
/** Layer heights (§4.2), mm from the 하면. H = footing height. */
export interface Layers {
B5: number;
@@ -90,9 +90,9 @@ export function bottomBars(L: number, W: number, H: number): FootingBar[] {
const l0 = L - 2 * FOOTING_DEFAULTS.cSide;
const leg2 = legB2(H);
return [
{ name: 'B1', size: 'S25', count: N2, length: l0 + 2 * FOOTING_DEFAULTS.legB1 },
{ name: 'B1', size: 'S25', count: N2, length: l0 + 2 * legLength('S25') },
{ name: 'B2', size: 'S25', count: N2, length: l0 + 2 * leg2 },
{ name: 'B3', size: 'S19', count: N2, length: l0 + 2 * FOOTING_DEFAULTS.legB3 },
{ name: 'B3', size: 'S19', count: N2, length: l0 + 2 * legLength('S19') },
{ name: 'B4', size: 'S25', count: N4, length: l0 },
];
}
@@ -107,28 +107,50 @@ export function crossBars(L: number, W: number, H: number): FootingBar[] {
const z = layers(H);
const leg6 = z.B5 - z.B68;
return [
{ name: 'B5', size: 'S29', count: N6, length: l0 + 2 * FOOTING_DEFAULTS.legB5 },
{ name: 'B5', size: 'S29', count: N6, length: l0 + 2 * legLength('S29') },
{ name: 'B6', size: 'S29', count: N6, length: l0 + 2 * leg6 },
{ name: 'B7', size: 'S19', count: N6, length: l0 + 2 * FOOTING_DEFAULTS.legB7 },
{ name: 'B7', size: 'S19', count: N6, length: l0 + 2 * legLength('S19') },
{ name: 'B8', size: 'S29', count: N8, length: l0 },
];
}
/** B10 levels: m = floor((z_B5 − e_top − c_b)/s_v) (§7.1), so the count is m + 1 (9 for P1, H 2000). */
export function b10Levels(H: number): number {
const m = Math.floor((layers(H).B5 - FOOTING_DEFAULTS.eTop - FOOTING_DEFAULTS.cBottom) / FOOTING_DEFAULTS.sV);
return m + 1;
}
/** Depth of a side bar from the side face: c_s + (d_a + d_b)/2 (§7, §8.3). B10-1 hangs on B6, B10-2 on B2. */
export const sideDepth = (dOuter: number, dB10: number) => FOOTING_DEFAULTS.cSide + (dOuter + dB10) / 2;
/** B10-1 (on W sides) and B10-2 (on L sides): m + 1 levels at s_v, two faces each (§7). */
export function sideBars(L: number, W: number): FootingBar[] {
const depth1 = 123.85; // B10-1 depth from the side
const depth2 = 122.25; // B10-2 depth from the side
const levels = 9; // 8 @ 200 (기본)
export function sideBars(L: number, W: number, H: number): FootingBar[] {
const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19); // B10-1: 123.85
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19); // B10-2: 122.25
const levels = b10Levels(H);
const hook = hookLength('main90', d('S19'));
return [
{ name: 'B10-1', size: 'S19', count: levels * 2, length: L - 2 * depth2 + 2 * hook },
{ name: 'B10-2', size: 'S19', count: levels * 2, length: W - 2 * depth1 + 2 * hookLength('main90', d('S19')) },
{ name: 'B10-2', size: 'S19', count: levels * 2, length: W - 2 * depth1 + 2 * hook },
];
}
/** Assembly levels j = n, 2n, … strictly inside the B10 levels, 0 < j < m (§8.1). */
export function assemblyJs(H: number): number[] {
const m = b10Levels(H) - 1;
const js: number[] = [];
for (let j = FOOTING_DEFAULTS.n; j < m; j += FOOTING_DEFAULTS.n) js.push(j);
return js;
}
/** Ends of B11 / B12 lie (d_B10 + d_assembly)/2 further inside than the B10 depth, so the two bars just touch (§8.3). */
export function assemblyEndOffset(): number {
return (d('S19') + d('S16')) / 2; // 17.5
}
/** B11 (along W, between the B10-2 levels) and B12 (along L), 4s apart with a 2s stagger per level (§8). */
export function assemblyBars(L: number, W: number): FootingBar[] {
const levelsJ = [2, 4, 6]; // j = n, 2n, 3n … inside the B10 levels (n = 2)
export function assemblyBars(L: number, W: number, H: number): FootingBar[] {
const levelsJ = assemblyJs(H);
const perLevel = (n: number, startOdd: number, startEven: number) =>
levelsJ.map((_, idx) => {
const i0 = idx % 2 === 0 ? startOdd : startEven;
@@ -139,31 +161,63 @@ export function assemblyBars(L: number, W: number): FootingBar[] {
// B11 is spread along L (n = 79) and B12 along W (n = 61); each level has its own count.
const b11 = perLevel(nL, 0, 2).reduce((a, b) => a + b, 0);
const b12 = perLevel(nW, 0, 2).reduce((a, b) => a + b, 0);
const hook = hookLength('main90', d('S16'));
const hook = Math.round(hookLength('main90', d('S16'))); // 246 mm, rounded as in the document (§8.3)
const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19);
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19);
return [
{ name: 'B11', size: 'S16', count: b11, length: W - 2 * 141.35 + 2 * hook },
{ name: 'B12', size: 'S16', count: b12, length: L - 2 * 139.75 + 2 * hook },
{ name: 'B11', size: 'S16', count: b11, length: W - 2 * (depth1 + assemblyEndOffset()) + 2 * hook },
{ name: 'B12', size: 'S16', count: b12, length: L - 2 * (depth2 + assemblyEndOffset()) + 2 * hook },
];
}
/** Plane positions of the grid along one side, mm from the footing centre: x_i = −side/2 + c_s + e + i·s (§3.1). */
export function gridPositions(side: number): number[] {
const g = grid(side);
return Array.from({ length: g.positions }, (_, i) => -side / 2 + FOOTING_DEFAULTS.cSide + g.e + i * FOOTING_DEFAULTS.s);
}
/** B9 bar of the rule (§9.1, Q-B18): a vertical bar at (x, n), mm from the footing centre. */
export interface B9Bar {
x: number;
n: number;
}
/**
* B9 전단철근 (확대기초철근 (1).md §9). Wraps B5 (top outer) and B2·B4 (bottom outer), H16 @500 staggered.
* Plane positions and hooks are proposed rules (Q-B18, Q-B19).
* B9 positions (확대기초철근 (1).md §9.1, rule applied): rows at W index 1, 5, 9 … (step 4 on the grid); in row r the L index
* starts at 1 (even r) or 3 (odd r) and steps by 4. A bar is left out when it falls in a column zone: its centre is closer
* than the column radius plus half a bar to a column axis (Q-B20, applied: the 기둥 구역 is removed).
*/
export function shearB9(L: number, W: number, H: number): { rows: number; perRow: number[]; count: number; length: number } {
const nW = grid(W).n; // 61
const nL = grid(L).n; // 79
export function b9Bars(L: number, W: number, columnX: number[], columnD: number = 2000): { rows: number; perRow: number[]; bars: B9Bar[] } {
const xs = gridPositions(L);
const ns = gridPositions(W);
const rowIndices: number[] = [];
for (let i = 1; i <= nW; i += 4) rowIndices.push(i);
// Even-numbered rows start at i_L = 1, odd-numbered rows at i_L = 3 (2s stagger).
const perRow = rowIndices.map((_, r) => {
for (let i = 1; i < ns.length; i += 4) rowIndices.push(i);
const halfBar = NOMINAL_DIAMETER.H16 / 2;
const bars: B9Bar[] = [];
const perRow = rowIndices.map((iW, r) => {
const start = r % 2 === 0 ? 1 : 3;
return Math.floor((nL - start) / 4) + 1;
let kept = 0;
for (let iL = start; iL < xs.length; iL += 4) {
const x = xs[iL];
const n = ns[iW];
if (columnX.some((cx) => Math.hypot(x - cx, n) < columnD / 2 + halfBar)) continue;
bars.push({ x, n });
kept++;
}
return kept;
});
const count = perRow.reduce((a, b) => a + b, 0);
return { rows: rowIndices.length, perRow, bars };
}
/**
* B9 전단철근 (확대기초철근 (1).md §9). Vertical bars from the B2·B4 level to the B5 level, H16, with 135° hooks at both ends.
* Length = l_v + 2·l_h (l_v = z_B5 − z_B24). The hook tail (6·d) turns toward the footing centre (Q-B19, applied).
*/
export function shearB9(L: number, W: number, H: number, columnX: number[] = [], columnD: number = 2000): { rows: number; perRow: number[]; count: number; length: number; bars: B9Bar[] } {
const { rows, perRow, bars } = b9Bars(L, W, columnX, columnD);
const lv = layers(H).B5 - layers(H).B24; // 1750
const lh = hookLength('stirrup135', d('H16'));
return { rows: rowIndices.length, perRow, count, length: lv + 2 * lh };
return { rows, perRow, count: bars.length, length: lv + 2 * lh, bars };
}
/** Leg offsets from the grid line (§5.3, §6.3): the B1/B3 and B5/B7 legs sit beside the outer leg. */
@@ -183,11 +237,10 @@ export function legOffsets(): { B1: number; B3: number; B5: number; B7: number }
}
/** B11/B12 heights (§8.1): B10 levels at 150 + k·s_v; the assembly levels are j = n, 2n, 3n (n = 2), and B12 sits d_B10 above B11. */
export function assemblyHeights(): { B11: number[]; B12: number[] } {
export function assemblyHeights(H: number): { B11: number[]; B12: number[] } {
const { cBottom } = FOOTING_DEFAULTS;
const sV = FOOTING_DEFAULTS.sV;
const n = FOOTING_DEFAULTS.n;
const js = [n, 2 * n, 3 * n];
const js = assemblyJs(H);
const B11 = js.map((j) => cBottom + j * sV);
const B12 = B11.map((z) => z + NOMINAL_DIAMETER.S19);
return { B11, B12 };
+41 -19
View File
@@ -1,7 +1,7 @@
import type { PierModel } from '../geometry';
import { bearingBlockRebar, totals } from './bearing-block';
import { barLength, bearingBlockRebar, totals } from './bearing-block';
import { bottomPiece, topBarLength, widthLayout } from './coping-main';
import { shearBars, shearBarsMulti, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS, shearULength, SIDE_LENGTHS, STEP_LENGTHS, capLength, PIECES_PER_POSITION } from './coping';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { columnRebar, COLUMN_P1 } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, isAssumedMaterial, Material, splitPieces } from './common';
@@ -98,14 +98,14 @@ interface FootingBar {
}
/** The footing bars of one footing (B1–B12 and B9), with the notes used in the 철근표. */
function footingBarList(Lf: number, Wf: number, Hf: number): FootingBar[] {
function footingBarList(Lf: number, Wf: number, Hf: number, columnX: number[]): FootingBar[] {
const out: FootingBar[] = [];
for (const b of bottomBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축' });
for (const b of crossBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축 직각' });
for (const b of sideBars(Lf, Wf)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf);
out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행 (위치·갈고리는 Q-B18·B19 제안)` });
for (const b of sideBars(Lf, Wf, Hf)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf, Hf)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf, columnX);
out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행, 기둥 구역 제외 (위치·갈고리는 Q-B18·B19 규칙 적용)` });
return out;
}
@@ -121,9 +121,20 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
const B = model.copingB * 1000;
// Bearing blocks (§6.3 · plan C2)
const blk = totals(bearingBlockRebar(model));
rows.push({ member: 'bearing', name: 'S1', size: 'H19', count: blk.S1, length: 2500, note: '블럭 배치 분리, 블럭당 8본' });
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: 2500, note: '블럭 배치 분리, 블럭당 8본' });
// Embedment in the coping (받침블럭철근.md §4): l_leg − (block height − bar depth), with the model's block heights.
const blocks = bearingBlockRebar(model);
const blk = totals(blocks);
const embedments = (name: 'S1' | 'S2') => blocks.flatMap((b) => b.bars.filter((r) => r.name === name).map((r) => r.embedment));
const emb1 = embedments('S1');
const emb2 = embedments('S2');
const range = (xs: number[]) => `${Math.round(Math.min(...xs))}–${Math.round(Math.max(...xs))}`;
const anchorage = stepDevLength(m); // RB-S04: the embedment must reach the H19 development length
const shortest = Math.min(...emb1, ...emb2);
if (shortest < anchorage) {
warnings.push({ code: 'RB-S04', message: `받침블럭 철근 묻힘 ${Math.round(shortest)} mm < 정착길이 ${anchorage} mm (코핑 안 묻힘 부족)`, target: 'bearing' });
}
rows.push({ member: 'bearing', name: 'S1', size: 'H19', count: blk.S1, length: barLength(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb1)} (블럭 상면 − 69.1)` });
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: barLength(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb2)} (블럭 상면 − 50)` });
// Coping (코핑철근.md §5–§8)
const width = widthLayout(B);
@@ -157,17 +168,26 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
if (nCol > 1) {
warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 T형 기준으로 두었습니다 (기둥마다 반복 규칙 미정, Q-K11).`, target: 'coping' });
}
const side = sideRows(41.288, 42.538);
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${SIDE_LENGTHS.C12[0]}–${SIDE_LENGTHS.C12[4]}` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: side.c13.length, length: SIDE_LENGTHS.C13, note: '측면, 끝면까지' });
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14Total(), length: STEP_LENGTHS.C14, note: `위 ${C14_COUNTS.upperLines} 줄 ${C14_COUNTS.upperPerLine}본씩, 아래 ${C14_COUNTS.lower.join(' · ')} (합 ${c14Total()})` });
rows.push({ member: 'coping', name: 'C15', size: 'H19', count: STEP_BAR_COUNTS.C15, length: STEP_LENGTHS.C15, note: '계단부, 가장 높은 구간 (거꾸로 U)' });
rows.push({ member: 'coping', name: 'C16', size: 'H19', count: STEP_BAR_COUNTS.C16, length: STEP_LENGTHS.C16[0], note: `계단부, 3·4구간 (${STEP_LENGTHS.C16[0]} / ${STEP_LENGTHS.C16[1]})` });
// Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2).
const side = copingSideBars(model.copingL * 1000, 41.288, 42.538);
const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs
const c12Range = side.c12.length ? `${Math.round(side.c12[0].straight)}–${Math.round(side.c12[side.c12.length - 1].straight)}` : '—';
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: side.c13.length ? side.c13[0].straight : null, note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` });
// C14 / C15 / C16 are derived from the model (코핑철근.md §7–8); the document's 38 / 11 / 22 and 3852 / 4070 / 4069 are checked in the tests.
const L14 = model.copingL * 1000;
const c14 = c14Bars(L14, model.copingRefTop, [...side.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom));
const lineCount = (line: number) => c14.filter((b) => b.line === line).length;
const omitted = c14.filter((b) => !b.legs).length;
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14.length, length: c14Length(model.copingB * 1000), note: `위 2 줄 ${lineCount(1)} · ${lineCount(2)}본씩, 아래 ${[3, 4, 5, 6].map(lineCount).join(' · ')} (합 ${c14.length}) · 연직 발 생략 ${omitted}개 (하면 밖)` });
const stairs = stairBars(model.copingL * 1000, model.copingB * 1000, model.copingRefTop);
rows.push({ member: 'coping', name: 'C15', size: 'H19', count: stairs.c15.length, length: stairs.c15[0].length, note: '계단부, 가장 높은 구간 (거꾸로 U)' });
rows.push({ member: 'coping', name: 'C16', size: 'H19', count: stairs.c16.length, length: stairs.c16[0].length, note: `계단부, 3·4구간 (${Math.round(stairs.c16[0].length)} / ${Math.round(stairs.c16[stairs.c16.length - 1].length)})` });
// Column (기둥철근.md · plan C4), P1 defaults
const col = columnRebar(COLUMN_P1, m);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: '외측 r 875, 마디 4697.5 + 갈고리 525 / 3000 ×3 / 3147 (코핑 밖 마디는 기둥철근.md §3.2)' });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: '내측 r 843.2, 마디 4847.5 / 3000 ×3 / 2389' });
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
rows.push({ member: 'column', name: '커플러', size: 'L150 Ø60', count: col.couplers, length: 150, note: '@3000, P2 +150' });
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` });
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 1개' });
@@ -182,7 +202,9 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
// the length is the longest of them (the largest footing governs the bar).
const agg = new Map<string, FootingBar>();
for (const f of model.footings) {
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000)) {
// Columns on this footing, as x mm from the footing centre (for the B9 column-zone rule).
const columnX = model.columns.filter((c) => Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000);
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, columnX)) {
const cur = agg.get(b.name);
if (cur) {
cur.count += b.count;
+3 -4
View File
@@ -805,10 +805,9 @@ function renderRebar(): void {
const body = el('tbody');
for (const r of shown) {
const tr = el('tr');
const nameCell = el('td', r.name);
nameCell.classList.add('bar-no');
// Clicking a 번호 highlights that row (시안: 번호를 누르면 그 철근만 강조).
nameCell.addEventListener('click', () => {
const nameCell = el('td', r.name, 'bar-no');
// Clicking a row (any cell) focuses that bar in the table and in 3D; clicking it again clears it (시안: 번호를 누르면 그 철근만 강조).
tr.addEventListener('click', () => {
state.rebarFocus = state.rebarFocus === r.name ? null : r.name;
renderAll();
});
+4
View File
@@ -293,6 +293,10 @@ select {
background: #e9eef4;
}
.rebar-table tbody tr { cursor: pointer; }
.rebar-table tr.hl td { background: #fff3cc; }
.rebar-table .bar-no { font-weight: 700; }
.parts {
border: 1px solid var(--line);
border-radius: 6px;
+219 -39
View File
@@ -1,10 +1,11 @@
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 { 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 { NOMINAL_DIAMETER } from '../core/rebar/common';
import { barsAcross, BLOCK_REBAR } from '../core/rebar/bearing-block';
import { hookLength, NOMINAL_DIAMETER } from '../core/rebar/common';
import { stretchMatrix } from './scene';
/**
@@ -61,51 +62,131 @@ export function buildRebar3D(m: PierModel): RebarScene {
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).
// 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 ? 3 : 0)}`;
addBar(name, 0, [cover, z, n], [L - cover, z, n], NOMINAL_DIAMETER.H29, COLOR.coping);
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 along the Level height.
// 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().ls / 2000;
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);
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]];
const ne = B / 2 - 0.1;
// 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 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);
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 at both side faces.
const side = sideRows(41.288, 42.538);
// 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 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);
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);
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);
const zBot = -1697.5;
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++) {
@@ -122,16 +203,30 @@ export function buildRebar3D(m: PierModel): RebarScene {
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);
const hookP6 = mm(Math.round(hookLength('tie90', NOMINAL_DIAMETER.H16))); // 135 mm
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);
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);
const yBar = base + mm(z);
// Main crosstie across the diameter
addBar('P6', c.k, [xA, yBar, zA], [xB, yBar, zB], NOMINAL_DIAMETER.H16, 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], NOMINAL_DIAMETER.H16, COLOR.tie);
addBar('P6', c.k, [xB, yBar, zB], [xB, yBar - hookP6, zB], NOMINAL_DIAMETER.H16, COLOR.tie);
}
}
}
@@ -151,37 +246,122 @@ export function buildRebar3D(m: PierModel): RebarScene {
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);
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) {
along('B1', n, bottom + mm(z.B1), NOMINAL_DIAMETER.S25, COLOR.footing);
along('B3', n, bottom + mm(z.B3), NOMINAL_DIAMETER.S19, COLOR.footing);
// 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), NOMINAL_DIAMETER.S25, COLOR.footing);
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) {
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);
// 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 {
acrossN('B8', s, bottom + mm(z.B68), NOMINAL_DIAMETER.S29, COLOR.footing);
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) {
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 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);
}
}
+18 -7
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { barsAcross, bearingBlockRebar, blockBars, totals } from '../src/core/rebar/bearing-block';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
@@ -16,13 +17,14 @@ describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
for (const bar of blockBars(150)) expect(bar.length).toBe(2500);
});
it('coping embedment: S2 600 (front) / 550 (back), S1 619.1 (front) / 569.1 (back), as in the spec', () => {
const front = blockBars(150);
const back = blockBars(200);
expect(front.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(600, 6);
expect(back.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(550, 6);
expect(front.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(619.1, 6);
expect(back.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(569.1, 6);
it('coping embedment from the model block height (block top − coping top: front 100, back 150): S2 650 / 600, S1 669.1 / 619.1', () => {
const m = buildPier(loadP1());
const rebar = bearingBlockRebar(m);
const emb = (row: '전열' | '후열', name: 'S1' | 'S2') => rebar.find((b) => b.row === row)!.bars.find((r) => r.name === name)!.embedment;
expect(emb('전열', 'S2')).toBeCloseTo(650, 6);
expect(emb('후열', 'S2')).toBeCloseTo(600, 6);
expect(emb('전열', 'S1')).toBeCloseTo(669.1, 6);
expect(emb('후열', 'S1')).toBeCloseTo(619.1, 6);
});
it('P1: 10 blocks, 80 bars each of S1 and S2 (spec table: 8 本 per block)', () => {
@@ -47,3 +49,12 @@ describe('받침블럭 도면 재현 (spec §6.3)', () => {
expect(across.firstEnd).toBeCloseTo(162.5, 6); // 50 + 75 + 37.5, printed as 50 + 113 in the drawing
});
});
describe('받침블럭 묻힘 검토 (spec §9 RB-S04)', () => {
it('P1: embedments 619–669 mm are shorter than the H19 development length 882, so RB-S04 is raised and the rows show the range', () => {
const { rows, warnings } = rebarSummary(buildPier(loadP1()));
expect(warnings.some((w) => w.code === 'RB-S04')).toBe(true);
expect(rows.find((r) => r.name === 'S1')!.note).toContain('묻힘 619–669');
expect(rows.find((r) => r.name === 'S2')!.note).toContain('묻힘 600–650');
});
});
+16
View File
@@ -84,3 +84,19 @@ describe('RB-C07 P4 삭제 규칙 (기둥철근.md §6.2)', () => {
expect(r.kept).toEqual([-60]);
});
});
describe('기둥철근 P1·P2 마디와 바닥 (기둥철근.md §3.2)', () => {
it('bottom at −1697.5 (−H_f + z_B7 + (d_B7 + d_P1)/2), P1 pieces 4697.5 + 525 / 3000 ×3 / 3147', () => {
expect(c.zBot).toBeCloseTo(-1697.55, 1);
expect(c.p1Hook).toBe(525);
expect(c.p1Pieces.map((v) => Math.round(v))).toEqual([5223, 3000, 3000, 3000, 3147]);
});
it('P2 pieces 4847.5 / 3000 ×3 / 2389', () => {
expect(c.p2Pieces.map((v) => Math.round(v))).toEqual([4848, 3000, 3000, 3000, 2389]);
});
it('P6 hook 135 each, so the length is 2071 (1800.9 + 2 × 135)', () => {
expect(c.p6Length).toBeCloseTo(2070.9, 1);
});
});
+12 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { c14Total, COPING_END_GAP, sideRows, shearBars, STEP_BAR_COUNTS } from '../src/core/rebar/coping';
import { c14Total, COPING_END_GAP, copingSideBars, sideRows, shearBars, STEP_BAR_COUNTS } from '../src/core/rebar/coping';
// P1 coping: L 12000, one column at the centre (T형).
const L = 12000;
@@ -43,6 +43,17 @@ describe('코핑 철근 (코핑철근.md, plan C3)', () => {
expect(r.c13).toEqual([42.538, 42.788]);
});
it('C12 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C13 11746 for L 12000 (코핑철근.md §6.2 table)', () => {
const s = copingSideBars(12000, 41.288, 42.538);
const expected = [3717, 5333, 6950, 8567, 10183];
s.c12.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0));
expect(s.c12[0].xw).toBeCloseTo(4141.7, 1);
s.c13.forEach((r) => {
expect(r.xw).toBe(127);
expect(r.straight).toBe(11746);
});
});
it('C14: 38 bars = 2 × 11 upper + 5 + 6 + 3 + 2 lower', () => {
expect(c14Total()).toBe(38);
});
+24 -12
View File
@@ -52,32 +52,44 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => {
});
it('B10-1 18 bars ≈ 10748, B10-2 18 bars ≈ 8494', () => {
const [b101, b102] = sideBars(L, W);
const [b101, b102] = sideBars(L, W, H);
expect(b101.count).toBe(18);
expect(b101.length).toBeCloseTo(10748, 0);
expect(b102.count).toBe(18);
expect(b102.length).toBeCloseTo(8494, 0);
});
it('B11 60 bars (20 / 20 / 20) ≈ 8359, B12 47 bars (16 / 15 / 16) ≈ 10613', () => {
const [b11, b12] = assemblyBars(L, W);
expect(b11.count).toBe(60);
// The document rounds the 90° hook to 246 mm; the formula gives 246.45, so B11 is 1.2 mm longer.
expect(Math.abs(b11.length - 8359)).toBeLessThan(1.5);
expect(b12.count).toBe(47);
expect(b12.length).toBeCloseTo(10613, 0);
it('B10 levels from the height: H 2000 → m 8 (9 levels), H 2200 → m 9 (10 levels), as in the document', () => {
expect(b10Levels(2000)).toBe(9);
expect(b10Levels(2200)).toBe(10);
expect(assemblyJs(2000)).toEqual([2, 4, 6]);
expect(assemblyJs(2200)).toEqual([2, 4, 6, 8]);
});
it('B9 shear: 16 rows, 20 per row, 320 in all, length ≈ 2128 (l_v 1750 + 2 × 189)', () => {
const b9 = shearB9(L, W, H);
it('B11 60 bars (20 / 20 / 20) ≈ 8359, B12 47 bars (16 / 15 / 16) ≈ 10613', () => {
const [b11, b12] = assemblyBars(L, W, H);
expect(b11.count).toBe(60);
expect(b11.length).toBeCloseTo(8359, 0);
expect(b12.count).toBe(47);
expect(b12.length).toBeCloseTo(10612.5, 1); // the document prints ≈ 10613
});
it('B9 shear: 16 rows, 20 per row on the grid, 320 in all, length ≈ 2128 (l_v 1750 + 2 × 189)', () => {
const b9 = shearB9(L, W, H, []);
expect(b9.rows).toBe(16);
expect(b9.perRow.every((c) => c === 20)).toBe(true);
expect(b9.count).toBe(320);
expect(b9.length).toBeCloseTo(2128, 0);
});
it('B9 column zone: bars whose centre is within D/2 + d/2 of a column axis are left out (P1: 14 bars)', () => {
const b9 = shearB9(L, W, H, [0], 2000);
expect(b9.count).toBe(306);
for (const b of b9.bars) expect(Math.hypot(b.x, b.n)).toBeGreaterThan(1000 + 7.95);
});
});
import { assemblyHeights, legOffsets } from '../src/core/rebar/footing';
import { assemblyHeights, assemblyJs, b10Levels, legOffsets } from '../src/core/rebar/footing';
describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () => {
it('leg offsets: B1 +25.4, B3 +22.25, B5 +28.6, B7 +23.85', () => {
@@ -89,7 +101,7 @@ describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () =>
});
it('B11 at 550 / 950 / 1350 and B12 at 569.1 / 969.1 / 1369.1', () => {
const h = assemblyHeights();
const h = assemblyHeights(H);
expect(h.B11).toEqual([550, 950, 1350]);
expect(h.B12.map((z) => Math.round(z * 10) / 10)).toEqual([569.1, 969.1, 1369.1]);
});
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { c14Bars, c14Length, copingSideBars, stairBars, undersideAt } from '../src/core/rebar/coping';
import { loadP1 } from './helpers';
const m = buildPier(loadP1());
const L = m.copingL * 1000;
const B = m.copingB * 1000;
const side = copingSideBars(L, 41.288, 42.538);
const rows = [...side.c12, ...side.c13];
const under = (x: number) => undersideAt(L, x, m.endBottom, m.levelBottom);
describe('C14 / C15 / C16 (코핑철근.md §7–8)', () => {
it('C14: 2 upper lines of 11 (C2, C3) and lower rows 5 / 6 / 3 / 2 = 38', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
const count = (line: number) => bars.filter((b) => b.line === line).length;
expect(count(1)).toBe(11);
expect(count(2)).toBe(11);
expect([3, 4, 5, 6].map(count)).toEqual([5, 6, 3, 2]);
expect(bars.length).toBe(38);
});
it('C14 heights: upper 43.251 / 43.194, lower 42.810 / 42.310 / 41.810 / 41.310', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
const z = (line: number) => bars.find((b) => b.line === line)!.z;
expect(z(1)).toBeCloseTo(43.251, 3);
expect(z(2)).toBeCloseTo(43.194, 3);
expect([3, 4, 5, 6].map(z)).toEqual([42.81, 42.31, 41.81, 41.31].map((v) => expect.closeTo(v, 2)));
});
it('C14 length 3447.5 (2855.5 run + 2 × 296 legs)', () => {
expect(c14Length(B)).toBeCloseTo(3447.5, 0);
});
it('C15: 11 bars, z 43.598, length 3852 (2088 + 2 × 882)', () => {
const { c15 } = stairBars(L, B, m.copingRefTop);
expect(c15.length).toBe(11);
expect(c15[0].z).toBeCloseTo(43.598, 3);
expect(c15[0].length).toBeCloseTo(3852, 0);
expect(c15[0].x0).toBeCloseTo(9812, 0);
});
it('C16: 22 bars (3·4구간), z 43.436 / 43.507, lengths 4070 / 4069', () => {
const { c16 } = stairBars(L, B, m.copingRefTop);
expect(c16.length).toBe(22);
expect(c16[0].z).toBeCloseTo(43.436, 3);
expect(c16[11].z).toBeCloseTo(43.507, 3);
expect(c16[0].length).toBeCloseTo(4070, 0);
expect(c16[11].length).toBeCloseTo(4069, 0);
});
it('C15 / C16 across positions: 137 from the cover line, then @250, 11 bars', () => {
const { c15 } = stairBars(L, B, m.copingRefTop);
expect(c15[0].n).toBeCloseTo(-B / 2 + 237, 0);
expect(c15[10].n - c15[0].n).toBeCloseTo(2500, 0);
});
});
describe('C14 연직 발 (코핑철근.md §8)', () => {
it('upper lines keep their legs; a lower bar keeps its legs only if they end above the underside', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
expect(bars.filter((b) => b.line <= 2).every((b) => b.legs)).toBe(true);
// The leg of the C12 5단 bar (line 4) at the ends (x 1 m, 11 m) would end below the sloped underside, so it is left out.
const omitted = bars.filter((b) => !b.legs).map((b) => [b.line, Math.round(b.x / 1000)]);
expect(omitted).toEqual([[4, 1], [4, 11]]);
});
});
+6 -6
View File
@@ -12,13 +12,13 @@ describe('③ 철근표 요약 (P1)', () => {
expect(count('S2')).toBe(80);
});
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 5 rows, C13 2 rows, C14 38, C15 11, C16 22', () => {
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 20 (5 rows × 일자 2 + ㄷ 2), C13 8 (2 rows), C14 38, C15 11, C16 22', () => {
for (const n of ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']) expect(count(n)).toBe(12);
expect(count('C9')).toBe(18);
expect(count('C10')).toBe(9);
expect(count('C11')).toBe(18);
expect(count('C12')).toBe(5);
expect(count('C13')).toBe(2);
expect(count('C12')).toBe(20);
expect(count('C13')).toBe(8);
expect(count('C14')).toBe(38);
expect(count('C15')).toBe(11);
expect(count('C16')).toBe(22);
@@ -43,9 +43,9 @@ describe('③ 철근표 요약 (P1)', () => {
expect(count('B12')).toBe(47);
});
it('B9: 320 bars; warnings: B2 needs a splice (13646 > 12000)', () => {
expect(count('B9')).toBe(320);
expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-F08']); // defaults are assumed values (I02)
it('B9: 306 bars (320 on the grid less 14 in the column zone); warnings: B2 needs a splice (13646 > 12000)', () => {
expect(count('B9')).toBe(306);
expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-S04', 'RB-F08']); // defaults are assumed values (I02); bearing embedment 619–669 < 882 (RB-S04)
expect(warnings.find((w) => w.code === 'RB-F08')!.bar).toBe('B2');
// B2 is split into two pieces (추천안): each piece is about half the 13646 bar plus half the lap.
expect(rows.find((r) => r.name === 'B2')!.length!).toBeCloseTo(7776, 0);
+72 -2
View File
@@ -13,11 +13,60 @@ describe('3D 철근 모델 (③)', () => {
}
});
it('P1 has one bar per 원주 등분 (46) and P2 likewise', () => {
expect(rb.byName.get('P1')!.length).toBe(46);
it('B1–B4 and B5–B8 all exist in 3D (B2 was missing)', () => {
for (const n of ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8']) {
expect(rb.byName.get(n)?.length ?? 0, n).toBeGreaterThan(0);
}
});
it('B2 legs reach the B1 height (1873 above the footing bottom), B1 legs hang 304.8 below B1', () => {
const f = buildPier(loadP1()).footings[0];
const box = (name: string) => {
const b = new THREE.Box3();
for (const mesh of rb.byName.get(name)!) b.expandByObject(mesh);
return b;
};
expect(box('B2').max.y - f.bottom).toBeCloseTo(1.873, 2);
expect(box('B1').min.y - f.bottom).toBeCloseTo(1.873 - 0.3048, 2);
});
it('C12 / C13 in 3D: per row two 일자 and two ㄷ (body + 2 legs each end)', () => {
expect(rb.byName.get('C12')!.length).toBe(5 * (2 + 2 * 3));
expect(rb.byName.get('C13')!.length).toBe(2 * (2 + 2 * 3));
});
it('C1 legs turn down 472 at the ends (to EL 42.812 at the end face)', () => {
const model = buildPier(loadP1());
const b = new THREE.Box3();
for (const mesh of rb.byName.get('C1')!) b.expandByObject(mesh);
expect(b.min.y).toBeCloseTo(model.copingRefTop - 0.127 - 0.472, 2);
});
it('C9 / C10 / C11: three 1740-wide 폐합 per position, each a U plus a cap (18 pieces each)', () => {
expect(rb.byName.get('C10')!.length).toBe(9 * 18);
expect(rb.byName.get('C9')!.length).toBe(18 * 18);
expect(rb.byName.get('C11')!.length).toBe(18 * 18);
});
it('C7 / C8 follow the underside: lowest at the Level (+127 − r), vertical legs up to the C1 level', () => {
const model = buildPier(loadP1());
const b = new THREE.Box3();
for (const n of ['C7', 'C8']) for (const mesh of rb.byName.get(n)!) b.expandByObject(mesh);
const r = 28.6 / 2000;
expect(b.min.y).toBeCloseTo(model.levelBottom + 0.127 - r, 2);
// Vertical legs end flat, so no radius on top. C1 is the top row at copingRefTop − 0.127.
expect(b.max.y).toBeCloseTo(model.copingRefTop - 0.127, 2);
});
it('P1 has one bar per 원주 등분 (46) plus its outward hook tail, P2 one per 등분 (46)', () => {
expect(rb.byName.get('P1')!.length).toBe(46 * 2);
expect(rb.byName.get('P2')!.length).toBe(46);
});
it('P6 crossties in 3D: 8 levels × 4 bars each with main bar + 2 downward hooks (3 pieces per bar = 96 pieces)', () => {
expect(rb.byName.get('P6')!.length).toBe(8 * 4 * 3);
});
it('highlight dims the other bars and keeps the chosen one', () => {
rb.highlight('P3');
const p3 = (rb.byName.get('P3')![0].material as THREE.MeshStandardMaterial).opacity;
@@ -28,3 +77,24 @@ describe('3D 철근 모델 (③)', () => {
expect((rb.byName.get('C1')![0].material as THREE.MeshStandardMaterial).opacity).toBe(1);
});
});
describe('받침블럭 S1 · S2 in 3D (받침블럭철근.md §3–5)', () => {
it('per block: 8 S1 and 8 S2, each a bar with two 700 legs (3 pieces)', () => {
const blocks = buildPier(loadP1()).blocks.length;
expect(rb.byName.get('S1')!.length).toBe(blocks * 8 * 3);
expect(rb.byName.get('S2')!.length).toBe(blocks * 8 * 3);
});
});
describe('확대기초 B9 · B10 · B11 · B12 in 3D (확대기초철근 (1).md §7–9)', () => {
it('B9: 306 vertical bars (column zone removed), each three pieces (bar + two hook tails)', () => {
expect(rb.byName.get('B9')!.length).toBe(306 * 3);
});
it('B10-1 / B10-2: 18 each (9 levels × 2 faces), each three pieces; B11 60, B12 47', () => {
expect(rb.byName.get('B10-1')!.length).toBe(18 * 3);
expect(rb.byName.get('B10-2')!.length).toBe(18 * 3);
expect(rb.byName.get('B11')!.length).toBe(60 * 3);
expect(rb.byName.get('B12')!.length).toBe(47 * 3);
});
});