From b8e0587465ace5fe098f69597376724278707e34 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 7 Oct 2026 08:04:04 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=98=B9=EB=B2=BD=20=EB=AC=BC?= =?UTF-8?q?=EB=B9=BC=EA=B8=B0=20=ED=8C=8C=EC=9D=B4=ED=94=84=20=EB=81=9D?= =?UTF-8?q?=EC=9D=84=20=EB=B2=BD=20=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=9E=90?= =?UTF-8?q?=EB=A6=84(73-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B07 수확은 화면 밖 SVG 라 clip 이 안 먹음 — 벽 밖 0.5 m 가 실려 그림 범위가 바뀌던 것 - 파이프 가운데 = 그 높이의 벽 두께 중심 · 시험에 벽 밖 판정 추가 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- .../B06_Section_UI_Cross_Wall_Hatch.ts | 48 ++++++++++++++++--- resources/tester/test_b06_wall_drain_pipe.py | 10 ++-- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts index c7160e262..0449d5ff9 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts @@ -17,7 +17,7 @@ * ========================================================================== */ import { REVET_THICKNESS_M } from "./B06_Section_UI_Cross_Culvert_Const"; -import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; +import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; const SVG_NS = "http://www.w3.org/2000/svg"; const HATCH_CLASS = "b06-chart__culvert-stone"; @@ -159,9 +159,32 @@ const DRAIN_DIAMETER_M = 0.15; const DRAIN_FALL = 0.1; const DRAIN_CLASS = "b06-chart__wall-drain"; +/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */ +function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null { + const dx = b.offset - a.offset; + const dy = b.elevation - a.elevation; + const hits: number[] = []; + polygon.forEach((p, i) => { + const q = polygon[(i + 1) % polygon.length]; + const ex = q.offset - p.offset; + const ey = q.elevation - p.elevation; + const denominator = dx * ey - dy * ex; + if (Math.abs(denominator) < 1e-12) return; + const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator; + const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator; + if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t); + }); + if (hits.length < 2) return null; + const at = (t: number): OffsetPoint => ({ + offset: a.offset + dx * t, + elevation: a.elevation + dy * t, + }); + return [at(Math.min(...hits)), at(Math.max(...hits))]; +} + /** * 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다 - * 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 끝은 벽 clip 이 자름. + * 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼. */ function drawDrains( group: SVGElement, @@ -172,20 +195,31 @@ function drawDrains( const bottom = wall.bottomBack.elevation; const height = wall.topBack.elevation - bottom; const count = Math.max(1, Math.round(height / DRAIN_BAND_M)); + const back = wall.backOffset; + const [, , topFront, bottomFront] = wall.points; for (let i = 0; i < count; i += 1) { const center = bottom + (height * (i + 0.5)) / count; + // 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움. + const front = + bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height); + const fall = (DRAIN_FALL * Math.abs(front - back)) / 2; const pipe = document.createElementNS(SVG_NS, "g"); pipe.setAttribute("class", DRAIN_CLASS); const title = document.createElementNS(SVG_NS, "title"); title.textContent = "물빼기(보기용)"; pipe.append(title); - // 배면 바깥 0.5m ~ 전면 발끝 바깥 0.5m — 기울기를 유지한 채 벽 clip 이 두께만큼 남김. - const back = wall.backOffset - wall.outward * 0.5; - const front = wall.bottomFront.offset + wall.outward * 0.5; - const fall = (DRAIN_FALL * Math.abs(front - back)) / 2; for (const side of [-0.5, 0.5]) { const level = center + side * DRAIN_DIAMETER_M; - const el = line([x(back), y(level + fall)], [x(front), y(level - fall)]); + const run = insideRun( + { offset: back, elevation: level + fall }, + { offset: front, elevation: level - fall }, + wall.points, + ); + if (!run) continue; + const el = line( + [x(run[0].offset), y(run[0].elevation)], + [x(run[1].offset), y(run[1].elevation)], + ); el.removeAttribute("class"); // 색은 묶음(`.b06-chart__wall-drain line`)이 정함 pipe.append(el); } diff --git a/resources/tester/test_b06_wall_drain_pipe.py b/resources/tester/test_b06_wall_drain_pipe.py index da5fda459..62502cca2 100644 --- a/resources/tester/test_b06_wall_drain_pipe.py +++ b/resources/tester/test_b06_wall_drain_pipe.py @@ -45,6 +45,8 @@ const drains = (heightM, form, retaining, outward = 1) => { const layer = make("svg"); appendWallHatch(layer, wall, (o) => o, (e) => -e, "t"); const bottom = wall.bottomBack.elevation; + const xs = wall.points.map((p) => p.offset); + const [lo, hi] = [Math.min(...xs) - 1e-6, Math.max(...xs) + 1e-6]; return walk(layer).map((pipe) => { const lines = pipe.children.filter((c) => c.tag === "line"); const mid = lines.map((l) => -(Number(l.attrs.y1) + Number(l.attrs.y2)) / 2); @@ -53,6 +55,7 @@ const drains = (heightM, form, retaining, outward = 1) => { lines: lines.length, center: (mid[0] + mid[1]) / 2 - bottom, frontLower: drop.every((d) => d > 0), + inside: lines.every((l) => [l.attrs.x1, l.attrs.x2].every((v) => lo <= +v && +v <= hi)), title: pipe.children.find((c) => c.tag === "title")?.text, cls: lines.map((l) => l.attrs.class ?? null), }; @@ -104,14 +107,15 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None ) got = json.loads(result.read_text(encoding="utf-8")) - assert [p["center"] for p in got["h15"]] == pytest.approx([0.75]) - assert [p["center"] for p in got["h30"]] == pytest.approx([0.75, 2.25]) - assert [p["center"] for p in got["h30right"]] == pytest.approx([0.75, 2.25]) + assert [p["center"] for p in got["h15"]] == pytest.approx([0.75], abs=0.01) + assert [p["center"] for p in got["h30"]] == pytest.approx([0.75, 2.25], abs=0.01) + assert [p["center"] for p in got["h30right"]] == pytest.approx([0.75, 2.25], abs=0.01) assert len(got["low"]) == 1, "낮은 옹벽도 최소 1개" assert got["wet"] == [] and got["concrete"] == [], "흙막이(찰쌓기 · 콘크리트)에 파이프가 섬" for pipe in got["h15"] + got["h30"] + got["h30right"]: assert pipe["lines"] == 2, "두 줄 평행선이 아님" assert pipe["frontLower"], "앞쪽이 낮지 않음" + assert pipe["inside"], "파이프가 벽 밖으로 나감(B07 수확은 clip 이 없음)" assert pipe["title"] == "물빼기(보기용)" assert pipe["cls"] == [None, None], "선 색은 묶음 토큰이 정함"