Files
Pier_v1/src/core/rebar/coping.ts
T
ypshimandClaude Haiku 5.5 0a5edfb152 feat(rebar,view): coping end height h1, taper h2 and Level width as inputs on ② (1단계)
The three values are inputs on the coping card, with the P1 values (1000 · 1500 · 2100) when absent, and they are tracked
like the other shape inputs. The geometry and the coping rebar read them from one CopingGeom: the U heights follow the
closed form (h1 − 2c) + h2·min(x_s, x_L0)/x_L0, which reproduces the P1 values. The side rows are assumed to keep their
position relative to the Level underside (명세 확인 필요). P1 results are unchanged; the full suite passes.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 11:44:44 +09:00

336 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 코핑 철근 (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, fyOf, hookLength, Material, NOMINAL_DIAMETER } from './common';
import { type CopingGeom, P1_COPING_GEOM } from '../coping-geom';
export const COPING_REBAR = {
/** 피복 (최외측 철근 중심까지). */
cover: 100,
/** 첫 전단철근 최소 여유 e_add. 끝 여유 e_min = cover + e_add = 300. */
eAdd: 200,
/** Level 폭 W_L. */
levelWidth: 2100,
/** C10 간격 s_L (기둥 중심부터). */
spacingL: 300,
/** C9·C11 간격 s_s (C10에서 바깥으로). */
spacingS: 250,
/** C12·C13 측면 수평 간격. */
spacingSide: 250,
/** 테이퍼 높이 h2 (코핑철근.md §2). */
taperHeight: 1500,
/** C14 위 2줄 간격 / 아래 줄 수평 간격 / 아래 줄 연직 간격 / 끝 여유. */
c14: { upperSpacing: 1000, lowerSpacing: 2000, verticalSpacing: 500, endGap: 500 },
} as const;
export const COPING_END_GAP = COPING_REBAR.cover + COPING_REBAR.eAdd;
export interface ShearBars {
/** C10 positions (mm), from the first to the last. */
c10: number[];
/** C9 positions (mm), from the left end toward the column. */
c9: number[];
/** C11 positions (mm), from the column toward the right end. */
c11: number[];
/** Distance from the coping end to the outermost C9 / C11 bar (mm). */
endGap: number;
}
/**
* Shear bars C9–C11 along the axis (코핑철근.md §3). A single column at the coping centre.
* C10: @300 from the column centre, out to the first position beyond the Level edge (k_max).
* C9 / C11: @250 outward from the outermost C10, as many as fit with the end gap of at least 300.
*/
export function shearBars(coping: number, xc = coping / 2, g: CopingGeom = P1_COPING_GEOM): ShearBars {
const { spacingL, spacingS } = COPING_REBAR;
const kMax = Math.ceil(g.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];
const n9 = Math.floor((left - COPING_END_GAP) / spacingS);
if (n9 < 0) {
throw new Error('RB-K01: C10 바깥 구간이 끝 여유보다 짧아 C9를 놓을 수 없습니다.');
}
const c9 = Array.from({ length: n9 }, (_, j) => Math.round(left - (n9 - j) * spacingS));
const c11 = Array.from({ length: n9 }, (_, j) => Math.round(right + (j + 1) * spacingS));
const endGap = Math.min(left - c9[0], coping - c11[c11.length - 1]);
return { c10, c9, c11, endGap: endGap };
}
/**
* Side horizontal bars (코핑철근.md §6): seven rows from the bottom at 250 mm.
* Rows below the sloped-bottom break (z_7,end) are C12, the rows at and above it are C13.
*/
export function sideRows(zBottom: number, zBreak: number, rows = 7): { c12: number[]; c13: number[] } {
const z = Array.from({ length: rows }, (_, i) => Math.round((zBottom + i * COPING_REBAR.spacingSide / 1000) * 1000) / 1000);
return { c12: z.filter((v) => v < zBreak - 1e-9), c13: z.filter((v) => v >= zBreak - 1e-9) };
}
/** C14 counts (코핑철근.md §8): 2 upper lines of 11 plus the lower rows 5 / 6 / 3 / 2 = 38. */
export const C14_COUNTS = { upperLines: 2, upperPerLine: 11, lower: [5, 6, 3, 2] as const };
export function c14Total(): number {
return C14_COUNTS.upperLines * C14_COUNTS.upperPerLine + C14_COUNTS.lower.reduce((a, b) => a + b, 0);
}
/** C15 (거꾸로 된 U, 가장 높은 계단) and C16 (L형, 2구간) counts (코핑철근.md §7). */
export const STEP_BAR_COUNTS = { C15: 11, C16: 22 } as const;
/**
* C9·C10 lower U length (코핑철근.md §4.3): 1740 + 2h. h rises linearly along the slope between the document's
* ends (x 300 → h 890.9, x 4550 → h 2178.8) and stays at the Level height 2300 from the Level edge (4950) on.
* The bar's upper cap (삿갓) length depends on l_cap, which is still open (Q-K5), so only the U is given here.
*/
export const SHEAR_U = { x0: 300, h0: 890.9, x1: 4550, h1: 2178.8, levelH: 2300, levelEdgeFromCentre: 1050, flat: 1740 } as const;
/**
* U height at x (mm): h = (h1 − 2c) + h2·min(x_s, x_L0)/x_L0, with x_L0 = L/2 − W_L/2 (코핑철근.md §4.2).
* Reproduces the P1 values (890.9 at x = 300, 2178.8 at x = 4550, 2300 at the Level).
*/
export function shearUHeight(x: number, coping: number, g: CopingGeom = P1_COPING_GEOM): number {
const xs = Math.min(x, coping - x); // mirror to the left half
const xL0 = coping / 2 - g.levelWidth / 2;
return g.h1 - 2 * COPING_REBAR.cover + (g.h2 * Math.min(xs, xL0)) / xL0;
}
export function shearULength(x: number, coping: number, g: CopingGeom = P1_COPING_GEOM): number {
return SHEAR_U.flat + 2 * shearUHeight(x, coping, g);
}
/** 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, g: CopingGeom = P1_COPING_GEOM): { c12: SideBar[]; c13: SideBar[] } {
const c = COPING_REBAR.cover;
const xL0 = L / 2 - g.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)) / g.h2)),
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, fyOf(m, 'H19'), 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, levelWidth = P1_COPING_GEOM.levelWidth): number {
const xL0 = L / 2 - 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.
*/
export const CAP = { flat: 1740, cover: 100 } as const;
export function capLength(x: number, coping: number, g: CopingGeom = P1_COPING_GEOM): number {
const h = shearUHeight(x, coping, g);
const lCap = h - CAP.cover;
return CAP.flat + 2 * lCap;
}
/**
* Step (mm) between the three 폐합 at one position, to the right (수정 2026-10-23): a 삿갓 sits one bar (d) beside its U,
* so the next loop's U starts two bars on. Loop i is then at x + i·step and its 삿갓 at x + i·step + d, with no overlap.
*/
export const SHEAR_LOOP_STEP = 2 * NOMINAL_DIAMETER.H25;
/** Pieces per position: 3 closed stirrups × (U + cap) = 6. Total 270 for C9 + C10 + C11 (document). */
export const PIECES_PER_POSITION = 6;
/**
* 전단철근 구간 표 (시안 ③ · 코핑): the segments of C9 / C10 / C11 along the coping, from the bar positions.
* One column gives the three T형 segments; several columns add the segments between them (추천안, Q-K2).
*/
export function shearSegments(coping: number, columns: number[] = [coping / 2], g: CopingGeom = P1_COPING_GEOM): { segment: string; from: number; to: number; spacing: number; count: number; bar: 'C9' | 'C10' | 'C11' | 'C9·C11' }[] {
const b = shearBarsMulti(coping, columns, g);
const seg = (name: string, xs: number[], bar: 'C9' | 'C10' | 'C11' | 'C9·C11', spacing: number) => ({
segment: name,
from: Math.min(...xs),
to: Math.max(...xs),
spacing,
count: xs.length,
bar,
});
const out = [seg('좌 경사', b.c9, 'C9', COPING_REBAR.spacingS)];
const groups = columns.length > 1 ? columns.length : 1;
for (let k = 0; k < groups; k++) {
const xc = columns[k] ?? coping / 2;
out.push(seg(`Level ${k + 1}`, b.c10.filter((x) => Math.abs(x - xc) <= (g.levelWidth / 2 + COPING_REBAR.spacingL * 4)).slice(0, 2 * 4 + 1), 'C10', COPING_REBAR.spacingL));
}
for (let k = 0; k + 1 < groups; k++) {
const between = b.between.filter((x) => x > columns[k] && x < columns[k + 1]);
if (between.length) out.push(seg(`기둥 사이 ${k + 1}`, between, 'C9·C11', COPING_REBAR.spacingS));
}
out.push(seg('우 경사', b.c11, 'C11', COPING_REBAR.spacingS));
return out;
}
/**
* Shear bars for a multi-column coping (코핑철근.md §9, 다주형 확장 원칙).
* C10: each column's centre, @300 out to the first position beyond its Level edge.
* C9 / C11: outward from the outermost C10 on each end, as for one column.
* Between columns: @250 in from both C10 groups, the remaining length shared evenly (추천안, Q-K2).
* With one column this is the same as shearBars.
*/
export function shearBarsMulti(coping: number, columns: number[], g: CopingGeom = P1_COPING_GEOM): ShearBars & { between: number[] } {
if (columns.length <= 1) {
const one = shearBars(coping, columns[0] ?? coping / 2, g);
return { ...one, between: [] };
}
const { spacingL, spacingS } = COPING_REBAR;
const kMax = Math.ceil(g.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)),
);
for (let i = 1; i < groups.length; i++) {
if (groups[i][0] <= groups[i - 1][groups[i - 1].length - 1]) {
throw new Error('기둥 간격이 좁아 C10 구간이 겹칩니다. 다주형 코핑 철근을 만들 수 없습니다.');
}
}
const c10 = groups.flat();
const left = groups[0][0];
const right = groups[groups.length - 1][groups[groups.length - 1].length - 1];
const n9 = Math.floor((left - COPING_END_GAP) / spacingS);
if (n9 < 0) throw new Error('RB-K01: C10 바깥 구간이 끝 여유보다 짧아 C9를 놓을 수 없습니다.');
const c9 = Array.from({ length: n9 }, (_, j) => Math.round(left - (n9 - j) * spacingS));
const c11 = Array.from({ length: n9 }, (_, j) => Math.round(right + (j + 1) * spacingS));
const between: number[] = [];
for (let i = 1; i < groups.length; i++) {
const a = groups[i - 1][groups[i - 1].length - 1];
const b = groups[i][0];
const gap = b - a;
const pitches = Math.floor(gap / spacingS);
for (let j = 1; j < pitches; j++) between.push(Math.round(a + (gap * j) / pitches));
}
const endGap = Math.min(left - c9[0], coping - c11[c11.length - 1]);
return { c10, c9, c11, between, endGap };
}