feat(rebar): 확대기초 배근 입력 8개 (피복 3·여유·격자 s·s_v·e_top·n), FT01·FT02 검사

- core/rebar/footing-settings.ts: 입력 정의, 기본값(확대기초철근 (1).md), 범위(제안), 검사 (FT01 범위, FT02 n 정수)
- 확대기초 계산 함수가 설정을 인자로 받음 (기본값 = 기존 값, 기존 호출은 그대로)
- 3D 확대기초 그림의 125·100 하드코딩 제거, 철근표·3D·기둥 층 높이가 같은 설정을 씀
- 패널: 상면·측면·하면 피복을 칸 분리, s_v와 e_top 분리. 고정 규칙(k=2, Δ₂=100, 전단 4s·엇갈림 2s, cycle 교대)과
  B2·B6 디리는 읽기 전용으로 표시
- 기본값에서 P1 결과 불변 (기존 테스트 통과)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 16:59:08 +09:00
1 parent 550b508e7a
commit 5a20a18185
11 files changed
+281 -96

No files matched your search

+2 -1
View File
@@ -23,7 +23,7 @@ export const EDITABLE_FIELDS = [
] as const;
/** Inputs tracked for overrides and pending changes: the numeric cards plus 열별 본수 (an array, compared by value). */
export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar', 'copingRebar', 'topSplice', 'columnRebar'] as const;
export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar', 'copingRebar', 'topSplice', 'columnRebar', 'footingRebar'] as const;
export type EditableKey = (typeof TRACKED_KEYS)[number];
@@ -57,6 +57,7 @@ export const FIELD_LABELS: Record<EditableKey, string> = {
copingRebar: '코핑 배근 입력',
topSplice: '상면 주철근 이음',
columnRebar: '기둥 배근 입력',
footingRebar: '확대기초 배근 입력',
strataRows: '지층 행',
};
+4
View File
@@ -3,6 +3,7 @@ import { buildIntegratedBlocks, type IntegratedBlock } from './integrated-block'
import { type BlockRebarSettings, DEFAULT_BLOCK_REBAR } from './rebar/bearing-block';
import { type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './rebar/coping-settings';
import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-settings';
import { type FootingRebarSettings, DEFAULT_FOOTING_REBAR } from './rebar/footing-settings';
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
import type { PierInput, Row } from './types';
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
@@ -184,6 +185,8 @@ export interface PierModel {
topSplice: TopSpliceSettings;
/** 기둥 배근 입력 used by the column rebar (승인 2026-10-26). */
columnRebar: ColumnRebarSettings;
/** 확대기초 배근 입력 used by the footing rebar (승인 2026-10-26). */
footingRebar: FootingRebarSettings;
columns: ColumnModel[];
footings: FootingModel[];
blindings: BlindingModel[];
@@ -475,6 +478,7 @@ export function buildPier(input: PierInput): PierModel {
copingRebar: { ...DEFAULT_COPING_REBAR, ...input.copingRebar },
topSplice: { ...DEFAULT_TOP_SPLICE, ...input.topSplice },
columnRebar: { ...DEFAULT_COLUMN_REBAR, ...input.columnRebar },
footingRebar: { ...DEFAULT_FOOTING_REBAR, ...input.footingRebar },
columns,
footings,
blindings,
+8 -5
View File
@@ -2,12 +2,15 @@ import type { PierModel } from '../geometry';
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, Material, NOMINAL_DIAMETER, spliceLength } from './common';
import { layers } from './footing';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings';
/**
* 기둥 철근 C4 (plan C4). Source: RAG/기둥철근 (1).md, 2026-10-22 최종 정리. Type-1 원형 중실 기둥.
* Lengths in mm, heights in mm from the 확대기초 상면 (up +).
*/
export interface ColumnInput {
/** 확대기초 배근 입력 (층 높이·띠철근 기준). Absent = the defaults. */
footing?: FootingRebarSettings;
/** 기둥 지름 D. */
D: number;
/** 기둥 높이 H_c (확대기초 상면 ~ 코핑 Level 하면). */
@@ -55,7 +58,7 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1):
const eP1 = Math.round((seg.top - (c3 - 0.1)) * 1000);
// e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23).
const eTop = Math.round((seg.top - (model.levelBottom + seg.top) / 2) * 1000);
return { ...base, ...model.columnRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm };
return { ...base, ...model.columnRebar, footing: model.footingRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm };
}
export const COLUMN_P1: ColumnInput = {
@@ -94,8 +97,8 @@ export function deleteInterferingP4(p4: number[], topBars: TopBar[], dP4: number
}
/** Top-layer bars of the footing as heights from its 상면 (B5 and B1, §4.2). */
export function footingTopBars(Hf: number): TopBar[] {
const z = layers(Hf);
export function footingTopBars(Hf: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): TopBar[] {
const z = layers(Hf, f);
return [
{ name: 'B5', z: z.B5 - Hf, d: NOMINAL_DIAMETER.S29 },
{ name: 'B1', z: z.B1 - Hf, d: NOMINAL_DIAMETER.S25 },
@@ -115,7 +118,7 @@ export function columnRebar(x: ColumnInput = COLUMN_P1, m: Material = DEFAULT_MA
const lD2 = developmentLength(dS32, fyOf(m, 'S32'), 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 zBot = -x.Hf + layers(x.Hf, x.footing ?? DEFAULT_FOOTING_REBAR).B7 + (NOMINAL_DIAMETER.S19 + dS32) / 2;
// Couplers at k·ctc from the footing, kept at least the margin below the P1 top (커플러 여유).
const couplerMargin = x.couplerTopMargin ?? 2500;
const couplerCount = Math.max(0, Math.floor((zTop1 - couplerMargin) / x.ctcCoupler));
@@ -145,7 +148,7 @@ export function columnRebar(x: ColumnInput = COLUMN_P1, m: Material = DEFAULT_MA
const z5last = lE - n5all * x.ctcP5;
const m4 = Math.floor((z5last + x.rP4 * x.Hf) / x.ctcP4);
const p4All = Array.from({ length: m4 }, (_, j) => z5last - (j + 1) * x.ctcP4);
const p4Check = deleteInterferingP4(p4All, footingTopBars(x.Hf));
const p4Check = deleteInterferingP4(p4All, footingTopBars(x.Hf, x.footing ?? DEFAULT_FOOTING_REBAR));
const p4 = p4Check.kept;
// P6 crossties (기둥철근.md §7): 4 bars per 단, from the first kept P4
+68
View File
@@ -0,0 +1,68 @@
/**
* 확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초). The editable inputs of the 확대기초 panel.
* Defaults are the 확대기초철근 (1).md values; the ranges are proposed and pending the design-standard check.
* Fixed values (k = 2, Δ₂ = 100, 전단 4s · 엇갈림 2s, cycle 교대 배치) are not inputs.
*/
export interface FootingRebarSettings {
/** 상면 피복 (최외측 철근 중심까지, mm). */
cTop: number;
/** 측면 피복 (mm). */
cSide: number;
/** 하면 피복 (mm). */
cBottom: number;
/** 첫 철근 최소 여유 (측면 피복 다음, mm). */
eMin: number;
/** 격자 실간격 s (mm). */
s: number;
/** B10 높이 간격 s_v (mm). */
sV: number;
/** B5 아래 최소 여유 e_top (mm). */
eTop: number;
/** 조립 단 배수 n (조립 단 간격 = n·s_v). */
n: number;
}
export const DEFAULT_FOOTING_REBAR: FootingRebarSettings = {
cTop: 100,
cSide: 100,
cBottom: 150,
eMin: 150,
s: 125,
sV: 200,
eTop: 150,
n: 2,
};
/** Allowed range per input (inclusive, mm; n is a count). */
export const FOOTING_REBAR_RANGES: Record<keyof FootingRebarSettings, [number, number]> = {
cTop: [80, 150],
cSide: [80, 150],
cBottom: [100, 200],
eMin: [100, 200],
s: [100, 150],
sV: [150, 250],
eTop: [100, 200],
n: [1, 3],
};
export interface FootingSettingsIssue {
code: 'FT01' | 'FT02';
key: keyof FootingRebarSettings;
message: string;
}
/** FT01: a value is not a finite number or is out of range (W). FT02: the 조립 단 배수 n is not an integer. */
export function footingSettingsIssues(s: FootingRebarSettings): FootingSettingsIssue[] {
const out: FootingSettingsIssue[] = [];
for (const key of Object.keys(FOOTING_REBAR_RANGES) as (keyof FootingRebarSettings)[]) {
const v = s[key];
const [lo, hi] = FOOTING_REBAR_RANGES[key];
if (!Number.isFinite(v) || v < lo || v > hi) {
out.push({ code: 'FT01', key, message: `FT01 ${key} ${v} 범위 밖 (${lo}–${hi})` });
}
}
if (Number.isFinite(s.n) && !Number.isInteger(s.n)) {
out.push({ code: 'FT02', key: 'n', message: `FT02 조립 단 배수 ${s.n}는 정수여야 합니다` });
}
return out;
}
+53 -67
View File
@@ -1,26 +1,15 @@
import { hookLength, NOMINAL_DIAMETER, type BarSize } from './common';
import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings';
/**
* 확대기초 철근 B1–B8, B10–B12 (plan C5). Source: RAG/확대기초철근 (1).md, 2026-10-22 최종 정리.
* B9 (전단철근) is not computed yet. Lengths in mm, heights in mm from the 확대기초 하면.
*/
export const FOOTING_DEFAULTS = {
/** 피복 상면 / 측면 / 하면 (최외측 철근 중심까지). */
cTop: 100,
cSide: 100,
cBottom: 150,
/** 첫 철근 최소 여유. */
eMin: 150,
/** 격자 실간격 s. */
s: 125,
/** 하면 1단과 2단의 간격 Δ_2. */
delta2: 100,
/** B10 높이 간격 s_v and 조립 단 배수 n. */
sV: 200,
n: 2,
/** 상단 최소 여유 e_top (B10 top level, §7.1). */
eTop: 150,
} as const;
/** 확대기초 고정 규칙 (사양): Δ₂ 하면 1·2단 간격 = 100 (고정). The other values are the editable defaults. */
export const DELTA2 = 100;
/** Kept for reference; the calculations read the settings passed in (승인 2026-10-26). */
export const FOOTING_DEFAULTS = { ...DEFAULT_FOOTING_REBAR, delta2: DELTA2 } as const;
export interface GridResult {
/** 격자 칸 수 n = floor((A − 2·e_min)/s); 위치 수 = n + 1. */
@@ -34,10 +23,10 @@ export interface GridResult {
}
/** Grid along one side of the footing (§3.1). side = W for B1–B4, L for B5–B8. */
export function grid(side: number, s: number = FOOTING_DEFAULTS.s, cSide: number = FOOTING_DEFAULTS.cSide, eMin: number = FOOTING_DEFAULTS.eMin): GridResult {
const A = side - 2 * cSide;
const n = Math.floor((A - 2 * eMin) / s);
const G = n * s;
export function grid(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): GridResult {
const A = side - 2 * f.cSide;
const n = Math.floor((A - 2 * f.eMin) / f.s);
const G = n * f.s;
const e = (A - G) / 2;
return { n, G, e, positions: n + 1 };
}
@@ -57,18 +46,17 @@ export interface Layers {
B7: number;
}
export function layers(H: number): Layers {
const { cTop, cBottom, delta2 } = FOOTING_DEFAULTS;
const zB5 = H - cTop;
export function layers(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): Layers {
const zB5 = H - f.cTop;
const zB1 = zB5 - (d('S29') + d('S25')) / 2;
const zB24 = cBottom;
const zB68 = cBottom + (d('S25') + d('S29')) / 2;
return { B5: zB5, B1: zB1, B24: zB24, B68: zB68, B3: zB24 + delta2, B7: zB68 + delta2 };
const zB24 = f.cBottom;
const zB68 = f.cBottom + (d('S25') + d('S29')) / 2;
return { B5: zB5, B1: zB1, B24: zB24, B68: zB68, B3: zB24 + DELTA2, B7: zB68 + DELTA2 };
}
/** Lengths of the B1–B4 family along the length L (§5.2). */
export function legB2(H: number): number {
const z = layers(H);
export function legB2(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number {
const z = layers(H, f);
return z.B1 - z.B24; // 1723 at H 2000
}
@@ -82,13 +70,13 @@ export interface FootingBar {
}
/** B1–B4 (§5): the grid along W, with lengths from L. */
export function bottomBars(L: number, W: number, H: number): FootingBar[] {
const g = grid(W);
export function bottomBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] {
const g = grid(W, f);
const n = g.positions;
const N4 = Math.ceil(n / 2); // B4 at even i
const N2 = Math.floor(n / 2); // B2 at odd i
const l0 = L - 2 * FOOTING_DEFAULTS.cSide;
const leg2 = legB2(H);
const l0 = L - 2 * f.cSide;
const leg2 = legB2(H, f);
return [
{ name: 'B1', size: 'S25', count: N2, length: l0 + 2 * legLength('S25') },
{ name: 'B2', size: 'S25', count: N2, length: l0 + 2 * leg2 },
@@ -98,13 +86,13 @@ export function bottomBars(L: number, W: number, H: number): FootingBar[] {
}
/** B5–B8 (§6): the grid along L, with lengths from W. */
export function crossBars(L: number, W: number, H: number): FootingBar[] {
const g = grid(L);
export function crossBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] {
const g = grid(L, f);
const n = g.positions;
const N8 = Math.ceil(n / 2); // B8 at even i
const N6 = Math.floor(n / 2); // B6, B5, B7 at odd i
const l0 = W - 2 * FOOTING_DEFAULTS.cSide;
const z = layers(H);
const l0 = W - 2 * f.cSide;
const z = layers(H, f);
const leg6 = z.B5 - z.B68;
return [
{ name: 'B5', size: 'S29', count: N6, length: l0 + 2 * legLength('S29') },
@@ -115,19 +103,19 @@ export function crossBars(L: number, W: number, H: number): FootingBar[] {
}
/** 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);
export function b10Levels(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number {
const m = Math.floor((layers(H, f).B5 - f.eTop - f.cBottom) / f.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;
export const sideDepth = (dOuter: number, dB10: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR) => f.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, 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);
export function sideBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] {
const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f); // B10-1: 123.85
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f); // B10-2: 122.25
const levels = b10Levels(H, f);
const hook = hookLength('main90', d('S19'));
return [
{ name: 'B10-1', size: 'S19', count: levels * 2, length: L - 2 * depth2 + 2 * hook },
@@ -136,10 +124,10 @@ export function sideBars(L: number, W: number, H: number): FootingBar[] {
}
/** 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;
export function assemblyJs(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number[] {
const m = b10Levels(H, f) - 1;
const js: number[] = [];
for (let j = FOOTING_DEFAULTS.n; j < m; j += FOOTING_DEFAULTS.n) js.push(j);
for (let j = f.n; j < m; j += f.n) js.push(j);
return js;
}
@@ -149,21 +137,21 @@ export function assemblyEndOffset(): number {
}
/** 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, H: number): FootingBar[] {
const levelsJ = assemblyJs(H);
export function assemblyBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] {
const levelsJ = assemblyJs(H, f);
const perLevel = (n: number, startOdd: number, startEven: number) =>
levelsJ.map((_, idx) => {
const i0 = idx % 2 === 0 ? startOdd : startEven;
return Math.floor((n - i0) / 4) + 1;
});
const nW = grid(W).positions - 1; // 61
const nL = grid(L).positions - 1; // 79
const nW = grid(W, f).positions - 1; // 61
const nL = grid(L, f).positions - 1; // 79
// 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 = 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);
const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f);
const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f);
return [
{ 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 },
@@ -171,9 +159,9 @@ export function assemblyBars(L: number, W: number, H: number): FootingBar[] {
}
/** 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);
export function gridPositions(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number[] {
const g = grid(side, f);
return Array.from({ length: g.positions }, (_, i) => -side / 2 + f.cSide + g.e + i * f.s);
}
/** B9 bar of the rule (§9.1, Q-B18): a vertical bar at (x, n), mm from the footing centre. */
@@ -187,9 +175,9 @@ export interface B9Bar {
* 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 b9Bars(L: number, W: number, columnX: number[], columnD: number = 2000): { rows: number; perRow: number[]; bars: B9Bar[] } {
const xs = gridPositions(L);
const ns = gridPositions(W);
export function b9Bars(L: number, W: number, columnX: number[], columnD: number = 2000, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { rows: number; perRow: number[]; bars: B9Bar[] } {
const xs = gridPositions(L, f);
const ns = gridPositions(W, f);
const rowIndices: number[] = [];
for (let i = 1; i < ns.length; i += 4) rowIndices.push(i);
const halfBar = NOMINAL_DIAMETER.H16 / 2;
@@ -213,9 +201,9 @@ export function b9Bars(L: number, W: number, columnX: number[], columnD: number
* 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
export function shearB9(L: number, W: number, H: number, columnX: number[] = [], columnD: number = 2000, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { rows: number; perRow: number[]; count: number; length: number; bars: B9Bar[] } {
const { rows, perRow, bars } = b9Bars(L, W, columnX, columnD, f);
const lv = layers(H, f).B5 - layers(H, f).B24; // 1750
const lh = hookLength('stirrup135', d('H16'));
return { rows, perRow, count: bars.length, length: lv + 2 * lh, bars };
}
@@ -237,11 +225,9 @@ 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(H: number): { B11: number[]; B12: number[] } {
const { cBottom } = FOOTING_DEFAULTS;
const sV = FOOTING_DEFAULTS.sV;
const js = assemblyJs(H);
const B11 = js.map((j) => cBottom + j * sV);
export function assemblyHeights(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { B11: number[]; B12: number[] } {
const js = assemblyJs(H, f);
const B11 = js.map((j) => f.cBottom + j * f.sV);
const B12 = B11.map((z) => z + NOMINAL_DIAMETER.S19);
return { B11, B12 };
}
+8 -7
View File
@@ -2,6 +2,7 @@ import type { PierModel } from '../geometry';
import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block';
import { DEFAULT_COPING_REBAR } from './coping-settings';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
import type { FootingRebarSettings } from './footing-settings';
import { bottomPiece, topBarLength, widthLayout, topSplicePieces } 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';
@@ -105,13 +106,13 @@ 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, columnX: number[]): FootingBar[] {
function footingBarList(Lf: number, Wf: number, Hf: number, columnX: number[], f: FootingRebarSettings): 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, Hf)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf, Hf)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf, columnX);
for (const b of bottomBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '교각축' });
for (const b of crossBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '교각축 직각' });
for (const b of sideBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf, Hf, f)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf, columnX, 2000, f);
out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행, 기둥 구역 제외 (위치·갈고리는 Q-B18·B19 규칙 적용)` });
return out;
}
@@ -287,7 +288,7 @@ export function rebarSummary(
for (const f of model.footings) {
// 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)) {
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, columnX, model.footingRebar)) {
const cur = agg.get(b.name);
if (cur) {
cur.count += b.count;
+3
View File
@@ -1,5 +1,6 @@
import type { CopingRebarSettings, TopSpliceSettings } from './rebar/coping-settings';
import type { ColumnRebarSettings } from './rebar/column-settings';
import type { FootingRebarSettings } from './rebar/footing-settings';
export type PierType = 'T형' | '2주형' | '3주형';
export type FoundationType = '말뚝기초' | '직접기초' | '현장타설말뚝';
@@ -83,6 +84,8 @@ export interface PierInput {
topSplice?: Partial<TopSpliceSettings>;
/** 기둥 배근 입력 (승인 2026-10-26, 시안 ③ 기둥). Absent = the 기둥철근 (1).md defaults. */
columnRebar?: Partial<ColumnRebarSettings>;
/** 확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초). Absent = the 확대기초철근 (1).md defaults. */
footingRebar?: Partial<FootingRebarSettings>;
strataRows?: StrataRow[];
}
+77
View File
@@ -30,6 +30,8 @@ import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/beari
import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './core/rebar/coping-settings';
import { COLUMN_REBAR_RANGES, type ColumnRebarSettings, columnSettingsIssues, DEFAULT_COLUMN_REBAR } from './core/rebar/column-settings';
import { columnInputFor, columnRebar } from './core/rebar/column';
import { DEFAULT_FOOTING_REBAR, FOOTING_REBAR_RANGES, type FootingRebarSettings, footingSettingsIssues } from './core/rebar/footing-settings';
import { legB2 } from './core/rebar/footing';
import { columnElevationSvg } from './view/column-elevation-svg';
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
import { groundSectionSvg } from './view/ground-svg';
@@ -1290,6 +1292,80 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void {
box.appendChild(wrap);
}
/** 확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초). Fixed rules (k = 2, Δ₂ = 100, 전단 4s · 엇갈림 2s, cycle 교대) are shown, not edited. */
const FOOTING_REBAR_FIELDS: { key: keyof FootingRebarSettings; label: string }[] = [
{ key: 'cTop', label: '상면 피복 (철근 중심, mm)' },
{ key: 'cSide', label: '측면 피복 (mm)' },
{ key: 'cBottom', label: '하면 피복 (mm)' },
{ key: 'eMin', label: '첫 철근 최소 여유 (mm)' },
{ key: 's', label: '격자 실간격 s (mm)' },
{ key: 'sV', label: 'B10 높이 간격 s_v (mm)' },
{ key: 'eTop', label: 'B5 아래 최소 여유 e_top (mm)' },
{ key: 'n', label: '조립 단 배수 n (B10 간격의 n배)' },
];
/** 적용: the values go to the draft and the model is rebuilt only when every check passes (FT01·FT02). */
function applyFootingRebar(name: string, next: FootingRebarSettings): void {
const p = state.piers.find((q) => q.input.name === name);
if (!p) return;
p.draft = { ...p.draft, footingRebar: next };
regenerateOne(name);
}
function renderFootingSettings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
const wrap = el('div', undefined, 'bearing-ref');
const current: FootingRebarSettings = { ...DEFAULT_FOOTING_REBAR, ...pier.draft.footingRebar };
wrap.appendChild(el('p', '확대기초 배근 입력 (기본값 = 확대기초철근 (1).md · 적용을 눌러야 반영)', 'lbl'));
const grid = el('div', undefined, 'edit-fields');
const inputs = new Map<keyof FootingRebarSettings, HTMLInputElement>();
for (const f of FOOTING_REBAR_FIELDS) {
const row = el('div', undefined, 'edit-row');
const label = el('label', f.label);
const input = el('input');
input.type = 'number';
input.step = 'any';
input.value = String(current[f.key]);
input.id = `footing-${f.key}`;
label.htmlFor = input.id;
inputs.set(f.key, input);
const [lo, hi] = FOOTING_REBAR_RANGES[f.key];
const changed = current[f.key] !== DEFAULT_FOOTING_REBAR[f.key] ? ' · 기본과 다름' : '';
row.append(label, input, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_FOOTING_REBAR[f.key]}${changed}`, 'muted small'));
grid.appendChild(row);
}
wrap.appendChild(grid);
// Fixed rules and derived values (읽기 전용). Derived from the current settings of the first footing.
const fs = current;
const fixed = el('div', undefined, 'muted small');
const H = pier.model.footings[0] ? (pier.model.footings[0].top - pier.model.footings[0].bottom) * 1000 : undefined;
fixed.textContent = [
`상면·2단 간격 = 2s (고정) ${2 * fs.s} mm`,
`하면 1·2단 간격 Δ₂ (고정) 100 mm`,
`전단 B9 간격 4s · 행 엇갈림 2s (고정) ${4 * fs.s} · ${2 * fs.s} mm`,
`조립 평면 간격 4s · 연직 n·s_v (자동) ${4 * fs.s} · ${fs.n * fs.sV} mm`,
`cycle 배치: 교대 배치 (고정)`,
H === undefined ? 'B2·B6 디리 (자동): 확대기초 없음' : `B2·B6 디리 (자동) z_B1 − z_B24 = ${legB2(H, fs)} mm`,
].join(' · ');
wrap.appendChild(fixed);
const msg = el('p', undefined, 'muted small');
const apply = el('button', '적용', 'btn small');
apply.type = 'button';
apply.addEventListener('click', () => {
const next = { ...current };
for (const f of FOOTING_REBAR_FIELDS) next[f.key] = Number(inputs.get(f.key)?.value);
const issues = footingSettingsIssues(next);
if (issues.length > 0) {
msg.textContent = issues.map((i) => i.message).join(' · ');
setError('확대기초 배근 입력을 확인하세요. 값은 반영되지 않았습니다.');
return;
}
applyFootingRebar(pier.input.name, next);
});
wrap.append(msg, apply);
box.appendChild(wrap);
}
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model || !rebar3d) return;
const L = pier.model.copingL;
@@ -1366,6 +1442,7 @@ function renderRebar(): void {
if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier);
if (state.rebarMember === 'coping') renderCopingSettings(box, pier);
if (state.rebarMember === 'column') renderColumnSettings(box, pier);
if (state.rebarMember === 'footing') renderFootingSettings(box, pier);
const shown = rows.filter((r) => r.member === state.rebarMember);
box.appendChild(rebarSelectBar(shown.map((r) => r.name)));
const table = el('table', undefined, 'grid-table rebar-table');
+17 -15
View File
@@ -1,6 +1,6 @@
import * as THREE from 'three';
import type { PierModel } from '../core/geometry';
import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, FOOTING_DEFAULTS, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing';
import { assemblyEndOffset, assemblyJs, b10Levels, b9Bars, gridPositions, grid, layers, legLength, legOffsets, sideDepth } from '../core/rebar/footing';
import { bottomPiece, MAIN, topSplicePieces, widthLayout } from '../core/rebar/coping-main';
import { C14_INSET, c14Bars, CAP, COPING_REBAR, SHEAR_LOOP_STEP, shearBars, shearBarsMulti, shearUHeight, copingSideBars, SIDE_HOOK_LEG, stairBars, undersideAt } from '../core/rebar/coping';
import { sideRowLevels } from '../core/coping-geom';
@@ -381,11 +381,13 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const Lf = f.length;
const Wf = f.width;
const Hf = (f.top - f.bottom) * 1000;
const z = layers(Hf);
const gW = grid(Wf * 1000);
const gL = grid(Lf * 1000);
const posW = (j: number) => -Wf / 2 + mm(100 + gW.e + j * 125);
const posL = (i: number) => -Lf / 2 + mm(100 + gL.e + i * 125);
// 확대기초 입력 (승인 2026-10-26): the grid, covers and layer heights come from the model's footing settings.
const fs = m.footingRebar;
const z = layers(Hf, fs);
const gW = grid(Wf * 1000, fs);
const gL = grid(Lf * 1000, fs);
const posW = (j: number) => -Wf / 2 + mm(fs.cSide + gW.e + j * fs.s);
const posL = (i: number) => -Lf / 2 + mm(fs.cSide + gL.e + i * fs.s);
const sA = -Lf / 2 + 0.1;
const sB = Lf / 2 - 0.1;
const nA = -Wf / 2 + 0.1;
@@ -435,7 +437,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
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) {
for (const b of b9Bars(Lf * 1000, Wf * 1000, colX, 2000, fs).bars) {
const x = mm(b.x);
const n = mm(b.n);
const tailDir = x >= 0 ? -1 : 1;
@@ -450,13 +452,13 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
};
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 d1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, fs); // B10-1 depth (mm), on the W faces
const d2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, fs); // 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 (let j = 0; j < b10Levels(Hf, fs); j++) {
const zj = bottom + mm(fs.cBottom + j * fs.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;
@@ -468,13 +470,13 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
}
}
// 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 xsL = gridPositions(Lf * 1000, fs);
const ysW = gridPositions(Wf * 1000, fs);
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);
assemblyJs(Hf, fs).forEach((jA, k) => {
const zA = bottom + mm(fs.cBottom + jA * fs.sV);
const zB = zA + dB10;
const i0 = k % 2 === 0 ? 0 : 2;
for (let iL = i0; iL < xsL.length; iL += 4) {
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { assemblyJs, gridPositions } from '../src/core/rebar/footing';
import { DEFAULT_FOOTING_REBAR, FOOTING_REBAR_RANGES, footingSettingsIssues } from '../src/core/rebar/footing-settings';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
describe('확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초)', () => {
it('the defaults are inside every range and raise no issue', () => {
expect(footingSettingsIssues(DEFAULT_FOOTING_REBAR)).toEqual([]);
for (const [key, [lo, hi]] of Object.entries(FOOTING_REBAR_RANGES)) {
const v = DEFAULT_FOOTING_REBAR[key as keyof typeof DEFAULT_FOOTING_REBAR];
expect(v >= lo && v <= hi, key).toBe(true);
}
});
it('a value outside its range gives FT01; a non-integer n gives FT02', () => {
expect(footingSettingsIssues({ ...DEFAULT_FOOTING_REBAR, cTop: 151 })).toEqual([expect.objectContaining({ code: 'FT01', key: 'cTop' })]);
expect(footingSettingsIssues({ ...DEFAULT_FOOTING_REBAR, n: 2.5 })).toEqual([expect.objectContaining({ code: 'FT02', key: 'n' })]);
});
it('the grid spacing s reaches the grid positions and the rebar table', () => {
const m = buildPier({ ...loadP1(), footingRebar: { s: 150 } });
expect(m.footingRebar.s).toBe(150);
const xs = gridPositions(8000, m.footingRebar);
expect(xs[1] - xs[0]).toBeCloseTo(150, 6);
const b4 = rebarSummary(m).rows.find((r) => r.name === 'B4')!;
const b4Default = rebarSummary(buildPier(loadP1())).rows.find((r) => r.name === 'B4')!;
expect(b4.count).not.toBe(b4Default.count);
});
it('the 조립 단 배수 n reaches the assembly levels', () => {
const H = 2000;
const base = assemblyJs(H, DEFAULT_FOOTING_REBAR);
const wider = assemblyJs(H, { ...DEFAULT_FOOTING_REBAR, n: 3 });
expect(wider.length).toBeLessThan(base.length);
expect(wider.every((j) => j % 3 === 0)).toBe(true);
});
});
+2 -1
View File
@@ -90,6 +90,7 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => {
});
import { assemblyHeights, assemblyJs, b10Levels, legOffsets } from '../src/core/rebar/footing';
import { DEFAULT_FOOTING_REBAR } from '../src/core/rebar/footing-settings';
describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () => {
it('leg offsets: B1 +25.4, B3 +22.25, B5 +28.6, B7 +23.85', () => {
@@ -119,7 +120,7 @@ describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () =>
});
it('variables: W 8000 with s 150 → 50 grid steps, e 150, B4 26 / B2 25; W 12000 → 92 steps, e 150', () => {
const g = grid(8000, 150);
const g = grid(8000, { ...DEFAULT_FOOTING_REBAR, s: 150 });
expect(g.n).toBe(50);
expect(g.e).toBeCloseTo(150, 6);
expect(grid(12000).n).toBe(92);