From 8aedc9b9697f3693d43c14c02e797ddac0ce49c2 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 10 Nov 2026 15:07:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(shape):=20=EA=B5=90=EC=A2=8C=EB=A9=B4=20EL?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=B9=A8=EB=B3=84=20=EC=BD=94=ED=95=91=20?= =?UTF-8?q?=EC=83=81=EB=A9=B4=20=EA=B3=84=EB=8B=A8=EC=9D=84=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A0=EB=8B=A4=20(W10=EC=9D=80=20=EC=A0=95=EB=B3=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 입력 교좌면 EL이 있으면 그 받침 구간의 코핑 상면 = 교좌면 EL (없으면 z − h1 / z − h2). 받침블럭 바닥과 코핑 상면이 항상 맞아 블럭이 뜨거나 묻히지 않는다. seatCalc는 기울기 계산값으로 남겨 W10 비교 기준으로 쓰고, W10은 경고(W)에서 정보(I)로 바꾼다. Co-Authored-By: Claude Haiku 5.5 --- src/core/geometry.ts | 10 ++++++---- src/core/validate.ts | 5 +++-- tests/seat-el.test.ts | 18 ++++++++++++++++++ tests/validate-and-foundation.test.ts | 9 ++++++++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 3af0d48..2f9bb9a 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -337,13 +337,15 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel { bounds.push((frontPlaced[k].g + frontPlaced[k + 1].g) / 2); } bounds.push(L); - const segments: Segment[] = frontPlaced.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: b.z - h1 })); + // 교좌면 EL per bearing (2026-11-10): when given, it is the coping top of that bearing's step; otherwise z − h1 (z − h2 behind). + const seatOf = new Map(input.bearings.map((r) => [r.no, r.seatEl])); + const segments: Segment[] = frontPlaced.map((b, k) => ({ s0: bounds[k], s1: bounds[k + 1], top: seatOf.get(b.no) ?? b.z - h1 })); // Lateral step (spec §4.3, Q1·Q2 추천안): front and back tops differ by 5 mm or more in some segment. const backSegments: Segment[] = backPlaced.map((b,k) => ({ s0: unequalRows ? (k === 0 ? 0 : (backPlaced[k-1].g+b.g)/2) : segments[k].s0, s1: unequalRows ? (k === backPlaced.length-1 ? L : (b.g+backPlaced[k+1].g)/2) : segments[k].s1, - top: b.z-h2, + top: seatOf.get(b.no) ?? b.z-h2, })); const stepBounds = [...new Set([0,L,...segments.slice(1).map(s=>s.s0),...backSegments.slice(1).map(s=>s.s0)])].sort((a,b)=>a-b); const stepSamples = stepBounds.slice(1).map((end,k)=>(stepBounds[k]+end)/2); @@ -478,9 +480,9 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel { }; const dropOf = (no: number) => soles.get(no)!.dropMm / 1000; // 교좌면 EL (2026-11-03): when the input gives it, the block bottom is that EL; otherwise the 두부보 상면 slope. - const seatOf = new Map(input.bearings.map((r) => [r.no, r.seatEl])); + // seatCalc stays the slope value (z − h1, z − h2) as the W10 reference. frontPlaced.forEach((b) => { - const seatCalc = segmentTop(segments, b.g); + const seatCalc = b.z - h1; const bottom = seatOf.get(b.no) ?? seatCalc; const skew = skewOf(b.no, b.k), drop = dropOf(b.no); blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness - drop, padTop: b.z - drop, rowSkewDeg: skew, plan: planOf(b.no, b.k, skew), seatCalc }); diff --git a/src/core/validate.ts b/src/core/validate.ts index d7107cb..68a6fd0 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -48,6 +48,7 @@ export function validateInput(input: PierInput, issues: Issue[]): boolean { /** Geometry-level checks (spec §10 W01–W05, W07, I01) on a built model. */ export function validatePier(input: PierInput, model: PierModel, issues: Issue[]): void { // W10 (2026-11-03): the input 교좌면 EL should match the 두부보 상면 slope within 5 mm; the input wins. + // Since 2026-11-10 the input also sets the coping top of that step, so this is information (I), not a warning. for (const b of model.blocks) { const seat = input.bearings.find((r) => r.no === b.no)?.seatEl; if (seat === undefined) continue; @@ -55,9 +56,9 @@ export function validatePier(input: PierInput, model: PierModel, issues: Issue[] if (Math.abs(diffMm) > 5) { issues.push({ code: 'W10', - level: 'W', + level: 'I', pier: input.name, - message: `${b.row} 받침 ${b.no}번 교좌면 EL ${seat.toFixed(3)}은(는) 두부보 상면 계산값 ${b.seatCalc.toFixed(3)}과 ${diffMm.toFixed(1)} mm 차이가 납니다 (허용 5 mm). 입력값을 씁니다.`, + message: `${b.row} 받침 ${b.no}번 교좌면 EL ${seat.toFixed(3)}은(는) 두부보 상면 계산값 ${b.seatCalc.toFixed(3)}과 ${diffMm.toFixed(1)} mm 차이가 납니다 (허용 5 mm). 입력값으로 받침블럭 바닥과 코핑 상면을 만듭니다.`, }); } } diff --git a/tests/seat-el.test.ts b/tests/seat-el.test.ts index 0f299e9..ffc466c 100644 --- a/tests/seat-el.test.ts +++ b/tests/seat-el.test.ts @@ -55,4 +55,22 @@ describe('교좌면 EL 입력 (2026-11-03)', () => { validatePier(input, m, issues); expect(issues.filter((i) => i.code === 'W10')).toEqual([]); }); + + it('an input 교좌면 EL also sets the coping top of that step, so the block sits on the coping (2026-11-10)', () => { + const m = buildPier(withSeat(loadP1(), [43.4, ...SEAT.slice(1)]), P1CTX); + const b1 = m.blocks.find((x) => x.no === 1 && x.row === '전열')!; + const seg = m.segments.find((s) => b1.g >= s.s0 && b1.g < s.s1)!; + expect(seg.top).toBeCloseTo(43.4, 6); + expect(b1.bottom).toBeCloseTo(seg.top, 9); + // seatCalc keeps the slope value (z − h1) as the W10 reference. + expect(b1.seatCalc).toBeCloseTo(43.411, 3); + }); + + it('W10 is information (I), not a warning, since the input builds the geometry', () => { + const input = withSeat(loadP1(), [43.5, ...SEAT.slice(1)]); + const m = buildPier(input, P1CTX); + const issues: Issue[] = []; + validatePier(input, m, issues); + expect(issues.filter((i) => i.code === 'W10').map((i) => i.level)).toEqual(['I']); + }); }); diff --git a/tests/validate-and-foundation.test.ts b/tests/validate-and-foundation.test.ts index 4724b1a..e5c79b5 100644 --- a/tests/validate-and-foundation.test.ts +++ b/tests/validate-and-foundation.test.ts @@ -214,7 +214,8 @@ describe('횡방향 단차 (spec §4.3, Q1·Q2 추천안)', () => { it('a 20 mm step in one segment splits the coping into two parts at the bearing-row midline', () => { const p1 = loadP1(); - const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.02 } : b)); + // The P1 sheet gives 교좌면 EL, which sets the step top (2026-11-10), so it is raised with z. + const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.02, seatEl: b.seatEl! + 0.02 } : b)); const m = buildPier({ ...p1, bearings }); expect(m.copingParts).toHaveLength(2); const [front, back] = m.copingParts; @@ -228,6 +229,12 @@ describe('횡방향 단차 (spec §4.3, Q1·Q2 추천안)', () => { expect(topAt(back, s2) - topAt(front, s2)).toBeCloseTo(0.02, 6); }); + it('a 20 mm higher 교좌면 EL alone (z unchanged) also makes the step (2026-11-10)', () => { + const p1 = loadP1(); + const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, seatEl: b.seatEl! + 0.02 } : b)); + expect(buildPier({ ...p1, bearings }).copingParts).toHaveLength(2); + }); + it('a 3 mm difference is below the 5 mm tolerance: no step', () => { const p1 = loadP1(); const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.003 } : b));