diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 1f52265..110edf0 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -409,12 +409,16 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel { const meanN = (rows: { n: number }[]) => rows.reduce((sum, r) => sum + r.n, 0) / rows.length; const copingParts: CopingPart[] = []; const stretchCount = kinkG === null ? 1 : 2; + // One front/back line for the whole pier (2026-11-10), so the parts of the two stretches meet at the kink without a + // sideways jog; per stretch only when some bearing would be closer than half a block to that line. + const nAll = (meanN(frontPlaced) + meanN(backPlaced)) / 2; + const oneLine = [...frontPlaced, ...backPlaced].every((b) => Math.abs(b.n - nAll) >= halfBlock); for (let k = 0; k < stretchCount; k++) { - const fRows = frontPlaced.filter((b) => b.k === k); - const bRows = backPlaced.filter((b) => b.k === k); + const fRows = oneLine ? frontPlaced : frontPlaced.filter((b) => b.k === k); + const bRows = oneLine ? backPlaced : backPlaced.filter((b) => b.k === k); const nStep = fRows.length && bRows.length ? (meanN(fRows) + meanN(bRows)) / 2 - : (meanN(frontPlaced) + meanN(backPlaced)) / 2; + : nAll; if (lateralStep) { // The front row may lie on either side of the axis (it depends on the IP1 → IP3 direction), so each part takes the // side of its own row; the order stays [front, back]. diff --git a/tests/kink-steps.test.ts b/tests/kink-steps.test.ts index 0e890c6..0f7da7d 100644 --- a/tests/kink-steps.test.ts +++ b/tests/kink-steps.test.ts @@ -53,3 +53,39 @@ describe('꺾임점 근처 계단 경계 (A, 2026-11-10)', () => { expect(m.backSegments![2].s0).toBeCloseTo(own, 9); }); }); + +describe('전·후열 나눔선 하나 (B, 2026-11-10)', () => { + const split = (m: ReturnType, k: number) => { + const [front] = m.copingParts.filter((p) => p.k === k); + return Math.abs(front.nFrom) < Math.abs(front.nTo) ? front.nFrom : front.nTo; + }; + const meanN = (m: ReturnType, row: string, k?: number) => { + const rows = m.bearings.filter((b) => b.row === row && (k === undefined || b.k === k)); + return rows.reduce((s, b) => s + b.n, 0) / rows.length; + }; + + it('both stretches use the same line, the mean of the two rows over the whole pier (no jog at the kink)', () => { + const m = buildPier(p11()); + expect(m.copingParts).toHaveLength(4); + const nAll = (meanN(m, '전열') + meanN(m, '후열')) / 2; + expect(split(m, 0)).toBeCloseTo(nAll, 9); + expect(split(m, 1)).toBeCloseTo(nAll, 9); + // Per stretch the lines would differ by about 0.16 m. + const per = (k: number) => (meanN(m, '전열', k) + meanN(m, '후열', k)) / 2; + expect(Math.abs(per(0) - per(1))).toBeGreaterThan(0.1); + }); + + it('falls back to a line per stretch when a bearing would be closer than half a block to the common line', () => { + const input = p11(); + const base = buildPier(input); + const nrm = base.stretches[1].nrm; + // Move back ⑦ towards the axis (+n side here is the front), to n ≈ +0.1. + const b7 = base.bearings.find((b) => b.no === 7)!; + const d = 0.1 - b7.n; + const bearings = input.bearings.map((b) => (b.no === 7 ? { ...b, x: b.x + d * nrm.e, y: b.y + d * nrm.n } : b)); + const m = buildPier({ ...input, bearings }); + const nAll = (meanN(m, '전열') + meanN(m, '후열')) / 2; + expect(m.bearings.some((b) => Math.abs(b.n - nAll) < DIM.blockSize / 2)).toBe(true); + for (const k of [0, 1]) expect(split(m, k)).toBeCloseTo((meanN(m, '전열', k) + meanN(m, '후열', k)) / 2, 9); + }); +});