feat(shape): 꺾임점에서 겹치는 기초를 하나의 꺾인 기초로 합친다

꺾임을 넘는 기초를 합치지 않아, 기둥 간격이 기초 길이보다 짧은 꺾인 교각(산외교 P11: 2.75 m)에서 두 기초가
겹치고(콘크리트 쐐기 틈, 철근 겹침, 수량 이중 계산) 말뚝도 겹쳤다. 꺾임 전후 기초가 겹치거나 미터 퍼짐 안에서
맞닿으면 하나의 꺾인 기초로 본다:
- 형상: 구간별 조각 둘(bent), 각 조각을 미터 퍼짐 + 10 mm 너머로 늘려 미터면에서 틈 없이 잇는다
- 말뚝: 편 길이에 한 번 배치하고 꺾임점 기준으로 구간을 나눈다
- 철근: 편 좌표의 한 기초(footing-units.ts)로 철근표와 3D가 같이 계산, 3D는 미터점에서 접는다

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-10 17:46:47 +09:00
1 parent ed84761449
commit ac646d554c
6 files changed
+142 -46

No files matched your search

+10 -5
View File
@@ -134,12 +134,17 @@ describe('bent coping model (stage 2)', () => {
expect(buildPier(bentInput('직접기초')).piles).toHaveLength(0);
});
it('2주형 bent with columns on both sides of the kink: separate footings, one per stretch', () => {
it('2주형 bent with columns on both sides of the kink: the overlapping footings become one bent footing (2026-11-10)', () => {
const m = buildPier(bentInput('말뚝기초', '2주형', 10));
expect(m.footings.map((f) => f.k)).toEqual([0, 1]);
expect(m.footings.every((f) => Math.abs(f.length - 10.4) < 1e-9)).toBe(true);
expect(m.piles).toHaveLength(40);
expect(m.piles.filter((p) => p.k === 1)).toHaveLength(20);
// The two 10.4 m footings overlap across the kink, so they are one bent footing, a part per stretch.
expect(m.footings.map((f) => [f.k, f.bent?.part])).toEqual([[0, 0], [1, 1]]);
// Piles are laid once over the unfolded footing: both stretches get some, and none overlap (they used to: 2 × 20).
expect(m.piles.some((p) => p.k === 0) && m.piles.some((p) => p.k === 1)).toBe(true);
const kink = m.stretches[1].base;
const g = m.piles.map((p) => ({ g: p.k === 0 ? p.s : p.s + kink, n: p.n }));
for (let i = 0; i < g.length; i++) for (let j = i + 1; j < g.length; j++) {
expect(Math.hypot(g[i].g - g[j].g, g[i].n - g[j].n)).toBeGreaterThan(1.0);
}
const scene = buildPierScene(m);
const footings = scene.parts.footings.children as THREE.Mesh[];
expect(footings).toHaveLength(2);
+38 -25
View File
@@ -10,6 +10,7 @@ import { multicolCopingInfo } from '../src/core/rebar/multicol-coping';
import { copingFold, inWedge, isTransverse } from '../src/core/rebar/coping-fold';
import { buildRebar3D } from '../src/view/rebar3d';
import { shearB9 } from '../src/core/rebar/footing';
import { footingUnits } from '../src/core/rebar/footing-units';
import { loadP1 } from './helpers';
// 산외교 P11 (샘플01) with the coping bent 8.767° at IP4 (2026-11-10 검토): back ⑥ g 11.64, ⑦ g 18.49, kink g 15.14.
@@ -210,35 +211,47 @@ describe('꺾인 코핑 3D 철근 (E, 2026-11-10)', () => {
}, 60000);
});
describe('꺾인 교각의 확대기초 철근 (2026-11-10)', () => {
// 3주형 as in the project (산외교 P11): columns ① ② share one 18.05 m footing in stretch 0, ③ has a 10.4 m footing in stretch 1.
describe('꺾인 기초 (2026-11-10)', () => {
// 3주형 as in the project (산외교 P11): footings ① ② (−0.10 … 17.95) and ③ (15.20 … 25.60) overlap 2.75 m across the kink.
const m = buildPier({ ...p11(), type: '3주형', columnCount: 3 });
const kink = m.stretches[1].base;
const spread = (m.footings[0].width / 2) * Math.tan((8.767 * Math.PI) / 360);
it('each footing draws its rebar in its own stretch', () => {
const rb = buildRebar3D(m);
const footingNames = /^B\d/;
const ks = new Set(rb.records.filter((r) => footingNames.test(r.name)).map((r) => r.k));
expect(m.footings.map((f) => f.k)).toEqual([0, 1]);
expect(ks).toEqual(new Set([0, 1]));
}, 60000);
it('B9 leaves out only the column zones of the same stretch', () => {
const rows = rebarSummary(m).rows;
const expected = m.footings.reduce((sum, f) => {
const cols = m.columns.filter((c) => c.k === f.k && Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000);
return sum + shearB9(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, cols, 2000, m.footingRebar).count;
}, 0);
expect(rows.find((r) => r.name === 'B9')!.count).toBe(expected);
it('the two footings become one bent footing: each part reaches past the mitre spread, one unfolded extent', () => {
expect(m.footings.map((f) => [f.k, f.bent?.part])).toEqual([[0, 0], [1, 1]]);
const [p0, p1] = m.footings;
expect(p0.bent!.gFrom).toBeCloseTo(-0.1, 9);
expect(p0.bent!.gTo).toBeCloseTo(kink + m.columns[2].s + 5.2, 9);
expect(p0.s + p0.length / 2).toBeGreaterThanOrEqual(kink + spread);
expect(p1.s - p1.length / 2).toBeLessThanOrEqual(-spread);
});
it('footings of different lengths keep each length (no longest-for-all), spliced bars count both pieces', () => {
const rows = rebarSummary(m).rows.filter((r) => r.member === 'footing');
expect(m.footings.map((f) => f.length)).toEqual([18.05, 10.4]);
const b5 = rows.find((r) => r.name === 'B5')!;
expect(b5.pieces!.length).toBe(2);
const total = b5.pieces!.reduce((s, p) => s + p.count, 0);
// The 18.05 m footing's bars are two pieces each; the 10.4 m footing's one piece.
expect(total).toBeGreaterThan(b5.count);
it('rebar is one footing of the unfolded length with all three columns (no double count of the overlap)', () => {
const units = footingUnits(m);
expect(units).toHaveLength(1);
expect(units[0].length).toBeCloseTo(m.footings[0].bent!.gTo - m.footings[0].bent!.gFrom, 9);
expect(units[0].columnX).toHaveLength(3);
const rows = rebarSummary(m).rows;
const u = units[0];
expect(rows.find((r) => r.name === 'B9')!.count).toBe(shearB9(u.length * 1000, u.width * 1000, (u.top - u.bottom) * 1000, u.columnX, 2000, m.footingRebar).count);
});
it('3D draws the bent footing in both stretches, bars along the footing turning at the mitre', () => {
const rb = buildRebar3D(m);
const footing = rb.records.filter((r) => /^B\d/.test(r.name));
expect(new Set(footing.map((r) => r.k))).toEqual(new Set([0, 1]));
// Every footing bar point stays within the unfolded footing (no part drawn past its own stretch end).
for (const r of footing) for (const p of r.pts) {
const g = r.k === 0 ? p[0] : p[0] + kink;
expect(g).toBeGreaterThan(-0.1 - spread - 1e-6);
expect(g).toBeLessThan(m.footings[0].bent!.gTo + spread + 1e-6);
}
}, 60000);
it('footings apart across the kink stay separate (2주형 sample: 10.4 m footings 4.9 m apart)', () => {
const two = buildPier(p11());
expect(two.footings.every((f) => f.bent === undefined)).toBe(true);
expect(footingUnits(two)).toHaveLength(2);
});
});