diff --git a/src/core/geometry.ts b/src/core/geometry.ts index fb2a00c..1f52265 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -334,9 +334,14 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel { if (unequalRows) { frontPlaced.sort((a,b)=>a.g-b.g); backPlaced.sort((a,b)=>a.g-b.g); } // Step boundaries sit midway between consecutive front bearings in coping coordinates (spec §4.3). + // A step boundary less than half a block from the kink moves onto it (2026-11-10), so the step face is the mitre + // plane and the two stretches meet without a wedge; only when both bearings stay half a block away from the kink. + const halfBlock = DIM.blockSize / 2; + const toKink = (bound: number, gLeft: number, gRight: number) => + kinkG !== null && Math.abs(bound - kinkG) < halfBlock && kinkG - gLeft >= halfBlock && gRight - kinkG >= halfBlock ? kinkG : bound; const bounds = [0]; for (let k = 0; k < frontPlaced.length - 1; k++) { - bounds.push((frontPlaced[k].g + frontPlaced[k + 1].g) / 2); + bounds.push(toKink((frontPlaced[k].g + frontPlaced[k + 1].g) / 2, frontPlaced[k].g, frontPlaced[k + 1].g)); } bounds.push(L); // 교좌면 EL per bearing (2026-11-10): when given, it is the coping top of that bearing's step; otherwise z − h1 (z − h2 behind). @@ -349,7 +354,7 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel { // front bound snaps to it, so slightly staggered rows (P1, ≈ 0.09 m) keep one boundary and no sliver step. const backBound = (k: number) => { const own = (backPlaced[k-1].g + backPlaced[k].g) / 2; - return !unequalRows && Math.abs(own - bounds[k]) < DIM.blockSize / 2 ? bounds[k] : own; + return !unequalRows && Math.abs(own - bounds[k]) < halfBlock ? bounds[k] : toKink(own, backPlaced[k-1].g, backPlaced[k].g); }; const backSegments: Segment[] = backPlaced.map((b,k) => ({ s0: k === 0 ? 0 : backBound(k), diff --git a/tests/kink-steps.test.ts b/tests/kink-steps.test.ts new file mode 100644 index 0000000..0e890c6 --- /dev/null +++ b/tests/kink-steps.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import * as XLSX from 'xlsx'; +import { parseWorkbook } from '../src/core/parseWorkbook'; +import { buildPier, DIM, defaultLevelWidthMm } from '../src/core/geometry'; +import type { PierInput } from '../src/core/types'; + +// 산외교 P11 (샘플01) with the coping bent 8.767° at IP4 (2026-11-10 검토): back ⑥ g 11.64, ⑦ g 18.49, kink g 15.14. +const SAMPLE = fileURLToPath(new URL('../project_data/01_엑셀/샘플01_산외교_교각입력데이터.xlsx', import.meta.url)); + +function p11(): PierInput { + const wb = XLSX.read(readFileSync(SAMPLE), { type: 'buffer' }); + wb.Sheets['IP']['F46'] = { t: 'n', v: 8.767 }; + const input = parseWorkbook(wb).piers.find((p) => p.name === 'P11')!; + return { ...input, levelWidthMm: input.levelWidthMm ?? defaultLevelWidthMm(input.type, 2000) }; +} + +const kinkOf = (m: ReturnType) => m.stretches[1].base; +const gOf = (m: ReturnType, no: number) => m.bearings.find((b) => b.no === no)!.g; + +describe('꺾임점 근처 계단 경계 (A, 2026-11-10)', () => { + it('a back boundary 0.07 m from the kink moves onto the kink (⑥|⑦)', () => { + const m = buildPier(p11()); + const kink = kinkOf(m); + const own = (gOf(m, 6) + gOf(m, 7)) / 2; + expect(Math.abs(own - kink)).toBeLessThan(0.1); + expect(m.backSegments![2].s0).toBeCloseTo(kink, 9); + expect(m.backSegments![1].s1).toBeCloseTo(kink, 9); + }); + + it('boundaries far from the kink stay midway between their bearings', () => { + const m = buildPier(p11()); + expect(m.backSegments![1].s0).toBeCloseTo((gOf(m, 5) + gOf(m, 6)) / 2, 9); + expect(m.backSegments![3].s0).toBeCloseTo((gOf(m, 7) + gOf(m, 8)) / 2, 9); + m.segments.slice(1).forEach((seg) => expect(Math.abs(seg.s0 - kinkOf(m))).toBeGreaterThan(DIM.blockSize / 2)); + }); + + it('no snap when a bearing would end up closer than half a block to the kink', () => { + const input = p11(); + const base = buildPier(input); + // ⑦ 0.4 m past the kink, ⑥ 0.6 m before it: midpoint 0.1 m before the kink, but ⑦ is within half a block. + const u1 = base.stretches[1].u; + const u0 = base.stretches[0].u; + const kink = kinkOf(base); + const d7 = kink + 0.4 - gOf(base, 7); + const d6 = kink - 0.6 - gOf(base, 6); + const bearings = input.bearings.map((b) => + b.no === 7 ? { ...b, x: b.x + d7 * u1.e, y: b.y + d7 * u1.n } : b.no === 6 ? { ...b, x: b.x + d6 * u0.e, y: b.y + d6 * u0.n } : b); + const m = buildPier({ ...input, bearings }); + const own = (gOf(m, 6) + gOf(m, 7)) / 2; + expect(kinkOf(m) - own).toBeCloseTo(0.1, 3); + expect(m.backSegments![2].s0).toBeCloseTo(own, 9); + }); +});