feat(b06): 맹암거 형상 누름 = 그 구조물 고름 — 물넘이와 같은 판정면 · 보라 손잡이 아래 · 끌기는 고름 아님 (288-2)

- 맹암거 둘레에 고름 표식(data-structure-id) · 287-5 판정면을 같이 탐(손잡이 아래 · 3 px 끌기 = 고름 아님)
- 형상 누름 창구 shapePickRoute — 관 지점(물넘이) = 그 카드 시설 고름 · 맹암거 = id 알림 → 구조물 목록이 id 로 고름(알약 · 폼 · 측점선 따라옴 · 노면공 포장 범위 고름과 따로)
- 목록 · 알약 고름 → 형상 강조(287-5 highlightShapes)
- 시험 test_288 · 287 대조 글

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-12 21:15:47 +09:00
co-authored by Claude Opus 5.5
parent fcf7daecb6
commit 990343edc1
7 changed files with 54 additions and 5 deletions
@@ -11,6 +11,8 @@
* ========================================================================== */
const SVG_NS = "http://www.w3.org/2000/svg";
/** 관 지점이 아닌 구조물(맹암거) 형상 누름 — 카드 밖(구조물 목록)이 id 로 고름(288-2). */
export const SHAPE_PICK_EVENT = "b06-structure-shape-pick";
/** 끌기로 볼 움직임(px) — 이보다 크면 고름 아님. */
export const SHAPE_DRAG_PX = 3;
@@ -49,6 +51,15 @@ export function attachShapePick(
return (active) => shapes.forEach((shape) => shape.classList.toggle("is-active", active));
}
/** 형상 누름 창구(287-5 · 288-2) — 관 지점(`pipe-…` · 물넘이포장) = 그 카드 시설 고름 · 그 밖(맹암거) = id 를
* 실은 알림을 위로 띄움(구조물 목록이 받음). */
export function shapePickRoute(from: Element, pickPipe: () => void): (structureId: string) => void {
return (structureId) => {
if (structureId.startsWith("pipe-")) return pickPipe();
from.dispatchEvent(new CustomEvent(SHAPE_PICK_EVENT, { bubbles: true, detail: structureId }));
};
}
/** 카드 안 형상 강조 — 고른 구조물 id 와 같은 형상만(목록 · 알약 · 폼 고름이 따라옴). */
export function highlightShapes(root: ParentNode, structureId: string | null): void {
for (const shape of root.querySelectorAll<SVGElement>("[data-structure-id]")) {
@@ -98,6 +98,7 @@ export function appendUnderdrainOverlay(
const body = document.createElementNS(SVG_NS, "polygon");
body.setAttribute("points", points);
body.setAttribute("class", "b06-chart__underdrain-body");
if (spec.structure_id) body.setAttribute("data-structure-id", spec.structure_id);
clipSeq += 1;
const clipId = `b06-underdrain-clip-${clipSeq}`;
const clip = document.createElementNS(SVG_NS, "clipPath");
+5 -2
View File
@@ -20,7 +20,7 @@ import {
appendFordSurfaceDropPlan,
} from "./B06_Section_UI_Cross_Ford_Pavement";
import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
import { attachShapePick } from "./B06_Section_UI_Cross_Shape_Pick";
import { attachShapePick, shapePickRoute } from "./B06_Section_UI_Cross_Shape_Pick";
import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain";
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
import { attachChuteBandPick } from "./B06_Section_UI_Cross_Wall_Hatch";
@@ -579,7 +579,10 @@ export function createCrossSectionCard(
setChuteActive(highlightRevet === "chute");
}
// 물넘이포장 · 맹암거 형상 고름(287-5 · 288-2) — 다른 시설과 같은 창구 · 보라 손잡이 아래 · 누름만
const setShapeActive = attachShapePick(plotLayer, revetOffset && (() => toggleRevet("pipe")));
const setShapeActive = attachShapePick(
plotLayer,
revetOffset && shapePickRoute(svg, () => toggleRevet("pipe")),
);
if (setShapeActive) {
const baseSetActive = setRevetActive;
setRevetActive = (key) => {
+6 -1
View File
@@ -69,7 +69,7 @@ import {
import type { SectionViewController } from "./B06_Section_UI_Section_View_Types";
import { createFillSlopeNotice } from "./B06_Section_UI_Cross_FillSlope_Notice";
import { highlightPavement, pavementAt } from "./B06_Section_UI_Cross_Design_Surface";
import { highlightShapes } from "./B06_Section_UI_Cross_Shape_Pick";
import { highlightShapes, SHAPE_PICK_EVENT } from "./B06_Section_UI_Cross_Shape_Pick";
import { createSectionProfile } from "./B06_Section_UI_Section_View_Profile";
import type { DesignPageShared } from "../A00_Common/design/design_shared";
@@ -440,6 +440,11 @@ export function createSectionView(
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다.
grid.style.gridTemplateColumns = `repeat(${columnCount}, ${cachedCardWidth}px)`;
grid.dataset.columns = String(columnCount); // 4열부터 면적표 「계」만(173-3 · CSS)
// 맹암거 형상 누름 = 그 구조물 id 고름(288-2 · 노면공 포장 범위 표식과 따로 · 같은 id 고름 창구)
grid.addEventListener(SHAPE_PICK_EVENT, (event) => {
const id = (event as CustomEvent<string>).detail;
if (id) pavementSelectListener?.(id);
});
cachedRowHeight.clear();
if (detail.cross_sections.length) {
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
@@ -73,6 +73,21 @@ const group = draw.appendUnderdrainOverlay(
);
const body = group?.children.find((k) => k.attrs.class === "b06-chart__underdrain-body");
// 288-2 형상 누름 창구 — 물넘이(pipe-…) = 시설 고름 · 맹암거 = id 알림(목록이 고름)
globalThis.CustomEvent = class {
constructor(type, init) {
this.type = type;
this.detail = init?.detail;
this.bubbles = init?.bubbles;
}
};
const shapePick = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts"));
const routed = [];
const from = { dispatchEvent: (event) => routed.push([event.type, event.detail, event.bubbles]) };
const route = shapePick.shapePickRoute(from, () => routed.push(["pipe"]));
route("pipe-230.00");
route("ud-1");
process.stdout.write(
JSON.stringify({
specs,
@@ -82,6 +97,8 @@ process.stdout.write(
noHeight: draw.underdrainOutline(design, null),
drew: !!group,
bodyId: body?.attrs["data-structure-id"] ?? null,
routed,
eventName: shapePick.SHAPE_PICK_EVENT,
stones: group?.children.filter((k) => k.tag === "g").flatMap((g) => g.children).length ?? 0,
}),
);
@@ -121,8 +121,8 @@ def test_형상_강조와_표식(ts):
def test_보라_손잡이는_계획고만_형상_누름은_고름만():
view = _read("B06_Section", "B06_Section_UI_Cross_View.ts")
# 형상 누름 = 시설 고름 창구만(설계 · 계획고를 안 건드림) · 보라 선 고름보다 먼저 꽂음(손잡이가 위)
assert 'attachShapePick(plotLayer, revetOffset && (() => toggleRevet("pipe")))' in view
assert view.index("attachShapePick(plotLayer") < view.index("attachDesignLinePick({")
assert 'shapePickRoute(svg, () => toggleRevet("pipe"))' in view
assert view.index("attachShapePick(") < view.index("attachDesignLinePick({")
pick = _read("B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts")
assert "onDesignChange" not in pick and "design_elevation" not in pick
assert '"b06-chart__culvert-revet-hit"' in pick # 구조물 판정면 앞(아래)에 꽂을 기준만
@@ -5,6 +5,8 @@
서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted`) · 제원 얹기 서버 `attach_underdrains` ↔ TS
`attachUnderdrainSpecs` 같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과
만남 · 절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림.
- 288-2 맹암거 형상 누름 = 그 구조물 id 고름(구조물 목록 창구 · 알약 · 폼 · 측점선 따라옴) · 물넘이와 같은
판정면(보라 손잡이 아래 · 끌기 = 고름 아님 · 287-5) · 노면공 포장 범위 고름과 따로.
진짜 TS 는 `helper_288_underdrain.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
"""
@@ -141,3 +143,13 @@ def test_횡단도_B07_이_맹암거를_그림():
assert "appendUnderdrainOverlay(svg, section.underdrain, design, x, y);" in b07
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
assert "tilted: Boolean(section.ford_pavement || section.underdrain)," in refresh
def test_맹암거_형상_누름은_그_구조물_고름(ts):
assert ts["bodyId"] == "ud-1" # 둘레 = 형상 고름 표식
assert ts["routed"] == [["pipe"], [ts["eventName"], "ud-1", True]]
view = (ROOT / "B06_Section" / "B06_Section_UI_Section_View.ts").read_text(encoding="utf-8")
assert "grid.addEventListener(SHAPE_PICK_EVENT, (event) => {" in view
assert "if (id) pavementSelectListener?.(id);" in view # = structuresPanel.selectStructure(id)
cross = (ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
assert 'revetOffset && shapePickRoute(svg, () => toggleRevet("pipe")),' in cross