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>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 11:44:44 +09:00
1 parent 5b5db60657
commit 0a5edfb152
9 files changed
+169 -45

No files matched your search

+6
View File
@@ -7,6 +7,9 @@ import type { PierInput } from './types';
export const EDITABLE_FIELDS = [
{ key: 'copingWidthMm', label: '코핑 폭 B', unit: 'mm' },
{ key: 'copingLengthMm', label: '코핑 길이 L', unit: 'mm' },
{ key: 'copingH1Mm', label: '끝단 높이 h1', unit: 'mm' },
{ key: 'copingH2Mm', label: '테이퍼 높이 h2', unit: 'mm' },
{ key: 'levelWidthMm', label: 'Level 폭', unit: 'mm' },
{ key: 'pileLengthMm', label: '말뚝 길이', unit: 'mm' },
] as const;
@@ -18,6 +21,9 @@ export type EditableKey = (typeof TRACKED_KEYS)[number];
export const FIELD_LABELS: Record<EditableKey, string> = {
copingWidthMm: '코핑 폭 B',
copingLengthMm: '코핑 길이 L',
copingH1Mm: '끝단 높이 h1',
copingH2Mm: '테이퍼 높이 h2',
levelWidthMm: 'Level 폭',
pileLengthMm: '말뚝 길이',
pileRows: '열별 본수',
};
+24
View File
@@ -0,0 +1,24 @@
/**
* Coping heights and Level width (mm) that the coping geometry and the coping rebar depend on (1단계, 승인 2026-10-23).
* Dependency-free so that both the model and the rebar modules can import it.
*/
export interface CopingGeom {
/** 끝단 높이 h1 (mm). */
h1: number;
/** 테이퍼 높이 h2 (mm). */
h2: number;
/** Level 폭 W_L (mm). */
levelWidth: number;
}
/** P1 values: 끝단 높이 1000, 테이퍼 높이 1500, Level 폭 2100. Used when the input does not give them. */
export const P1_COPING_GEOM: CopingGeom = { h1: 1000, h2: 1500, levelWidth: 2100 };
/**
* Side-row levels (EL m) of C12 / C13 from the Level underside: the lowest row and the row where the sloped underside breaks.
* P1: 40.911 + 0.377 = 41.288 and 40.911 + 1.627 = 42.538. ASSUMPTION: the side rows keep this position relative to the
* Level underside when h1 or h2 changes (명세 확인 필요).
*/
export function sideRowLevels(levelBottom: number): { zBottom: number; zBreak: number } {
return { zBottom: levelBottom + 0.377, zBreak: levelBottom + 1.627 };
}
+14 -6
View File
@@ -1,6 +1,7 @@
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
import type { PierInput, Row } from './types';
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
/** Dimensions in m. Sources: intent.PR sheet (P1 rules) and spec.md §3 defaults. */
export const DIM = {
@@ -161,6 +162,8 @@ export interface PierModel {
mitre: Vec2 | null;
levelBottom: number;
endBottom: number;
/** Coping heights and Level width in mm, as used by the coping rebar (1단계). */
copingGeom: CopingGeom;
bearings: BearingLocal[];
blocks: BlockModel[];
columns: ColumnModel[];
@@ -216,6 +219,10 @@ export function buildPier(input: PierInput): PierModel {
const L = input.copingLengthMm / 1000;
const B = input.copingWidthMm / 1000;
// Coping heights and Level width (m): inputs, with the P1 values when absent (1단계).
const copingH1 = (input.copingH1Mm ?? P1_COPING_GEOM.h1) / 1000;
const copingH2 = (input.copingH2Mm ?? P1_COPING_GEOM.h2) / 1000;
const levelW = (input.levelWidthMm ?? P1_COPING_GEOM.levelWidth) / 1000;
const planHalfB = B / 2;
const ip1v: Vec2 = { e: ip1.x, n: ip1.y };
const ip2v: Vec2 = { e: ip2.x, n: ip2.y };
@@ -290,20 +297,20 @@ export function buildPier(input: PierInput): PierModel {
// Columns (spec §4.5). Positions are coping coordinates; a column must stay a Level half-width away from the kink.
const colG = columnPositions(input.type, L);
if (kinkG !== null) {
const near = nearKink(axis, colG.map((g) => sStart + g), DIM.levelWidth / 2);
const near = nearKink(axis, colG.map((g) => sStart + g), levelW / 2);
if (near.length) throw new Error('미실시: 꺾임점 근처(Level 반폭 이내)에 기둥이 있습니다. 기둥 위치를 확인하세요.');
}
const colK = colG.map((g) => (kinkG !== null && g > kinkG ? 1 : 0));
// Coping underside: ends at (top − end height h1 = 1000), Level zone at (top − h1 − h2 taper height = 1500).
const levelBottom = input.copingTopEl - DIM.copingH1 - DIM.copingH2;
const endBottom = input.copingTopEl - DIM.copingH1;
const levelStart = Math.min(...colG) - DIM.levelWidth / 2;
const levelEnd = Math.max(...colG) + DIM.levelWidth / 2;
const levelBottom = input.copingTopEl - copingH1 - copingH2;
const endBottom = input.copingTopEl - copingH1;
const levelStart = Math.min(...colG) - levelW / 2;
const levelEnd = Math.max(...colG) + levelW / 2;
if (levelStart < 0 || levelEnd > L) throw new Error('코핑 길이가 Level 구간보다 짧습니다.');
// Underside from the right end to the left end. Between columns (2·3주형) it drops 300 below Level with 300 × 300 haunches (spec §4.5, Q3).
const underside: [number, number][] = [[L, endBottom]];
const levelZones = [...colG].sort((a, b) => b - a).map((s) => [s - DIM.levelWidth / 2, s + DIM.levelWidth / 2] as const);
const levelZones = [...colG].sort((a, b) => b - a).map((s) => [s - levelW / 2, s + levelW / 2] as const);
levelZones.forEach(([zoneStart, zoneEnd], k) => {
underside.push([zoneEnd, levelBottom], [zoneStart, levelBottom]);
const next = levelZones[k + 1];
@@ -432,6 +439,7 @@ export function buildPier(input: PierInput): PierModel {
mitre: axis.mitre,
levelBottom,
endBottom,
copingGeom: { h1: copingH1 * 1000, h2: copingH2 * 1000, levelWidth: levelW * 1000 },
bearings: [...frontPlaced, ...backPlaced].map((b) => ({ no: b.no, row: b.row, k: b.k, s: b.s, g: b.g, n: b.n, z: b.z })),
blocks,
columns,
+27 -23
View File
@@ -3,6 +3,7 @@
* 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,
@@ -40,9 +41,9 @@ export interface ShearBars {
* 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): ShearBars {
const { spacingL, spacingS, levelWidth } = COPING_REBAR;
const kMax = Math.ceil(levelWidth / 2 / spacingL); // k_max = ceil((W_L/2)/s_L), spec §3.1
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];
@@ -82,15 +83,18 @@ export const STEP_BAR_COUNTS = { C15: 11, C16: 22 } as const;
*/
export const SHEAR_U = { x0: 300, h0: 890.9, x1: 4550, h1: 2178.8, levelH: 2300, levelEdgeFromCentre: 1050, flat: 1740 } as const;
export function shearUHeight(x: number, coping: number): number {
/**
* 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 slope = (SHEAR_U.h1 - SHEAR_U.h0) / (SHEAR_U.x1 - SHEAR_U.x0);
const h = SHEAR_U.h0 + (xs - SHEAR_U.x0) * slope;
return Math.min(h, SHEAR_U.levelH);
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): number {
return SHEAR_U.flat + 2 * shearUHeight(x, coping);
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). */
@@ -113,13 +117,13 @@ export interface SideBar {
* 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[] } {
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 - COPING_REBAR.levelWidth / 2;
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)) / COPING_REBAR.taperHeight)),
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)),
};
}
@@ -159,8 +163,8 @@ export interface C14Bar {
}
/** 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;
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);
}
@@ -246,8 +250,8 @@ export function stairBars(L: number, B: number, topEl: number, m: Material = DEF
*/
export const CAP = { flat: 1740, cover: 100 } as const;
export function capLength(x: number, coping: number): number {
const h = shearUHeight(x, coping);
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;
}
@@ -265,8 +269,8 @@ 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]): { segment: string; from: number; to: number; spacing: number; count: number; bar: 'C9' | 'C10' | 'C11' | 'C9·C11' }[] {
const b = shearBarsMulti(coping, columns);
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),
@@ -279,7 +283,7 @@ export function shearSegments(coping: number, columns: number[] = [coping / 2]):
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) <= (COPING_REBAR.levelWidth / 2 + COPING_REBAR.spacingL * 4)).slice(0, 2 * 4 + 1), 'C10', COPING_REBAR.spacingL));
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]);
@@ -296,13 +300,13 @@ export function shearSegments(coping: number, columns: number[] = [coping / 2]):
* 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[]): ShearBars & { between: number[] } {
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);
const one = shearBars(coping, columns[0] ?? coping / 2, g);
return { ...one, between: [] };
}
const { spacingL, spacingS, levelWidth } = COPING_REBAR;
const kMax = Math.ceil(levelWidth / 2 / spacingL); // k_max = ceil((W_L/2)/s_L), spec §3.1
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)),
);
+10 -7
View File
@@ -2,6 +2,7 @@ import type { PierModel } from '../geometry';
import { barLength, bearingBlockRebar, totals } from './bearing-block';
import { bottomPiece, topBarLength, widthLayout } from './coping-main';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { sideRowLevels } from '../coping-geom';
import { columnRebar, COLUMN_P1 } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, splitPieces } from './common';
@@ -162,21 +163,22 @@ export function rebarSummary(
rows.push({ member: 'coping', name: 'C8', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' });
const colS = model.columns.map((c) => c.s * 1000);
const nCol = Math.max(1, colS.length);
const shear = nCol > 1 ? shearBarsMulti(L, colS) : { ...shearBars(L), between: [] as number[] };
const geom = model.copingGeom;
const shear = nCol > 1 ? shearBarsMulti(L, colS, geom) : { ...shearBars(L, undefined, geom), between: [] as number[] };
const uRange = (xs: number[]) => {
const u = xs.map((x) => Math.round(shearULength(x, L)));
const u = xs.map((x) => Math.round(shearULength(x, L, geom)));
return `${Math.min(...u)}–${Math.max(...u)}`;
};
const capRange = (xs: number[]) => {
const c = xs.map((x) => Math.round(capLength(x, L)));
const c = xs.map((x) => Math.round(capLength(x, L, geom)));
return `${Math.min(...c)}–${Math.max(...c)}`;
};
const shearNote = (label: string, xs: number[]) =>
`${label}, 하부 U ${uRange(xs)}, 삿갓 ${capRange(xs)} (l_cap 추천안, Q-K5), 조각 ${xs.length * PIECES_PER_POSITION}`;
// Each position has three 폐합, each with one 하부 U and one 삿갓 (6 pieces). Pieces: U and 삿갓 lengths at each position.
const loopPieces = (xs: number[]) => xs.flatMap((x) => [
{ length: shearULength(x, L), count: 3 },
{ length: capLength(x, L), count: 3 },
{ length: shearULength(x, L, model.copingGeom), count: 3 },
{ length: capLength(x, L, model.copingGeom), count: 3 },
]);
rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, pieces: loopPieces(shear.c9), note: shearNote('전단 좌 @250', shear.c9) });
rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, pieces: loopPieces(shear.c10), note: shearNote('전단 Level @300', shear.c10) });
@@ -188,7 +190,8 @@ export function rebarSummary(
warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 T형 기준으로 두었습니다 (기둥마다 반복 규칙 미정, Q-K11).`, target: 'coping' });
}
// 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 levels = sideRowLevels(model.levelBottom);
const side = copingSideBars(model.copingL * 1000, levels.zBottom, levels.zBreak, model.copingGeom);
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)}` : '—';
// Per bar: 2 일자 (straight) and 2 ㄷ (hook body + legs), so 4 bars per row position.
@@ -197,7 +200,7 @@ export function rebarSummary(
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), 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 c14 = c14Bars(L14, model.copingRefTop, [...side.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth));
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}개 (하면 밖)` });
+4
View File
@@ -31,6 +31,10 @@ export interface PierInput {
pileLengthMm: number;
copingWidthMm: number;
copingLengthMm: number;
/** 끝단 높이 h1, 테이퍼 높이 h2, Level 폭 (mm). Absent = the P1 values (1000 · 1500 · 2100). */
copingH1Mm?: number;
copingH2Mm?: number;
levelWidthMm?: number;
columnCount: number;
/** 열별 본수 (spec §4.6): one count per 열 across the footing width. Absent = the defaults. */
pileRows?: number[];