feat(rebar): 다주형 코핑 배근을 3주형형상배근.md 규칙으로 재작성
2주형 DWG 실측 규칙(§4–§8)을 생성기에 옮기고, 3D·삽도·철근표가 같은 조각을 쓴다. - 전단 C9·C10·C11 (H22): 끝면 피복선 1개 + @300, 기둥 구간 8@300, 기둥 옆 밀집 @150 (바깥 14 · 안쪽 16칸, 가운데 기둥 양쪽 16칸), 기둥 사이 ceil 등분. 번호는 그 자리 하면 (Level C10 · 오목 C11 · 테이퍼·헌치 C9). 다리 8개 두 묶음(가운데 250), 폐합 4개, 위 끝 = 계단 상면 − c - 상면 1단 C1(양끝 갈고리) · C4: 8 m 이음, 이음 중심 n등분점 ∓ l_s. 2단 C3 · C2 = 기둥 중심 + 4450까지, 가운데 기둥 위 C3-1 · C2-1 (± 4450). 폭 27개(cycle-1 14 · cycle-2 13) - 하면 C7: 전 길이, Level 꺾임점에서 철근을 따라 l_s/2씩 겹침, 양끝 위로 갈고리. C8: 경간마다 ± 2230 안쪽 - 측면 C12(상단 전 길이) · C13(하단, 하면에서 끊김) H16 @200, 다리 8개 위치마다 - C14 크로스타이 연직 600 · 교각축 1200 엇갈림, C15 끝단보다 100 이상 높은 계단 전체에 ⊓ - 경고 RB-M01(밀집 구간 겹침·코핑 끝 초과 → 위치를 합쳐 표시), M02, M03, M04, M05, M06, M08 - 다주형 기본값 피복 120 · 다리 8. 기둥 위치 입력 e_c · s_c 추가(Q-M10 미결, 없으면 L/5) - §11 재현 테스트: 2주형 112위치(간격 문자열 일치), 3주형 173위치 · C9 78 · C10 41 · C11 54, C1/C4 8 m 이음 조각, C3 10324 · C2 9930 · C3-1/C2-1 8900, C7 5조각, C8 8540 × 26, C12 6단 36960, C13 4333/4400, RB-M05 위치 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87b445965b
commit
364b329c54
7 files changed
+396
-131
No files matched your search
+12
-2
@@ -350,7 +350,7 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel {
|
||||
const lateralStep = stepSamples.some(s => Math.abs(segmentTop(segments,s)-segmentTop(backSegments,s)) >= DIM.lateralTolerance);
|
||||
|
||||
// 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);
|
||||
const colG = columnPositions(input.type, L, input.columnEndOffsetMm, input.columnSpacingMm);
|
||||
if (kinkG !== null) {
|
||||
const near = nearKink(axis, colG.map((g) => sStart + g), levelW / 2);
|
||||
if (near.length) throw new Error('미실시: 꺾임점 근처(Level 반폭 이내)에 기둥이 있습니다. 기둥 위치를 확인하세요.');
|
||||
@@ -551,8 +551,18 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel {
|
||||
};
|
||||
}
|
||||
|
||||
export function columnPositions(type: PierInput['type'], L: number): number[] {
|
||||
/**
|
||||
* Column centres along the coping (m). spec §4.5: L/5 · (L/2) · 4L/5. For 2·3주형 the 끝 여유 e_c and 간격 s_c may be typed
|
||||
* (Q-M10 미결, 3주형형상배근.md §2.1): e_c + k·s_c, s_c defaulting to the even spacing between e_c and L − e_c.
|
||||
*/
|
||||
export function columnPositions(type: PierInput['type'], L: number, endOffsetMm?: number, spacingMm?: number): number[] {
|
||||
if (type === 'T형') return [L / 2];
|
||||
const n = type === '2주형' ? 2 : 3;
|
||||
if (endOffsetMm !== undefined) {
|
||||
const e = endOffsetMm / 1000;
|
||||
const step = spacingMm !== undefined ? spacingMm / 1000 : (L - 2 * e) / (n - 1);
|
||||
return Array.from({ length: n }, (_, k) => e + k * step);
|
||||
}
|
||||
if (type === '2주형') return [L / 5, L - L / 5];
|
||||
return [L / 5, L / 2, L - L / 5];
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ export const DEFAULT_COPING_REBAR: CopingRebarSettings = {
|
||||
|
||||
/** 1106 ‘2주형 3주형’ 전용 삽도 우선(사용자 선택). 명시 입력은 이 기본값보다 우선한다. */
|
||||
export function copingDefaults(columnCount: number): CopingRebarSettings {
|
||||
return columnCount > 1 ? { ...DEFAULT_COPING_REBAR, spacingS: 300, spacingL: 300,
|
||||
// 다주형 (3주형형상배근.md §1.2): 피복 120 (Q-M6), 전단 @300 · 다리 8개 (Q-M3), 측면 @200, 크로스타이 연직 600.
|
||||
return columnCount > 1 ? { ...DEFAULT_COPING_REBAR, cover: 120, shearLegs: 8, spacingS: 300, spacingL: 300,
|
||||
sideSpacing: 200, c14UpperSpacing: 600, c14LowerSpacing: 600 } : { ...DEFAULT_COPING_REBAR };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* 다주형(2·3주형) 코핑 철근 — RAG/3주형형상배근.md §4–§8 (2026-11-06, 2주형 DWG 실측 규칙, 번호 = 역할 Q-M1).
|
||||
* Pure. One physical piece per entry; 3D, 삽도, 철근표, 재료표 share these pieces. T형 keeps its own generator.
|
||||
*
|
||||
* Coordinates: x along the coping from BP1 (m), EL (m), y across the width from the axis (m, the scene's third axis).
|
||||
* Rules still open (Q-M2 밀집 구간, Q-M3 다리 8개, Q-M4 3주형 확장, Q-M5 측면 형상, Q-M6 피복, Q-M7 계단부, Q-M12 이음)
|
||||
* use the document's current assumptions and are flagged in the summary.
|
||||
*/
|
||||
import type { PierModel } from '../geometry';
|
||||
import { sideRowLevels } from '../coping-geom';
|
||||
import { DEFAULT_MATERIAL, fyOf, hookLength, type Material, NOMINAL_DIAMETER, spliceLength } from './common';
|
||||
import { widthLayout } from './coping-main';
|
||||
import { C14_INSET, SIDE_HOOK_LEG, stairBars } from './coping';
|
||||
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, type Material, NOMINAL_DIAMETER, spliceLength } from './common';
|
||||
|
||||
export type CopingPoint = [number, number, number];
|
||||
export interface CopingBar { name: string; size: string; diameterMm: number; pts: CopingPoint[]; length: number }
|
||||
@@ -29,110 +34,275 @@ export function copingPathLength(pts: CopingPoint[]): number {
|
||||
return pts.slice(1).reduce((sum, p, i) => sum + Math.hypot(...p.map((v, j) => v - pts[i][j])), 0) * 1000;
|
||||
}
|
||||
|
||||
/** Shared physical pieces for multi-column 3D and quantities. T-shaped legacy layout is unchanged. */
|
||||
export function multicolCopingBars(model: PierModel, material: Material = DEFAULT_MATERIAL): CopingBar[] {
|
||||
/** Document values that are not screen inputs yet (3주형형상배근.md §1.2, m unless noted). */
|
||||
export const MULTI_REBAR = {
|
||||
/** 밀집 간격 s_d (mm) and counts: outer (end side) / inner (span side) of a column zone (Q-M2). */
|
||||
denseSpacingMm: 150, denseOuter: 14, denseInner: 16,
|
||||
/** Column zone around the centre: outer columns a_o … a_i (span side +), middle columns ± a_m (Q-M2). */
|
||||
zoneOuter: 1.15, zoneInner: 1.25, zoneMiddle: 1.2,
|
||||
/** Gap between the two leg groups (Q-M3). */
|
||||
legGroupGap: 0.25,
|
||||
/** 상면 2단 끊는 위치 l_cut and C8 끝 l_8 from the column centre. */
|
||||
topCut: 4.45, c8End: 2.23,
|
||||
/** 정척 for the 상면 1단 이음 (Q-M9, mm). */
|
||||
standardMm: 8000,
|
||||
/** C14: vertical pitch, pitch along the axis, stagger between rows. */
|
||||
c14Vertical: 0.6, c14Along: 1.2, c14Stagger: 0.6,
|
||||
/** h_16: a step at least this much above the end top gets C15 (m). */
|
||||
stepMin: 0.1,
|
||||
} as const;
|
||||
|
||||
export interface MulticolInfo { bars: CopingBar[]; warnings: { code: string; message: string }[] }
|
||||
|
||||
/**
|
||||
* Shear positions along the coping (§4.1): end, dense, column zone, dense, span … (sorted, m). Positions outside the cover
|
||||
* lines are dropped; `short` lists spans where the dense zones meet or overlap (RB-M01) and `tight` spans whose equal bay
|
||||
* is below s_d (RB-M02).
|
||||
*/
|
||||
export function multicolShearLayout(L: number, columns: number[], c: number, s: number, sd: number): { positions: number[]; short: number[]; tight: number[] } {
|
||||
const xs = [...columns].sort((a, b) => a - b);
|
||||
const n = xs.length;
|
||||
const out: number[] = [];
|
||||
const run = (from: number, to: number, count: number) => { for (let j = 0; j <= count; j++) out.push(from + (to - from) * j / count); };
|
||||
const groups = xs.map((x, k) => {
|
||||
const zone: [number, number] = n === 1 ? [x - MULTI_REBAR.zoneMiddle, x + MULTI_REBAR.zoneMiddle]
|
||||
: k === 0 ? [x - MULTI_REBAR.zoneOuter, x + MULTI_REBAR.zoneInner]
|
||||
: k === n - 1 ? [x - MULTI_REBAR.zoneInner, x + MULTI_REBAR.zoneOuter]
|
||||
: [x - MULTI_REBAR.zoneMiddle, x + MULTI_REBAR.zoneMiddle];
|
||||
const left = k === 0 ? MULTI_REBAR.denseOuter : MULTI_REBAR.denseInner;
|
||||
const right = k === n - 1 ? MULTI_REBAR.denseOuter : MULTI_REBAR.denseInner;
|
||||
const start = zone[0] - left * sd, end = zone[1] + right * sd;
|
||||
run(start, zone[0], left);
|
||||
run(zone[0], zone[1], Math.max(1, Math.round((zone[1] - zone[0]) / s)));
|
||||
run(zone[1], end, right);
|
||||
return [start, end] as const;
|
||||
});
|
||||
// Ends: the bar at the cover line, then @s from the dense start; the remainder is the first space (§4.1).
|
||||
out.push(c, L - c);
|
||||
for (let x = groups[0][0] - s; x > c + 1e-6; x -= s) out.push(x);
|
||||
for (let x = groups[n - 1][1] + s; x < L - c - 1e-6; x += s) out.push(x);
|
||||
// Spans between dense zones: m = ceil(l / s) equal bays.
|
||||
const short: number[] = [], tight: number[] = [];
|
||||
for (let k = 1; k < n; k++) {
|
||||
const a = groups[k - 1][1], b = groups[k][0];
|
||||
if (b - a < 1e-6) { short.push(k); continue; }
|
||||
const m = Math.ceil((b - a) / s - 1e-9);
|
||||
if ((b - a) / m < sd - 1e-9) tight.push(k);
|
||||
for (let j = 1; j < m; j++) out.push(a + (b - a) * j / m);
|
||||
}
|
||||
if (groups[0][0] < c - 1e-9 || groups[n - 1][1] > L - c + 1e-9) short.push(0);
|
||||
const inside = out.filter(x => x >= c - 1e-9 && x <= L - c + 1e-9);
|
||||
return { positions: [...new Map(inside.map(x => [Math.round(x * 1e6), x])).values()].sort((a, b) => a - b), short, tight };
|
||||
}
|
||||
|
||||
/** Shear positions only (see multicolShearLayout). */
|
||||
export function multicolShearPositions(L: number, columns: number[], c: number, s: number, sd: number): number[] {
|
||||
return multicolShearLayout(L, columns, c, s, sd).positions;
|
||||
}
|
||||
|
||||
/** Leg positions across the width (§4.3): two groups of four, 250 apart; otherwise equal spacing. Closures by index. */
|
||||
export function multicolLegs(B: number, c: number, legs: number): { y: number[]; closures: [number, number][] } {
|
||||
if (legs === 8) {
|
||||
const p = (B - 2 * c - MULTI_REBAR.legGroupGap) / 6;
|
||||
const y = [0, 1, 2, 3].map(i => -B / 2 + c + i * p);
|
||||
y.push(...[0, 1, 2, 3].map(i => -B / 2 + c + 3 * p + MULTI_REBAR.legGroupGap + i * p));
|
||||
return { y, closures: [[0, 2], [1, 4], [3, 6], [5, 7]] };
|
||||
}
|
||||
const pitch = (B - 2 * c) / (legs - 1);
|
||||
const y = Array.from({ length: legs }, (_, i) => -B / 2 + c + i * pitch);
|
||||
return { y, closures: Array.from({ length: legs / 2 }, (_, i) => [i, i + legs / 2] as [number, number]) };
|
||||
}
|
||||
|
||||
/** Bars across the width: the edge bars at the cover line and `spaces` bays of `pitch` between, leftover split (§5.1). */
|
||||
function widthBars(B: number, c: number, pitch: number): number[] {
|
||||
const spaces = Math.floor((B - 2 * c) / pitch + 1e-9) - 2;
|
||||
const g = (B - 2 * c - spaces * pitch) / 2;
|
||||
return [-B / 2 + c, ...Array.from({ length: spaces + 1 }, (_, i) => -B / 2 + c + g + i * pitch), B / 2 - c];
|
||||
}
|
||||
|
||||
/** Shared physical pieces for multi-column 3D and quantities, with the RB-M warnings of §11. */
|
||||
export function multicolCopingInfo(model: PierModel, material: Material = DEFAULT_MATERIAL): MulticolInfo {
|
||||
const bars: CopingBar[] = [];
|
||||
const L = model.copingL, B = model.copingB, top = model.copingRefTop;
|
||||
const warnings: MulticolInfo['warnings'] = [];
|
||||
const L = model.copingL, B = model.copingB;
|
||||
const settings = model.copingRebar;
|
||||
if ([settings.spacingS, settings.spacingL, settings.sideSpacing, settings.c14UpperSpacing, settings.c14LowerSpacing].some(v => !Number.isFinite(v) || v <= 0)
|
||||
if ([settings.spacingS, settings.sideSpacing, settings.mainSpacing, settings.mainLayerGap, settings.cover].some(v => !Number.isFinite(v) || v <= 0)
|
||||
|| !Number.isInteger(settings.shearLegs) || settings.shearLegs < 2 || settings.shearLegs > 8 || settings.shearLegs % 2) {
|
||||
throw new Error('다주형 배근 간격은 양수, 전단 다리 수는 2~8의 짝수여야 합니다.');
|
||||
}
|
||||
const c = settings.cover / 1000, s = settings.spacingS / 1000, sd = MULTI_REBAR.denseSpacingMm / 1000;
|
||||
const envelope = copingEnvelope(model);
|
||||
const add = (name: string, size: keyof typeof NOMINAL_DIAMETER, pts: CopingPoint[]) => {
|
||||
if (pts.length < 2) return;
|
||||
bars.push({ name, size, diameterMm: NOMINAL_DIAMETER[size], pts, length: copingPathLength(pts) });
|
||||
};
|
||||
const width = widthLayout(B * 1000, settings);
|
||||
const ns = [width.yE, ...Array.from({ length: width.n + 1 }, (_, i) => width.yE + width.g + width.spacing * i), B * 1000 - width.yE].map(v => v / 1000 - B / 2);
|
||||
// C5/C6 are excluded from the multi-column layout: the 1106 전용 삽도 (A92 · A61) shows no C5/C6 (대안A, 2026-11-06).
|
||||
// Cycle-specific splice locations remain explicitly pending.
|
||||
for (let k = 1; k <= 3; k++) for (const [j, n] of ns.entries()) {
|
||||
if (j % 2 === 0 && k > 1) continue;
|
||||
const name = `C${k + (j % 2 ? 0 : 3)}`;
|
||||
const size = Number(name.slice(1)) <= 4 ? 'H25' : 'H29';
|
||||
const d = NOMINAL_DIAMETER[size];
|
||||
const a = settings.cover / 1000 + (k - 1) * d / 1000, b = L - a;
|
||||
const z = top - 0.127 - (k - 1) * settings.mainLayerGap / 1000;
|
||||
const hook = hookLength('main90', d) / 1000;
|
||||
const total = (b - a + 2 * hook) * 1000;
|
||||
const lap = spliceLength(d, fyOf(material, size), material.fck) / 1000;
|
||||
if (model.topSplice.mode === 'apply' && total > 12000) {
|
||||
const split = model.topSplice.at === 'center' ? L / 2 : b + hook + lap / 2 - 12;
|
||||
add(name, size, [[a, z - hook, n], [a, z, n], [split + lap / 2, z, n]]);
|
||||
add(name, size, [[split - lap / 2, z, n], [b, z, n], [b, z - hook, n]]);
|
||||
} else add(name, size, [[a, z - hook, n], [a, z, n], [b, z, n], [b, z - hook, n]]);
|
||||
const columns = model.columns.map(col => col.s).sort((a, b) => a - b);
|
||||
const lw = model.copingGeom.levelWidth / 1000;
|
||||
const levelBottom = model.levelBottom;
|
||||
const hr = Math.max(0, ...envelope.xs.map(x => envelope.under(x) - levelBottom).filter(v => v < (model.endBottom - levelBottom) - 1e-6));
|
||||
const steps = [...model.segments, ...(model.backSegments ?? [])];
|
||||
const stepTop = (x: number) => Math.min(...steps.filter(g => x >= g.s0 - 1e-9 && x <= g.s1 + 1e-9).map(g => g.top));
|
||||
const d = (size: keyof typeof NOMINAL_DIAMETER) => NOMINAL_DIAMETER[size];
|
||||
const fy = (size: string) => fyOf(material, size);
|
||||
|
||||
// --- 전단 C9 · C10 · C11 (§4) -------------------------------------------------------------------
|
||||
const legs = multicolLegs(B, c, settings.shearLegs);
|
||||
const ds = d('H22') / 1000;
|
||||
const shearName = (x: number) => {
|
||||
const rel = envelope.under(x) - levelBottom;
|
||||
if (Math.abs(rel) < 1e-6) return 'C10';
|
||||
if (hr > 0 && Math.abs(rel - hr) < 1e-6) return 'C11';
|
||||
return 'C9';
|
||||
};
|
||||
const layout = multicolShearLayout(L, columns, c, s, sd);
|
||||
if (layout.short.length) warnings.push({ code: 'RB-M01', message: `코핑이 짧아 기둥 옆 밀집 구간(바깥 ${MULTI_REBAR.denseOuter} · 안쪽 ${MULTI_REBAR.denseInner}칸 @${MULTI_REBAR.denseSpacingMm})이 ${layout.short.includes(0) ? '코핑 끝을 넘거나 ' : ''}서로 겹칩니다. 겹친 위치를 합쳐 표시했습니다 — 3주형형상배근.md는 오류로 정합니다 (Q-M2 밀집 구간 규칙 확인).` });
|
||||
if (layout.tight.length) warnings.push({ code: 'RB-M02', message: `기둥 사이 등분 간격이 밀집 간격 ${MULTI_REBAR.denseSpacingMm}보다 작습니다 (경간 ${layout.tight.join(', ')}).` });
|
||||
for (const x of layout.positions) {
|
||||
const name = shearName(x);
|
||||
// The closures (U + cap each) sit side by side, centred on the position so the end-line bars stay inside the coping.
|
||||
const span = (legs.closures.length * 2 - 1) * ds;
|
||||
legs.closures.forEach(([i, j], q) => {
|
||||
const xu = x - span / 2 + q * 2 * ds, xc = xu + ds;
|
||||
const yTop = stepTop(x) - c;
|
||||
const bottom = envelope.inset(xu, c);
|
||||
const capBottom = envelope.inset(xc, c) + 0.1;
|
||||
add(name, 'H22', [[xu, yTop, legs.y[i]], [xu, bottom, legs.y[i]], [xu, bottom, legs.y[j]], [xu, yTop, legs.y[j]]]);
|
||||
add(name, 'H22', [[xc, capBottom, legs.y[i]], [xc, yTop, legs.y[i]], [xc, yTop, legs.y[j]], [xc, capBottom, legs.y[j]]]);
|
||||
});
|
||||
}
|
||||
for (const [j, n] of ns.entries()) {
|
||||
const name = j % 2 ? 'C7' : 'C8', size = j % 2 ? 'H29' : 'H25';
|
||||
const lap = spliceLength(NOMINAL_DIAMETER[size], fyOf(material, size), material.fck) / 2000;
|
||||
const a = settings.cover / 1000, b = L - a;
|
||||
const z = (x: number) => envelope.inset(x, (settings.cover + width.yE - 100) / 1000);
|
||||
const path = (from: number, to: number): CopingPoint[] => [from, ...envelope.xs.filter(x => x > from && x < to), to].map(x => [x, z(x), n]);
|
||||
add(name, size, [[a, top - 0.127, n], ...path(a, L / 2 + lap)]);
|
||||
add(name, size, [...path(L / 2 - lap, b), [b, top - 0.127, n]]);
|
||||
|
||||
// --- 상면 C1 · C4 (1단, 8 m 이음) · C3 · C2 · C3-1 · C2-1 (2단) (§5.2) -------------------------------
|
||||
const top = widthBars(B, c, settings.mainSpacing / 1000);
|
||||
const z1 = model.copingRefTop - c, z2 = z1 - settings.mainLayerGap / 1000;
|
||||
const h25 = hookLength('main90', d('H25')) / 1000;
|
||||
const ls25 = spliceLength(d('H25'), fy('H25'), material.fck) / 1000;
|
||||
const a = c, b = L - c;
|
||||
// Smallest n with every piece ≤ 8 m (hooks included), joints at the n-section points ∓ l_s (C1 −, C4 +).
|
||||
const pieces = (shift: number, hooks: boolean) => {
|
||||
for (let n = 1; n < 100; n++) {
|
||||
const cuts = Array.from({ length: n - 1 }, (_, j) => a + (b - a) * (j + 1) / n + shift);
|
||||
const ends = [a, ...cuts, b];
|
||||
const spans = ends.slice(1).map((e, i) => [i === 0 ? ends[0] : ends[i] - ls25 / 2, i === n - 1 ? e : e + ls25 / 2] as [number, number]);
|
||||
const lengths = spans.map(([p, q], i) => (q - p) + (hooks && i === 0 ? h25 : 0) + (hooks && i === n - 1 ? h25 : 0));
|
||||
if (lengths.every(l => l <= MULTI_REBAR.standardMm / 1000 + 1e-9)) return { spans, cuts };
|
||||
}
|
||||
throw new Error('상면 1단 이음 조각을 정할 수 없습니다.');
|
||||
};
|
||||
const c1 = pieces(-ls25, true), c4 = pieces(ls25, false);
|
||||
top.forEach((y, i) => {
|
||||
const cycle1 = i % 2 === 0;
|
||||
const plan = cycle1 ? c1 : c4;
|
||||
plan.spans.forEach(([p, q], k) => {
|
||||
const pts: CopingPoint[] = [[p, z1, y], [q, z1, y]];
|
||||
if (cycle1 && k === 0) pts.unshift([p, z1 - h25, y]);
|
||||
if (cycle1 && k === plan.spans.length - 1) pts.push([q, z1 - h25, y]);
|
||||
add(cycle1 ? 'C1' : 'C4', 'H25', pts);
|
||||
});
|
||||
// 2단: 끝단 쪽은 기둥 중심 + l_cut까지 (C3 끝 갈고리 / C2 직선), 가운데 기둥 위는 ± l_cut (C3-1 / C2-1).
|
||||
const xFirst = columns[0], xLast = columns.at(-1)!;
|
||||
const outerName = cycle1 ? 'C3' : 'C2', midName = cycle1 ? 'C3-1' : 'C2-1';
|
||||
add(outerName, 'H25', [...(cycle1 ? [[a, z2 - h25, y] as CopingPoint] : []), [a, z2, y], [xFirst + MULTI_REBAR.topCut, z2, y]]);
|
||||
add(outerName, 'H25', [[xLast - MULTI_REBAR.topCut, z2, y], [b, z2, y], ...(cycle1 ? [[b, z2 - h25, y] as CopingPoint] : [])]);
|
||||
for (const x of columns.slice(1, -1)) add(midName, 'H25', [[x - MULTI_REBAR.topCut, z2, y], [x + MULTI_REBAR.topCut, z2, y]]);
|
||||
});
|
||||
// RB-M05: 상면 1단 이음 중심이 기둥 구간 안 (정보).
|
||||
const zoneOf = (x: number) => columns.some((col, k) => {
|
||||
const n = columns.length;
|
||||
const [lo, hi] = k === 0 ? [col - MULTI_REBAR.zoneOuter, col + MULTI_REBAR.zoneInner] : k === n - 1 ? [col - MULTI_REBAR.zoneInner, col + MULTI_REBAR.zoneOuter] : [col - MULTI_REBAR.zoneMiddle, col + MULTI_REBAR.zoneMiddle];
|
||||
return x >= lo - 1e-9 && x <= hi + 1e-9;
|
||||
});
|
||||
const inZone = [...c1.cuts.map(x => ['C1', x] as const), ...c4.cuts.map(x => ['C4', x] as const)].filter(([, x]) => zoneOf(x));
|
||||
if (inZone.length) warnings.push({ code: 'RB-M05', message: `상면 1단 8 m 이음 중심 ${inZone.length}곳이 기둥 구간 안입니다 (${inZone.map(([nm, x]) => `${nm} ${x.toFixed(3)}`).join(' · ')}). 정보.` });
|
||||
|
||||
// --- 하면 C7 (전 길이, Level 꺾임점 겹침) · C8 (경간마다) (§5.3–5.4) ---------------------------------
|
||||
const bottomBars = widthBars(B, c, 2 * settings.mainSpacing / 1000);
|
||||
const h29 = hookLength('main90', d('H29')) / 1000;
|
||||
const ls29 = spliceLength(d('H29'), fy('H29'), material.fck) / 1000;
|
||||
const path = (from: number, to: number, y: number): CopingPoint[] =>
|
||||
[from, ...envelope.xs.filter(x => x > from + 1e-9 && x < to - 1e-9), to].map(x => [x, envelope.inset(x, c), y]);
|
||||
// The lap runs l_s/2 along the bar on each side of a Level kink (not l_s/2 horizontally, which is longer on a haunch).
|
||||
const walk = (x0: number, dist: number, dir: 1 | -1) => {
|
||||
const stops = [...envelope.xs.filter(x => dir > 0 ? x > x0 + 1e-9 : x < x0 - 1e-9).sort((p, q) => dir * (p - q)), dir > 0 ? b : a];
|
||||
let x = x0, left = dist;
|
||||
for (const next of stops) {
|
||||
const seg = Math.hypot(next - x, envelope.under(next) - envelope.under(x));
|
||||
if (seg >= left - 1e-12) return x + (next - x) * left / seg;
|
||||
left -= seg; x = next;
|
||||
}
|
||||
return x;
|
||||
};
|
||||
const kinks = columns.flatMap(x => [x - lw / 2, x + lw / 2]).slice(1, -1);
|
||||
const c7Ends = [a, ...kinks, b];
|
||||
for (const y of bottomBars) {
|
||||
for (let k = 1; k < c7Ends.length; k++) {
|
||||
const from = k === 1 ? a : walk(c7Ends[k - 1], ls29 / 2, -1), to = k === c7Ends.length - 1 ? b : walk(c7Ends[k], ls29 / 2, 1);
|
||||
const pts = path(from, to, y);
|
||||
if (k === 1) pts.unshift([a, pts[0][1] + h29, y]);
|
||||
if (k === c7Ends.length - 1) pts.push([b, pts.at(-1)![1] + h29, y]);
|
||||
add('C7', 'H29', pts);
|
||||
}
|
||||
}
|
||||
// Plan groups retain their functional IDs; the drawing also uses C13 for a shear loop.
|
||||
const columns = model.columns.map(c => c.s).sort((a, b) => a - b);
|
||||
const half = model.copingGeom.levelWidth / 2000;
|
||||
const kMax = Math.ceil(half * 1000 / settings.spacingL);
|
||||
const groups = columns.map(c => Array.from({ length: 2 * kMax + 1 }, (_, i) => c + (i - kMax) * settings.spacingL / 1000));
|
||||
const positions: { name: string; x: number }[] = groups.flatMap(g => g.map(x => ({ name: 'C10', x })));
|
||||
const gap = (settings.cover + settings.firstShearGap) / 1000;
|
||||
for (let x = groups[0][0] - settings.spacingS / 1000; x >= gap - 1e-9; x -= settings.spacingS / 1000) positions.push({ name: 'C9', x });
|
||||
for (let x = groups.at(-1)!.at(-1)! + settings.spacingS / 1000; x <= L - gap + 1e-9; x += settings.spacingS / 1000) positions.push({ name: 'C11', x });
|
||||
for (let i = 1; i < groups.length; i++) {
|
||||
const a = groups[i - 1].at(-1)!, b = groups[i][0], count = Math.floor((b - a) * 1000 / settings.spacingS);
|
||||
for (let j = 1; j < count; j++) positions.push({ name: 'C9·C11 사이', x: a + (b - a) * j / count });
|
||||
const c8y = bottomBars.slice(1).map((y, i) => (y + bottomBars[i]) / 2);
|
||||
for (let k = 1; k < columns.length; k++) {
|
||||
const from = columns[k - 1] + MULTI_REBAR.c8End, to = columns[k] - MULTI_REBAR.c8End;
|
||||
for (const y of c8y) add('C8', 'H25', [[from, levelBottom + hr + c, y], [to, levelBottom + hr + c, y]]);
|
||||
}
|
||||
const ds = NOMINAL_DIAMETER.H22 / 1000, yTop = top - settings.cover / 1000;
|
||||
const legs = settings.shearLegs, pitch = (B - 2 * settings.cover / 1000) / (legs - 1);
|
||||
for (const { name, x } of positions) for (let i = 0; i < legs / 2; i++) {
|
||||
const xu = x + i * 2 * ds, xc = xu + ds;
|
||||
const a = -B / 2 + settings.cover / 1000 + i * pitch, b = a + legs / 2 * pitch;
|
||||
const bottom = envelope.inset(xu, (settings.cover + 12) / 1000);
|
||||
const capBottom = envelope.inset(xc, (settings.cover + 112) / 1000);
|
||||
add(name, 'H22', [[xu, yTop, a], [xu, bottom, a], [xu, bottom, b], [xu, yTop, b]]);
|
||||
add(name, 'H22', [[xc, capBottom, a], [xc, yTop, a], [xc, yTop, b], [xc, capBottom, b]]);
|
||||
}
|
||||
const levels = sideRowLevels(model.levelBottom, model.endBottom, top);
|
||||
|
||||
// --- 측면 수평 C12 (상단, 전 길이) · C13 (하단 변화구간, 하면에서 끊김), 다리 위치마다 (§6) -----------------
|
||||
const zBreak = model.endBottom + c;
|
||||
const sideTop = z2 - (d('H25') + d('H16')) / 2000;
|
||||
const sideRows: { z: number; intervals: [number, number][] }[] = [];
|
||||
for (let z = levels.zBottom; z <= top - settings.sideEndGap / 1000 + 1e-9; z += settings.sideSpacing / 1000) {
|
||||
for (let z = levelBottom + c + settings.sideSpacing / 1000; z <= sideTop + 1e-9; z += settings.sideSpacing / 1000) {
|
||||
// Where the bar clears the underside by c (vertical): one interval per stretch between the walls of the 오목.
|
||||
const xsAll = [a, ...envelope.xs.filter(x => x > a && x < b), b];
|
||||
const intervals: [number, number][] = [];
|
||||
const points = [0.127, ...envelope.xs.filter(x => x > 0.127 && x < L - 0.127), L - 0.127];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
let a = points[i - 1], b = points[i];
|
||||
const za = envelope.inset(a, 0.127), zb = envelope.inset(b, 0.127);
|
||||
if (Math.min(za, zb) > z) continue;
|
||||
if (za > z) a += (b - a) * (za - z) / (za - zb);
|
||||
else if (zb > z) b = a + (b - a) * (z - za) / (zb - za);
|
||||
const previous = intervals.at(-1);
|
||||
if (previous && Math.abs(previous[1] - a) < 1e-8) previous[1] = b;
|
||||
else intervals.push([a, b]);
|
||||
for (let i = 1; i < xsAll.length; i++) {
|
||||
let p = xsAll[i - 1], q = xsAll[i];
|
||||
const zp = envelope.under(p) + c, zq = envelope.under(q) + c;
|
||||
if (Math.min(zp, zq) > z + 1e-9) continue;
|
||||
if (zp > z) p += (q - p) * (zp - z) / (zp - zq);
|
||||
else if (zq > z) q = p + (q - p) * (z - zp) / (zq - zp);
|
||||
const last = intervals.at(-1);
|
||||
if (last && Math.abs(last[1] - p) < 1e-8) last[1] = q; else intervals.push([p, q]);
|
||||
}
|
||||
sideRows.push({ z, intervals });
|
||||
const name = z < levels.zBreak - 1e-9 ? 'C13' : 'C12', face = B / 2 - 0.127;
|
||||
for (const [a, b] of intervals) {
|
||||
add(name, 'H16', [[a, z, -face], [b, z, -face]]);
|
||||
add(name, 'H16', [[a, z, face], [b, z, face]]);
|
||||
// Short isolated intervals must not grow hooks through the adjacent haunch.
|
||||
const leg = Math.min(SIDE_HOOK_LEG / 1000, (b - a) / 2), zh = z + NOMINAL_DIAMETER.H16 / 1000;
|
||||
for (const [x, direction] of [[a, 1], [b, -1]]) add(name, 'H16', [[x + direction * leg, zh, -face], [x, zh, -face], [x, zh, face], [x + direction * leg, zh, face]]);
|
||||
const name = z < zBreak - 1e-9 ? 'C13' : 'C12';
|
||||
for (const [p, q] of intervals) for (const y of legs.y) add(name, 'H16', [[p, z, y], [q, z, y]]);
|
||||
}
|
||||
|
||||
// --- C14 크로스타이 (§7): 연직 600, 교각축 1200, 단마다 600 엇갈림 ---------------------------------------
|
||||
const tie = hookLength('tie90', d('H16')) / 1000;
|
||||
const yEdge = B / 2 - c;
|
||||
const c14Rows: number[] = [];
|
||||
for (let z = levelBottom + c + MULTI_REBAR.c14Vertical; z < sideTop - 1e-9; z += MULTI_REBAR.c14Vertical) c14Rows.push(z);
|
||||
c14Rows.push(sideTop);
|
||||
c14Rows.forEach((z, r) => {
|
||||
for (let x = MULTI_REBAR.c14Along / 2 + (r % 2) * MULTI_REBAR.c14Stagger; x <= L - MULTI_REBAR.c14Along / 2 + 1e-9; x += MULTI_REBAR.c14Along) {
|
||||
if (z < envelope.under(x) + c + tie) continue;
|
||||
add('C14', 'H16', [[x, z - tie, -yEdge], [x, z, -yEdge], [x, z, yEdge], [x, z - tie, yEdge]]);
|
||||
}
|
||||
});
|
||||
|
||||
// --- C15 계단부 (§8): 끝단보다 h_16 이상 높은 계단 전체에 ⊓ 하나 ------------------------------------------
|
||||
const front = model.segments;
|
||||
const endTop = Math.min(front[0].top, front.at(-1)!.top);
|
||||
const high = front.filter(g => g.top - endTop >= MULTI_REBAR.stepMin - 1e-9);
|
||||
if (high.length) {
|
||||
const from = high[0].s0 + c, to = high.at(-1)!.s1 - c;
|
||||
const z15 = Math.min(...high.map(g => g.top)) - c;
|
||||
const ld = developmentLength(d('H19'), fy('H19'), material.fck) / 1000;
|
||||
top.filter((_, i) => i % 2 === 0).forEach(y => add('C15', 'H19', [[from, z15 - ld, y], [from, z15, y], [to, z15, y], [to, z15 - ld, y]]));
|
||||
const above = Math.max(...front.map(g => g.top)) - (z15 + c);
|
||||
if (above > MULTI_REBAR.stepMin + 1e-9) warnings.push({ code: 'RB-M04', message: `가장 높은 계단 상면이 C15보다 ${Math.round(above * 1000)} mm 높습니다 (h_16 ${MULTI_REBAR.stepMin * 1000} 초과). C16 병행은 미정 (Q-M7).` });
|
||||
}
|
||||
const hook = hookLength('main90', NOMINAL_DIAMETER.H16) / 1000;
|
||||
const face = B / 2 - C14_INSET / 1000;
|
||||
const addCross = (x: number, z: number) => {
|
||||
if (z < envelope.inset(x, 0.1)) return;
|
||||
const legsFit = z - hook >= envelope.inset(x, 0.1);
|
||||
add('C14', 'H16', legsFit ? [[x, z - hook, -face], [x, z, -face], [x, z, face], [x, z - hook, face]] : [[x, z, -face], [x, z, face]]);
|
||||
};
|
||||
for (const k of [1, 2]) for (let x = L / 2 % (settings.c14UpperSpacing / 1000); x <= L - 0.6; x += settings.c14UpperSpacing / 1000) if (x >= 0.6) addCross(x, top - 0.127 - k * settings.mainLayerGap / 1000 + 0.024);
|
||||
for (const [i, row] of sideRows.entries()) if (i % 2 === 0) for (let x = 0.6; x <= L - 0.6; x += settings.c14LowerSpacing / 1000) if (row.intervals.some(([a, b]) => x >= a && x <= b)) addCross(x, row.z + 0.02);
|
||||
const stairs = stairBars(L * 1000, B * 1000, top, material, 2 * settings.mainSpacing);
|
||||
for (const [name, list] of [['C15', stairs.c15], ['C16', stairs.c16]] as const) for (const s of list) {
|
||||
const a = s.x0 / 1000, b = s.x1 / 1000, n = s.n / 1000, h = s.leg / 1000;
|
||||
add(name, 'H19', [[a, s.z - h, n], [a, s.z, n], [b, s.z, n], ...(s.legs === 'both' ? [[b, s.z - h, n] as CopingPoint] : [])]);
|
||||
}
|
||||
return bars;
|
||||
|
||||
// RB-M03: 상면 1단 외 철근이 12 m를 넘으면 이음 미정 (Q-M12).
|
||||
const long = [...new Set(bars.filter(x => !['C1', 'C4'].includes(x.name) && x.length > 12000).map(x => x.name))];
|
||||
if (long.length) warnings.push({ code: 'RB-M03', message: `${long.join(' · ')} 길이가 12000을 넘습니다. 상면 1단 외 철근의 8 m 이음 적용은 미정입니다 (Q-M12).` });
|
||||
return { bars, warnings };
|
||||
}
|
||||
|
||||
/** Shared physical pieces for multi-column 3D and quantities (3주형형상배근.md §4–§8). */
|
||||
export function multicolCopingBars(model: PierModel, material: Material = DEFAULT_MATERIAL): CopingBar[] {
|
||||
return multicolCopingInfo(model, material).bars;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14L
|
||||
import { sideRowLevels } from '../coping-geom';
|
||||
import { CAST_PENDING } from './cast-in-place';
|
||||
import { castRebarLayout } from './cast-layout';
|
||||
import { multicolCopingBars } from './multicol-coping';
|
||||
import { multicolCopingInfo } from './multicol-coping';
|
||||
import { columnRebar, columnInputFor } from './column';
|
||||
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
|
||||
import { DEFAULT_MATERIAL, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, splitPieces, STANDARD_BAR } from './common';
|
||||
@@ -48,6 +48,17 @@ export interface RebarWarning {
|
||||
bar?: string;
|
||||
}
|
||||
|
||||
/** 다주형 코핑 번호별 위치 (3주형형상배근.md §3, 번호 = 역할). */
|
||||
const MULTI_NOTE: Record<string, string> = {
|
||||
C1: '상면 1단 cycle-1, 양끝 갈고리, 8 m 이음', C4: '상면 1단 cycle-2, 직선, 8 m 이음 (± l_s 엇갈림)',
|
||||
C3: '상면 2단 cycle-1, 끝단 쪽 (기둥 중심 + 4450까지)', C2: '상면 2단 cycle-2, 끝단 쪽',
|
||||
'C3-1': '상면 2단 cycle-1, 가운데 기둥 위 ± 4450', 'C2-1': '상면 2단 cycle-2, 가운데 기둥 위',
|
||||
C7: '하면 전 길이, Level 꺾임점 겹침, 양끝 위로 갈고리', C8: '경간마다 오목 하면, 기둥 중심 ± 2230 안쪽',
|
||||
C9: '전단 · 테이퍼·헌치', C10: '전단 · 기둥 Level', C11: '전단 · 기둥 사이 오목',
|
||||
C12: '측면 상단 (전 길이), 다리 위치마다', C13: '측면 하단 변화구간 (하면에서 끊김), 다리 위치마다',
|
||||
C14: '크로스타이 연직 600 · 교각축 1200 엇갈림', C15: '계단부 ⊓ (끝단보다 100 이상 높은 계단 전체)',
|
||||
};
|
||||
|
||||
/** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */
|
||||
const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>> = {
|
||||
S1: { spacing: '125', actual: '125', position: '블럭 상면 − 69.1', direction: '교각축 직각', shape: '양끝 다리 꺾임', basis: 'calc' },
|
||||
@@ -381,17 +392,18 @@ export function rebarSummary(
|
||||
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
|
||||
}
|
||||
if (nCol > 1 && model.kink === null) {
|
||||
const grouped = new Map<string, ReturnType<typeof multicolCopingBars>>();
|
||||
for (const b of multicolCopingBars(model, mCoping)) {
|
||||
const info = multicolCopingInfo(model, mCoping);
|
||||
const grouped = new Map<string, typeof info.bars>();
|
||||
for (const b of info.bars) {
|
||||
const group = grouped.get(b.name) ?? [];
|
||||
group.push(b); grouped.set(b.name, group);
|
||||
}
|
||||
for (const [name, bars] of grouped) rows.push({ member: 'coping', name, size: bars[0].size,
|
||||
count: bars.length, length: null, pieces: bars.map(b => ({ length: b.length, count: 1 })),
|
||||
note: '다주형 전용 삽도 규격 · 실제 콘크리트 하면에 따른 조각별 중심선 길이. 번호·cycle별 이음 상세 확인 필요' });
|
||||
warnings.push({ code: 'RB-M02', target: 'coping', message: '다주형 전용 삽도 우선: C1~4 H25·C7 H29·C8 H25·전단 H22·수평/C14 H16. C5/C6은 전용 삽도에 없어 제외했습니다. C11/C13 중복 번호·cycle별 이음 위치·분리 수평철근 갈고리는 상세 확인 필요. 해당 수량은 검토용입니다.' });
|
||||
warnings.push({ code: 'RB-M01', target: 'coping', message: '다주형 C14는 전용 삽도의 H16·기본600 간격과 실제 하면을 사용합니다. 기둥별 반복·번호 대응 상세는 확인이 필요합니다.' });
|
||||
warnings.push({ code: 'RB-M03', target: 'column', message: '다주형 단부는 삽도의 지름 이상·코핑 반높이 연장을 적용했습니다. P4/P5 번호는 기존 역할(단부 P5·기초내 P4)을 유지하며 도면 번호 대응 확인이 필요합니다.' });
|
||||
note: `다주형 (3주형형상배근.md) · ${MULTI_NOTE[name] ?? ''} · 조각별 중심선 길이` });
|
||||
for (const w of info.warnings) warnings.push({ code: w.code, target: 'coping', message: w.message });
|
||||
warnings.push({ code: 'RB-M06', target: 'coping', message: '다주형 전단 다리와 코핑 안 기둥 주철근의 간섭은 검토하지 않았습니다 (T형 Q-K3과 같음). 밀집 구간·다리 8개·측면 형상·피복 120은 가정값입니다 (Q-M2·Q-M3·Q-M5·Q-M6).' });
|
||||
warnings.push({ code: 'RB-M08', target: 'column', message: '다주형 기둥 단부는 지름 이상·코핑 반높이 연장을 적용했습니다 (그림 D, Pier A337 1.5D — Q-M8 미결).' });
|
||||
}
|
||||
// Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). The spacing rows take their values from the inputs (승인 2026-10-26).
|
||||
const withMeta = rows.map((r) => {
|
||||
|
||||
@@ -67,6 +67,9 @@ export interface PierInput {
|
||||
solplate?: SolplateRule;
|
||||
/** 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유, 받침별 A × B, 열 T·받침 S 직접 수정. Absent = 기본값. */
|
||||
sole?: SoleSettings;
|
||||
/** 다주형 기둥 위치 입력 (Q-M10 미결): 끝 ~ 기둥1 중심 e_c, 기둥 간격 s_c (mm). Absent = L/5 규칙. */
|
||||
columnEndOffsetMm?: number;
|
||||
columnSpacingMm?: number;
|
||||
/** 교각 종단경사 (소수, (−) = 교량진행방향으로 낮아짐; Pier 시트 "종단경사" 열, Q-SH2). 짝 받침이 없을 때 S에 쓴다. */
|
||||
longitudinalSlope?: number;
|
||||
/** 지반·지층 (승인 2026-10-26): 위치, 지하수위 고려와 EL, 지층별 행 (위에서 아래, 행의 EL = 그 층의 하단 EL).
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
// Coping (straight coping only; a bent coping is not drawn in 3D yet).
|
||||
if (straight && m.columns.length > 1) {
|
||||
for (const b of multicolCopingBars(m, mCoping)) {
|
||||
if (/^C[1-8]$/.test(b.name)) addPath(b.name, 0, b.pts, b.diameterMm, COLOR.coping);
|
||||
if (/^C[1-8](-\d+)?$/.test(b.name)) addPath(b.name, 0, b.pts, b.diameterMm, COLOR.coping);
|
||||
else for (let i = 1; i < b.pts.length; i++) addBar(b.name, 0, b.pts[i - 1], b.pts[i], b.diameterMm, COLOR.shear);
|
||||
}
|
||||
}
|
||||
|
||||
+98
-29
@@ -1,47 +1,116 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { shearBarsMulti } from '../src/core/rebar/coping';
|
||||
import { multicolCopingInfo, multicolShearLayout } from '../src/core/rebar/multicol-coping';
|
||||
import { rebarSummary } from '../src/core/rebar/summary';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
// 2주형 on the P1 sheet with a 12 m coping (columns at L/5, 4L/5). 3주형 needs 20 m (columns 4 / 10 / 16): with 12 m the
|
||||
// columns are 3600 apart and leave no flat 오목 part between the 1500 haunches (RB-M01).
|
||||
const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000 });
|
||||
const three = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 20000 });
|
||||
const count = (rows: ReturnType<typeof rebarSummary>['rows'], name: string) => rows.find((r) => r.name === name)!.count;
|
||||
// RAG/3주형형상배근.md §11 (2026-11-06): 2주형 DWG (L 24200) and the 3주형 합성 예제 (L 37200), B 3500, D 3000,
|
||||
// h1 = h2 = 1500, 기둥 끝 여유 5600 · 간격 13000 (Q-M10 input), 피복 120, 다리 8개.
|
||||
const dwg = (type: '2주형' | '3주형', L: number, extra = {}) => buildPier({ ...loadP1(), type, columnCount: type === '2주형' ? 2 : 3,
|
||||
copingLengthMm: L, copingWidthMm: 3500, columnDiameterMm: 3000, copingH1Mm: 1500, copingH2Mm: 1500, columnEndOffsetMm: 5600, ...extra });
|
||||
const two = dwg('2주형', 24200), three = dwg('3주형', 37200);
|
||||
const info2 = multicolCopingInfo(two), info3 = multicolCopingInfo(three);
|
||||
const named = (info: typeof info2, name: string) => info.bars.filter((b) => b.name === name);
|
||||
/** Lengths of the pieces of one bar line (the first y of that name), in order along x. */
|
||||
const line = (info: typeof info2, name: string) => {
|
||||
const all = named(info, name);
|
||||
const y = all[0].pts[1][2];
|
||||
return all.filter((b) => b.pts.some((p) => Math.abs(p[2] - y) < 1e-9)).sort((a, b) => a.pts[0][0] - b.pts[0][0]).map((b) => b.length);
|
||||
};
|
||||
const near = (got: number[], want: number[], tol: number) => {
|
||||
expect(got).toHaveLength(want.length);
|
||||
got.forEach((g, i) => expect(Math.abs(g - want[i]), `#${i}: ${g} vs ${want[i]}`).toBeLessThanOrEqual(tol));
|
||||
};
|
||||
|
||||
describe('다주형 철근 (2주형·3주형, 코핑철근.md §9)', () => {
|
||||
it('2주형: H22 @300, six physical pieces per position', () => {
|
||||
const rows = rebarSummary(two).rows;
|
||||
expect(count(rows, 'C10')).toBe(18 * 6);
|
||||
expect(count(rows, 'C9')).toBe(3 * 6);
|
||||
expect(count(rows, 'C11')).toBe(3 * 6);
|
||||
expect(count(rows, 'C9·C11 사이')).toBe(15 * 6);
|
||||
describe('다주형 코핑 배근 (3주형형상배근.md §11 재현)', () => {
|
||||
it('column positions from e_c 5600 / s_c 13000 (Q-M10 input) and the multi-column defaults (피복 120, 다리 8)', () => {
|
||||
expect(two.columns.map((c) => c.s)).toEqual([5.6, 18.6]);
|
||||
expect(three.columns.map((c) => c.s).map((x) => Math.round(x * 1000) / 1000)).toEqual([5.6, 18.6, 31.6]);
|
||||
expect(three.copingRebar).toMatchObject({ cover: 120, shearLegs: 8, spacingS: 300, sideSpacing: 200 });
|
||||
});
|
||||
|
||||
it('3주형: C10 27 positions and two gaps of eleven, each with six physical pieces', () => {
|
||||
const rows = rebarSummary(three).rows;
|
||||
// Per column 9 positions (±4 @300); between groups 8.8 − 5.2 = 3.6 m → floor(3600 / 300) = 12 bays → 11 positions.
|
||||
expect(count(rows, 'C10')).toBe(27 * 6);
|
||||
expect(count(rows, 'C9·C11 사이')).toBe(22 * 6);
|
||||
it('2주형 shear: 112 positions, 120 · 130 · 7@300 · 14@150 · 8@300 · 16@150 · 19@300 · 16@150 · 8@300 · 14@150 · 7@300 · 130 · 120', () => {
|
||||
const { positions } = multicolShearLayout(24.2, [5.6, 18.6], 0.12, 0.3, 0.15);
|
||||
expect(positions).toHaveLength(112);
|
||||
expect(positions[0]).toBeCloseTo(0.12, 9);
|
||||
const gaps = positions.slice(1).map((x, i) => Math.round((x - positions[i]) * 10000) / 10);
|
||||
const want = [130, ...Array(7).fill(300), ...Array(14).fill(150), ...Array(8).fill(300), ...Array(16).fill(150), ...Array(19).fill(300),
|
||||
...Array(16).fill(150), ...Array(8).fill(300), ...Array(14).fill(150), ...Array(7).fill(300), 130];
|
||||
expect(gaps).toEqual(want);
|
||||
});
|
||||
|
||||
it('column bars repeat per column: P1 230 × 2 and 230 × 3', () => {
|
||||
expect(count(rebarSummary(two).rows, 'P1')).toBe(460);
|
||||
expect(count(rebarSummary(three).rows, 'P1')).toBe(690);
|
||||
it('3주형 shear: 173 positions, spans 20@287.5, C9 78 · C10 41 · C11 54 (8 pieces per position: 4 closures × U + 삿갓)', () => {
|
||||
const { positions } = multicolShearLayout(37.2, [5.6, 18.6, 31.6], 0.12, 0.3, 0.15);
|
||||
expect(positions).toHaveLength(173);
|
||||
const gaps = positions.slice(1).map((x, i) => Math.round((x - positions[i]) * 10000) / 10);
|
||||
expect(gaps.filter((g) => g === 287.5)).toHaveLength(40);
|
||||
expect([named(info3, 'C9').length, named(info3, 'C10').length, named(info3, 'C11').length].map((n) => n / 8)).toEqual([78, 41, 54]);
|
||||
});
|
||||
|
||||
it('a multi-column pier gets the RB-M01 note about C14', () => {
|
||||
expect(rebarSummary(two).warnings.map((w) => w.code)).toContain('RB-M01');
|
||||
it('3주형 상면 1단: C1 7 pieces 4911.5 · 6805 × 5 · 7961.5, C4 7567.5 · 6805 × 5 · 4517.5 (8 m 이음), 14 / 13 bars', () => {
|
||||
near(line(info3, 'C1'), [4911.5, 6805, 6805, 6805, 6805, 6805, 7961.5], 1);
|
||||
near(line(info3, 'C4'), [7567.5, 6805, 6805, 6805, 6805, 6805, 4517.5], 1);
|
||||
expect(named(info3, 'C1')).toHaveLength(98);
|
||||
expect(named(info3, 'C4')).toHaveLength(91);
|
||||
});
|
||||
|
||||
it('the single column (T형) has no between row and no RB-M01', () => {
|
||||
const t = rebarSummary(buildPier(loadP1()));
|
||||
expect(t.rows.find((r) => r.name === 'C9·C11 사이')).toBeUndefined();
|
||||
expect(t.warnings.map((w) => w.code)).not.toContain('RB-M01');
|
||||
it('2주형 상면 1단: C1 5 pieces 4423.5 · 6317 × 3 · 7473.5', () => {
|
||||
near(line(info2, 'C1'), [4423.5, 6317, 6317, 6317, 7473.5], 1);
|
||||
});
|
||||
|
||||
it('shear bars for a columns too close to share a Level are refused', () => {
|
||||
expect(() => shearBarsMulti(12000, [3000, 3500])).toThrow('겹칩니다');
|
||||
it('상면 2단: C3 10324 × 2 · C2 9930 × 2 per bar, 가운데 기둥 위 C3-1 / C2-1 8900', () => {
|
||||
near(line(info3, 'C3'), [10324, 10324], 1);
|
||||
near(line(info3, 'C2'), [9930, 9930], 1);
|
||||
near(line(info3, 'C3-1'), [8900], 1);
|
||||
expect(named(info3, 'C3-1')).toHaveLength(14);
|
||||
expect(named(info3, 'C2-1')).toHaveLength(13);
|
||||
expect(named(info2, 'C3-1')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('RB-M05: 8 m 이음 중심 in a column zone — 3주형 C4 17.485 · C1 19.715, 2주형 C4 6.437 · C1 17.763', () => {
|
||||
const m3 = info3.warnings.find((w) => w.code === 'RB-M05')!.message;
|
||||
expect(m3).toContain('C4 17.485');
|
||||
expect(m3).toContain('C1 19.715');
|
||||
const m2 = info2.warnings.find((w) => w.code === 'RB-M05')!.message;
|
||||
expect(m2).toContain('C4 6.437');
|
||||
expect(m2).toContain('C1 17.763');
|
||||
});
|
||||
|
||||
it('하면 C7: 3주형 5 pieces 8690.5 / 11679 / 4917 / 11679 / 8690.5, 2주형 3 pieces (lap l_s/2 along the bar at the Level kinks)', () => {
|
||||
// The outer piece follows the taper from the cover line: 7344 + 858.5 + 472 = 8674.5 (DWG 실측 경로 7360 → 8690.5).
|
||||
near(line(info3, 'C7'), [8690.5, 11679, 4917, 11679, 8690.5], 20);
|
||||
near(line(info2, 'C7'), [8690.5, 11679, 8690.5], 20);
|
||||
expect(named(info3, 'C7')).toHaveLength(14 * 5);
|
||||
});
|
||||
|
||||
it('하면 C8: 8540 per span (기둥 중심 ± 2230 안쪽), 13 × 2 = 26 for 3주형', () => {
|
||||
expect(named(info3, 'C8')).toHaveLength(26);
|
||||
expect(named(info3, 'C8').every((b) => Math.abs(b.length - 8540) < 1e-6)).toBe(true);
|
||||
});
|
||||
|
||||
it('측면: C12 6 rows (z 1720 … 2720 above Level) at the 8 leg lines, 36960; C13 z 320 → 4330 / 4400 / 4330 (cut at each column)', () => {
|
||||
const c12 = named(info3, 'C12');
|
||||
expect([...new Set(c12.map((b) => Math.round((b.pts[0][1] - three.levelBottom) * 1000)))]).toEqual([1720, 1920, 2120, 2320, 2520, 2720]);
|
||||
expect(c12).toHaveLength(6 * 8);
|
||||
expect(c12.every((b) => Math.abs(b.length - 36960) < 1e-6)).toBe(true);
|
||||
const low = named(info3, 'C13').filter((b) => Math.abs(b.pts[0][1] - three.levelBottom - 0.32) < 1e-9).map((b) => Math.round(b.length));
|
||||
expect([...new Set(low)].sort()).toEqual([4333, 4400]);
|
||||
expect(low).toHaveLength(3 * 8);
|
||||
});
|
||||
|
||||
it('RB-M01: s_c 7000 — the dense zones of neighbouring columns overlap (warning; positions merged)', () => {
|
||||
const short = multicolCopingInfo(dwg('3주형', 37200, { columnSpacingMm: 7000 }));
|
||||
expect(short.warnings.map((w) => w.code)).toContain('RB-M01');
|
||||
expect(info3.warnings.map((w) => w.code)).not.toContain('RB-M01');
|
||||
});
|
||||
|
||||
it('철근표 rows reuse the pieces; the multi-column pier gets RB-M06 / RB-M08 notes; T형 none', () => {
|
||||
const summary = rebarSummary(three);
|
||||
for (const name of ['C1', 'C4', 'C3', 'C2', 'C3-1', 'C2-1', 'C7', 'C8', 'C9', 'C10', 'C11', 'C12', 'C13', 'C14']) {
|
||||
const row = summary.rows.find((r) => r.name === name)!;
|
||||
expect(row.count, name).toBe(named(info3, name).length);
|
||||
}
|
||||
expect(summary.warnings.map((w) => w.code)).toEqual(expect.arrayContaining(['RB-M05', 'RB-M06', 'RB-M08']));
|
||||
expect(rebarSummary(buildPier(loadP1())).warnings.map((w) => w.code)).not.toContain('RB-M06');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user