diff --git a/src/core/geometry.ts b/src/core/geometry.ts index cfa3b2c..eae9646 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -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 }))); } } } diff --git a/src/core/validate.ts b/src/core/validate.ts index 009100b..48e0df4 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -6,7 +6,6 @@ const EXPECTED_COLUMNS: Record = { '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; diff --git a/src/view/scene.ts b/src/view/scene.ts index 334d663..a6b55d0 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -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) { diff --git a/tests/bent-coping.test.ts b/tests/bent-coping.test.ts index 0f130d2..18bccfe 100644 --- a/tests/bent-coping.test.ts +++ b/tests/bent-coping.test.ts @@ -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', () => { diff --git a/tests/validate-and-foundation.test.ts b/tests/validate-and-foundation.test.ts index b2320d6..b084318 100644 --- a/tests/validate-and-foundation.test.ts +++ b/tests/validate-and-foundation.test.ts @@ -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 배치'); }); });