fix(B06): 옹벽 물빼기 파이프 끝을 벽 면에서 자름(73-2)
- B07 수확은 화면 밖 SVG 라 clip 이 안 먹음 — 벽 밖 0.5 m 가 실려 그림 범위가 바뀌던 것 - 파이프 가운데 = 그 높이의 벽 두께 중심 · 시험에 벽 밖 판정 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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], "선 색은 묶음 토큰이 정함"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user