feat(core,view): bent coping stage 3 — footings and piles in stretch frames

- Footings, blindings and piles on a bent axis are placed in their stretch's
  frame; 일체기초 is grouped per stretch, so a combined footing never crosses
  the kink (separate footings, each aligned with its stretch).
- Footings, blindings and piles are trimmed at the mitre plane like the coping.
- The stage-2 refusal for bent foundations is removed; the Level-half-width
  refusal for columns at the kink remains.
- Tests: T형 footing crossing the kink stays in stretch 0; 2주형 with columns
  on both sides gives one footing per stretch, with mitre clipping in the scene.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:38:01 +09:00
1 parent c9dae4ff9b
commit 9e1e25f8fe
5 files changed
+56 -23

No files matched your search

+17 -9
View File
@@ -77,6 +77,7 @@ export interface ColumnModel {
export interface FootingModel {
s: number;
k: number;
top: number;
bottom: number;
length: number;
@@ -85,6 +86,7 @@ export interface FootingModel {
export interface BlindingModel {
s: number;
k: number;
top: number;
length: number;
width: number;
@@ -178,7 +180,7 @@ export function solveIp4(ip1: Vec2, ip2: Vec2, ip3: Vec2, angleDeg: number): Vec
* Builds the first-stage model of a T-type pier from one input record. Pure function.
* Coordinates: g = arc length from the coping start (BP1), so g = 0 at BP1 and g = L at BP2.
* Each object carries its stretch k and a local s measured from that stretch's base (k = 0: BP1, k = 1: IP4).
* A bent coping (IP4 angle ≠ 0) is supported for 현장타설말뚝 only; footings and piles on a bent axis are stage 3.
* A bent coping (IP4 angle ≠ 0) puts each footing in its stretch's frame; footings that would cross the kink are not combined.
*/
export function buildPier(input: PierInput): PierModel {
const ip = (name: string) => {
@@ -191,9 +193,6 @@ export function buildPier(input: PierInput): PierModel {
const ip3 = ip('IP3');
const ip4 = ip('IP4');
const bent = ip4.angleDeg !== 0;
if (bent && input.foundationType !== '현장타설말뚝') {
throw new Error('미실시: 꺾인 코핑의 확대기초·말뚝은 3단계에서 구현합니다. 현재는 현장타설말뚝만 지원합니다.');
}
const L = input.copingLengthMm / 1000;
const B = input.copingWidthMm / 1000;
@@ -325,6 +324,14 @@ export function buildPier(input: PierInput): PierModel {
}
}
// Footings are grouped per stretch so that a 일체기초 never crosses the kink; each group is aligned with its stretch.
const footingSets: { s: number; k: number; length: number }[] = [];
for (let k = 0; k < stretchCount; k++) {
for (const g of footingGroups(colG.filter((_, i) => colK[i] === k))) {
footingSets.push({ s: g.s - base[k], k, length: g.length });
}
}
const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH;
const columns: ColumnModel[] = colG.map((g, i) => ({
s: g - base[colK[i]],
@@ -345,17 +352,18 @@ export function buildPier(input: PierInput): PierModel {
piles.push({ s: c.s, k: c.k, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter });
});
} else {
for (const g of footingGroups(colG)) {
footings.push({ s: g.s, top: footingTop, bottom: input.footingBottomEl, length: g.length, width: DIM.footingW });
for (const f of footingSets) {
footings.push({ s: f.s, k: f.k, top: footingTop, bottom: input.footingBottomEl, length: f.length, width: DIM.footingW });
blindings.push({
s: g.s,
s: f.s,
k: f.k,
top: input.footingBottomEl,
length: g.length + DIM.blindingMargin,
length: f.length + DIM.blindingMargin,
width: DIM.footingW + DIM.blindingMargin,
thickness: DIM.blindingT,
});
if (input.foundationType === '말뚝기초') {
piles.push(...pilesFor(g.s, input.footingBottomEl, pileLength, g.length));
piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, f.length).map((p) => ({ ...p, k: f.k })));
}
}
}
-1
View File
@@ -6,7 +6,6 @@ const EXPECTED_COLUMNS: Record<PierType, number> = { 'T형': 1, '2주형': 2, '3
/** Rules from spec §10 that are not implemented in this stage. Shown to the user, never silently skipped. */
export const NOT_IMPLEMENTED_RULES = [
'E04 fck 미입력 (배근 단계, 3단계)',
'꺾인 코핑의 확대기초·말뚝 (3단계; 현장타설말뚝만 지원)',
'W06 속성 복사 건너뜀 (2단계)',
'W08 솔플레이트 T1−T2 검사 (2단계)',
] as const;
+9 -3
View File
@@ -44,6 +44,12 @@ export function stretchMatrix(m: PierModel, k: number): THREE.Matrix4 {
* so each stretch's full-length slab is trimmed to its own half and the two meet at the mitred joint.
* Three.js keeps fragments with a non-negative distance from the plane, hence the sign flip for stretch 0.
*/
/** Mitre plane for one stretch as a list, or undefined on a straight axis. */
export function planesOf(m: PierModel, k: number): THREE.Plane[] | undefined {
const plane = mitrePlane(m, k);
return plane ? [plane] : undefined;
}
/** Straight-frame matrix (stretch 0). */
export function frameMatrix(m: PierModel): THREE.Matrix4 {
return stretchMatrix(m, 0);
@@ -102,19 +108,19 @@ export function buildPierScene(m: PierModel): PierScene {
for (const f of m.footings) {
const g = new THREE.BoxGeometry(f.length, f.top - f.bottom, f.width);
g.translate(f.s, (f.top + f.bottom) / 2, 0);
add('footings', g);
add('footings', g, 1, f.k, planesOf(m, f.k));
}
for (const b of m.blindings) {
const g = new THREE.BoxGeometry(b.length, b.thickness, b.width);
g.translate(b.s, b.top - b.thickness / 2, 0);
add('blinding', g);
add('blinding', g, 1, b.k, planesOf(m, b.k));
}
for (const p of m.piles) {
const g = new THREE.CylinderGeometry(p.diameter / 2, p.diameter / 2, p.top - p.bottom, 20);
g.translate(p.s, (p.top + p.bottom) / 2, p.n);
add('piles', g, 1, p.k);
add('piles', g, 1, p.k, planesOf(m, p.k));
}
for (const b of m.blocks) {
+28 -8
View File
@@ -17,10 +17,10 @@ const FRONT_N = -0.7;
const BACK_N = 0.6;
const GS = [0.6, 1.9, 3.2];
function bentInput(foundationType: PierInput['foundationType'] = '현장타설말뚝'): PierInput {
function bentInput(foundationType: PierInput['foundationType'] = '현장타설말뚝', type: PierInput['type'] = 'T형', coping = L): PierInput {
const base = loadP1();
const axis = buildAxis(IP1, IP3, solveIp4(IP1, { e: 1, n: 0 }, IP3, ANGLE));
const sStart = copingSpan(axis, L).sStart;
const sStart = copingSpan(axis, coping).sStart;
const z = base.copingTopEl + 0.15;
const bearingsAt = (row: '전열' | '후열', n: number) =>
GS.map((g, i) => {
@@ -29,10 +29,10 @@ function bentInput(foundationType: PierInput['foundationType'] = '현장타설
});
return {
...base,
type: 'T형',
columnCount: 1,
type,
columnCount: type === '2주형' ? 2 : 1,
foundationType,
copingLengthMm: L * 1000,
copingLengthMm: coping * 1000,
copingWidthMm: B * 1000,
ips: [
{ name: 'IP1', x: IP1.e, y: IP1.n, angleDeg: 0 },
@@ -123,9 +123,29 @@ describe('bent coping model (stage 2)', () => {
}
});
it('bent coping with footings or piles on the axis is refused until stage 3', () => {
expect(() => buildPier(bentInput('말뚝기초'))).toThrow('미실시');
expect(() => buildPier(bentInput('직접기초'))).toThrow('미실시');
it('footings are aligned with their stretch: a T형 footing crossing the kink stays in stretch 0 and is trimmed there', () => {
const piled = buildPier(bentInput('말뚝기초'));
expect(piled.footings).toHaveLength(1);
expect(piled.footings[0].k).toBe(0);
expect(piled.footings[0].length).toBeCloseTo(10.4, 6);
expect(piled.blindings[0].k).toBe(0);
expect(piled.piles).toHaveLength(20);
expect(piled.piles.every((p) => p.k === 0)).toBe(true);
expect(buildPier(bentInput('직접기초')).piles).toHaveLength(0);
});
it('2주형 bent with columns on both sides of the kink: separate footings, one per stretch', () => {
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);
const scene = buildPierScene(m);
const footings = scene.parts.footings.children as THREE.Mesh[];
expect(footings).toHaveLength(2);
for (const mesh of footings) {
expect(((mesh.material as THREE.MeshStandardMaterial).clippingPlanes ?? []).length).toBe(1);
}
});
it('a kink outside the coping is refused', () => {
+2 -2
View File
@@ -181,9 +181,9 @@ describe('꺾인 코핑 IP4 (spec §4.1)', () => {
expect(solveIp4({ e: 0, n: 0 }, { e: 1, n: 0 }, { e: 10, n: 2 }, 0)).toBeNull();
});
it('bent coping is still reported as 미실시 by buildPier', () => {
it('a bend on a straight P1 sheet (IP1, IP2, IP3 collinear) is refused: IP4 would land on IP3', () => {
const p1 = loadP1();
const ips = p1.ips.map((p) => (p.name === 'IP4' ? { ...p, angleDeg: 5 } : p));
expect(() => buildPier({ ...p1, ips })).toThrow('미실시');
expect(() => buildPier({ ...p1, ips })).toThrow('꺾임각과 IP 배치');
});
});