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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user