feat(B06): 306 횡단 맹암거 안 엇갈려 겹친 돌 무늬 · 그림 전용 · 고르기 차례 = 안쪽 판정면 맨 아래 · 테두리 판정 면 판정 맨 위

- 돌 무늬: 켜마다 반 칸 엇갈림 · 이웃 · 위아래와 겹침 · 기울기 ±22° 번갈아 교차 · 맹암거 색(카드 · [상세] 모달 같은 그림)
- 맹암거 무리 pointer-events none — 성토 띠 · 포장 · 넓은 관 판정면이 맹암거 안에서도 골라짐
- 차례: 보라 선 > 맹암거 테두리 > 구조물 판정면(작은 것 위) > 그림 · 띠 > 맹암거 안쪽
- 시험 test_306_underdrain_pick

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
2026-10-13 18:07:53 +09:00
co-authored by Claude Opus 5.5
parent c06ad0835f
commit 0a8008ce09
5 changed files with 427 additions and 6 deletions
+31 -1
View File
@@ -5,12 +5,19 @@
* 판정면(구조물 · 띠 · 형상)을 다 꽂은 뒤 넓이 큰 것부터 아래로 다시 세움(작은 것 위) ·
* 보라 선 판정(넓이 0 · 요구 185 사면 손잡이)은 맨 위 · 면적 띠(절 · 성토)는 맨 아래 그대로.
* 끌고 뗀 누름(3 px 넘게 움직임)은 고름 아님.
* 맹암거(306-2) = 안쪽 판정면은 맨 아래(겹친 띠 · 구조물이 위) · 테두리 판정(투명 굵은 선)이 면 판정 맨 위.
* ========================================================================== */
import { thickenDown } from "./B06_Section_UI_Cross_Shape_Pick";
/** 면 판정 — 구조물(벽 · 관 · 박스 · 도수로 띠 등) · 물넘이포장 · 맹암거 형상. */
const SURFACE_HIT_CLASSES = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"];
/** 선 판정 — 보라 설계선 조각(`_Cross_Design_Pick`). */
const LINE_HIT_CLASS = "b06-chart__design-hit";
/** 맹암거 몸(그림) · 테두리 판정 클래스(306-2). */
const UNDERDRAIN_GROUP = "b06-chart__underdrain";
const UNDERDRAIN_BODY = "b06-chart__underdrain-body";
const EDGE_CLASS = "b06-chart__shape-hit--edge";
/** 끌기로 볼 움직임(px) — 형상 고름(`SHAPE_DRAG_PX`)과 같은 값. */
const DRAG_PX = 3;
@@ -33,13 +40,36 @@ export function pointsArea(points: string | null): number {
/** 판정면을 넓이 차례로 다시 세우고 보라 선 판정을 맨 위로 · 끌기 누름을 막음. */
export function stackPickTargets(layer: SVGElement): void {
const kids = [...layer.children];
// 맹암거 판정면 = 형상 고름이 몸 둘레(`thickenDown`)로 꽂은 것
const bodies = new Set(
kids
.filter((el) => el.classList.contains(UNDERDRAIN_GROUP))
.flatMap((group) => [...group.children])
.filter((el) => el.classList.contains(UNDERDRAIN_BODY))
.map((el) => thickenDown(el.getAttribute("points") ?? "")),
);
const inners = kids.filter(
(el) =>
el.classList.contains("b06-chart__shape-hit") && bodies.has(el.getAttribute("points") ?? ""),
);
const edges = inners.map((inner) => {
layer.prepend(inner);
const edge = inner.cloneNode() as SVGElement;
edge.classList.add(EDGE_CLASS);
edge.addEventListener("click", (event) => {
event.stopPropagation();
inner.dispatchEvent(new MouseEvent("click")); // 고름은 안쪽 판정면 창구 하나
});
return edge;
});
const surfaces = kids
.filter((el) => SURFACE_HIT_CLASSES.some((name) => el.classList.contains(name)))
.filter((el) => !inners.includes(el))
.map((el) => ({ el, area: pointsArea(el.getAttribute("points")) }))
.sort((a, b) => b.area - a.area)
.map(({ el }) => el);
const lines = kids.filter((el) => el.classList.contains(LINE_HIT_CLASS));
layer.append(...surfaces, ...lines);
layer.append(...surfaces, ...edges, ...lines);
let down: { x: number; y: number } | null = null;
layer.addEventListener(
"pointerdown",
+25 -4
View File
@@ -5,17 +5,39 @@
* · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
* · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도.
* · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남.
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(흙막이 돌 그리기 `drawStones`).
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(306-1) — 켜마다 반 칸 엇갈리고 이웃과 겹친 돌 ·
* 기울기를 번갈아 서로 교차 · 그림 전용(누름 안 받음 · CSS `.b06-chart__underdrain`).
* 높이가 비면 그리지 않음(수치를 지어내지 않음).
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { drawStones } from "./B06_Section_UI_Cross_Wall_Hatch";
import { cell, courses, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
const SVG_NS = "http://www.w3.org/2000/svg";
let clipSeq = 0;
/** 돌 한 칸(m · 보기용) · 이웃과 겹침 배수 · 번갈아 기울기(도). */
const STONE_M = 0.25;
const STONE_OVERLAP = 1.4;
const STONE_TILT_DEG = 22;
/** 엇갈려 겹친 돌 무늬(306-1) — 켜(가로줄)마다 반 칸 밀고 · 돌은 칸보다 커서 이웃 · 위아래와 겹침 ·
* 기울기를 번갈아 서로 교차. `frame` v = −0.5(노면) ~ 0.5(옵셋선). */
export function drawCrossedStones(group: SVGElement, frame: BandFrame, heightM: number): void {
const rows = courses(heightM, STONE_M);
const count = courses(frame.spanM, STONE_M);
const across = (heightM / rows) * STONE_OVERLAP;
const along = (frame.spanM / count) * STONE_OVERLAP;
for (let r = 0; r < rows; r += 1) {
const shift = r % 2 ? 0 : 0.5;
for (let i = 0; i + shift <= count; i += 1) {
const tilt = (i + r) % 2 ? STONE_TILT_DEG : -STONE_TILT_DEG;
const lean = { ...frame, leanDegrees: frame.leanDegrees + tilt };
group.append(cell(lean, (i + shift) / count, (r + 0.5) / rows - 0.5, across, along, 0.45));
}
}
}
type Edge = { offset_m: number; elevation_m: number };
@@ -120,7 +142,7 @@ export function appendUnderdrainOverlay(
};
const [ax, ay] = px(at(0, 0));
const [bx, by] = px(at(1, 0));
drawStones(
drawCrossedStones(
stones,
{
spanM: Math.hypot(hi - lo, grade * (hi - lo)),
@@ -128,7 +150,6 @@ export function appendUnderdrainOverlay(
point: (u, v) => px(at(u, v)),
leanDegrees: (Math.atan2(bx - ax, -(by - ay)) * 180) / Math.PI,
},
false,
height,
);
group.append(body, clip, stones);
@@ -267,7 +267,25 @@
stroke-width: 1;
}
/* 맹암거(288-1): 노면 아래 돌 무늬 형상 */
/* 맹암거(288-1): 노면 아래 돌 무늬 형상 · 그림 전용 — 누름은 판정면(306-2) */
.b06-chart__underdrain {
pointer-events: none;
}
/* 엇갈려 겹친 돌(306-1) — 맹암거 색 · 밝음 · 어두움 모두 읽힘 */
.b06-chart__underdrain .b06-chart__culvert-stone {
stroke: #3aa0a8;
opacity: 0.75;
}
/* 맹암거 테두리 판정(306-2) — 면 판정 맨 위 · 안쪽 판정면은 맨 아래 */
.b06-chart__shape-hit.b06-chart__shape-hit--edge {
fill: none;
stroke: transparent;
stroke-width: 8;
pointer-events: stroke;
}
.b06-chart__underdrain-body {
fill: color-mix(in srgb, #3aa0a8 16%, transparent);
stroke: #3aa0a8;