Files
Aislo/B06_Section/B06_Section_UI_Cross_Ford.ts
T
eomsangdonandClaude Opus 5.5 cb4bab7bf2 feat(구조물표): 323-5 고름 한 칸 — 관 세션 id(옮겨도 그대로 · 저장 안 함) · 고름 강조 한 규칙(pickLights · 카드 형상 · 3D 같은 함수) · 그린 것마다 신원표(data-structure-id + data-part) · 같은 자리 거리 = PIPE_MATCH_M 하나 · 322-0 이 깬 옛 시험 도움 파일 · 줄 수 고침
- `design_pipe_ids.ts` — 장부(세션 pipe-ids) · pipe-s{차례} · 자리 → id(찾는 거리 안 가장 가까운 것) · followMovedPipe 가 id 를 새 자리로(고름 id 그대로 · 순환 import 없게 design_selection 에서 옮김) · 설계 페이지가 프로젝트를 정함(usePipeIds) · 자리 이름 만들기 열 곳 · 되읽기 둘 걷음 · 측점 계획 주인 = 세션 id(withPipeIds)
- `design_pick_match.ts` — pickLights(① id ② 같은 자리 ③ 고른 부분) · paintPicked(카드 통째 형상 · 부분 · 관 계열 · 포장층은 제 손잡이) · tagShape — 관 세트 벽 · 집수정 · 관 · 세월교 측벽 · 몸체 · BOX 끝 · C군 벽 · 물넘이 콘크리트에 신원표 · 형상 판정면은 통째 형상 · 콘크리트만
- 3D — 부재 맞춤 = pickLights(옛 0.01 걷음) · C군 벽 솔리드 = 구조물 id(덮는 측점 어디서 골라도 한 번 스윕한 벽이 켜짐)
- 322-0 이 깬 것 고침 — 노면공 흐름 도움 파일(165 · 231-1) 정의 판 확인 흉내 · design_view_profile 700줄(322-0 이 704)
- 시험: test_323_5_pick(세션 id · 옮김 · 강조 규칙 · 칠하기) · 옛 시험 도움 파일 흉내 · 기대 글 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 18:06:13 +09:00

219 lines
8.9 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Ford.ts
* 세월교 측점 횡단 카드의 구체(측벽·바닥판·관·성토 채움) **오버레이 그리기**.
*
* 기하 계산은 `B06_Section_UI_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리).
* 여기서는 계산 결과(`FordLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
*
* 그리는 순서 = 시공 순서: ① 바닥판 → ② 측벽(ㄴ형 집수정 재사용) → ③ 관 →
* ④ 계류측 성토부선·절토선. 관 위 성토 채움은 그리지 않는다(2026-08-25 사용자).
* ========================================================================== */
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
import { tagShape } from "../A00_Common/design/design_pick_match";
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
import {
appendPipeInfoText,
outlineBox,
type OffsetPoint,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
export {
computeFordLayout,
DEFAULT_FORD_ADJUST,
DEFAULT_FORD_WALL_ADJUST,
fordAdjustOf,
fordDropOf,
} from "./B06_Section_UI_Cross_Ford_Geom";
export type {
FordAdjust,
FordLayout,
FordSideLayout,
FordWallAdjust,
FordWallRole,
} from "./B06_Section_UI_Cross_Ford_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
/** 고르는 부재 — 측벽(유입 · 유출) 또는 본체(바닥판 · 관 · 좌측 「세월교」 상자 · 66-1). */
export type FordPick = FordWallRole | "body";
/** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */
export type FordHighlightSetter = (key: FordPick | null) => void;
/**
* 세월교 구체 오버레이 — computeFordLayout 결과를 그린다.
*
* `onSelectWall`이 오면 측벽이 **선택 대상**이 된다(집수정과 같은 규칙).
* 클릭은 여기서 멈춘다 — 밑에 깔린 면적 밴드로 번지면 강조가 딴 것으로 바뀐다.
*/
export function appendFordOverlay(
layer: SVGElement,
layout: FordLayout,
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
onSelectWall?: (key: FordPick) => void,
): FordHighlightSetter {
const { ford } = layout;
const ownerId = ford.chainage_m != null ? pipeIdAt(ford.chainage_m) : null; // 신원표(323-5)
const toXY = (point: OffsetPoint): [number, number] => [
x(point.offset),
toDisplayY(point.elevation),
];
const polygon = (
points: OffsetPoint[],
className: string,
tooltip: string,
): SVGPolygonElement => {
const shape = document.createElementNS(SVG_NS, "polygon");
shape.setAttribute("points", points.map((point) => toXY(point).join(",")).join(" "));
shape.setAttribute("class", className);
const title = document.createElementNS(SVG_NS, "title");
title.textContent = tooltip;
shape.append(title);
return shape;
};
const planLine = (points: OffsetPoint[], tooltip: string): void => {
if (points.length < 2) return;
const line = document.createElementNS(SVG_NS, "polyline");
line.setAttribute("points", points.map((point) => toXY(point).join(",")).join(" "));
// 성토·절토 접속선은 공사 계획선 — 설계선과 같은 보라 실선.
line.setAttribute("class", "b06-chart__design-cross");
const title = document.createElementNS(SVG_NS, "title");
title.textContent = tooltip;
line.append(title);
layer.append(line);
};
const wingText = (wing: {
installed: boolean;
length_m: number | null;
angle_deg: number | null;
}): string => (wing.installed ? `${wing.length_m ?? 0}m·${wing.angle_deg ?? 0}°` : "없음");
// ① 바닥판 — 두 측벽 사이를 잇는 판. 각 측 바깥 연장은 측벽 부재로 함께 그린다.
const slab = layer.appendChild(
polygon(
layout.slabBridge,
"b06-chart__ford-slab",
`세월교 바닥판 — 길이 ${layout.slabLengthM.toFixed(2)}m · 두께 ${ford.slab_thickness_m.toFixed(2)}m` +
` (날개벽 유입 ${wingText(ford.wing_in)} / 유출 ${wingText(ford.wing_out)} 투영 연장 반영)`,
),
);
// ② 측벽 — ㄴ형 집수정 부재(기운 벽 + 바닥판)를 좌우에 하나씩.
const walls = new Map<FordWallRole, SVGPolygonElement[]>();
for (const side of layout.sides) {
const role = side.role === "inlet" ? "유입" : "유출";
const shapes: SVGPolygonElement[] = [];
for (const part of side.parts) {
const shape = polygon(
part.points,
`b06-chart__culvert-basin${onSelectWall ? " is-selectable" : ""}`,
`${role} 측벽(ㄴ형) ${part.kind === "wall" ? "벽" : "바닥판"} — 높이 ${side.heightM.toFixed(2)}m` +
` · 두께 ${ford.wall_thickness_m.toFixed(2)}m` +
(onSelectWall ? " (클릭 후 조정창에서 높이·좌우·상하 조정)" : ""),
);
if (onSelectWall) {
shape.addEventListener("click", (event) => {
event.stopPropagation();
onSelectWall(side.role);
});
}
tagShape(shape, ownerId, side.role);
shapes.push(shape);
layer.append(shape);
}
walls.set(side.role, shapes);
}
// ③ 관 — 바닥판 상면에 안힌다. 단면엔 1개만, 련수는 라벨로 알린다.
const pipeOuter = polygon(
layout.pipe.outer,
"b06-chart__culvert-pipe b06-chart__culvert-pipe--outer",
`${layout.label.text} — 관 밑이 바닥판 상면`,
);
const pipeInner = polygon(layout.pipe.inner, "b06-chart__culvert-pipe", layout.label.text);
layer.append(pipeOuter, pipeInner);
const bodyShapes = [slab, pipeOuter, pipeInner];
for (const shape of bodyShapes) tagShape(shape, ownerId, "body");
// 도수로 · 돌붙임 띠(90-2) — 유출 측벽 끝 ~ 원지반 성토부선 따라(흙막이 쪽은 배관 오버레이가 그림)
if (ford.chute)
appendChuteBand(
layer,
layout.sides
.filter((side) => side.role === "outlet")
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut")),
ford.chute,
x,
toDisplayY,
layout.ground,
);
// ④ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계).
for (const side of layout.sides) {
const role = side.role === "inlet" ? "유입" : "유출";
for (const segment of side.fillSegments) {
if (segment.kind === "cut") continue;
planLine(
segment.points,
`${role} 측벽 계류측 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m`,
);
}
if (side.cutLine) {
planLine(side.cutLine, `${role} 측벽 절토선(계획선)`);
}
}
// 관·채움이 벽 위에 겹쳐 클릭을 가로챈다 — 투명 겹면을 맨 위에 얹어 벽을 고른다
// (배수관 기슭막이 `culvert-revet-hit`와 같은 수법).
if (onSelectWall) {
// 본체 판정면 = 바닥판 밑 ~ 측벽 상단 최고 사각형 하나(관 위 성토 · 벽-관 사이 포함 · 89-2) —
// 측벽 판정면 · 흙막이(뒤에 그리는 배관 오버레이)가 위에 얹혀 겹친 자리는 그쪽이 먹는다(66-1).
const offsets = [
...layout.slabBridge,
...layout.sides.flatMap((side) => side.parts.flatMap((part) => part.points)),
].map((point) => point.offset);
const top = Math.max(...layout.sides.map((side) => side.top.elevation));
const bottom = Math.min(...layout.slabBridge.map((point) => point.elevation));
const [from, to] = [Math.min(...offsets), Math.max(...offsets)];
const bodyHit = polygon(
[
{ offset: from, elevation: top },
{ offset: to, elevation: top },
{ offset: to, elevation: bottom },
{ offset: from, elevation: bottom },
],
"b06-chart__culvert-revet-hit",
"세월교 본체 선택",
);
bodyHit.addEventListener("click", (event) => {
event.stopPropagation();
onSelectWall("body");
});
layer.append(bodyHit);
// 측벽 판정면 = ㄴ(벽 + 바닥판)을 감싸는 가상 사각형(79-6) — 얇은 벽만 잡던 것을 넓힘.
for (const side of layout.sides) {
const points = side.parts.flatMap((part) => part.points);
if (!points.length) continue;
const hit = polygon(outlineBox(points), "b06-chart__culvert-revet-hit", "");
hit.addEventListener("click", (event) => {
event.stopPropagation();
onSelectWall(side.role);
});
layer.append(hit);
}
}
// 세월교 관 정보 글 = 노폭 · 노견 라벨 위 줄(225-1) · 그 라벨이 없으면 옛 자리.
appendPipeInfoText(layer, layout.label.text, toXY(layout.label.at));
return (key: FordPick | null): void => {
for (const [role, shapes] of walls) {
for (const shape of shapes) shape.classList.toggle("is-active", role === key);
}
for (const shape of bodyShapes) shape.classList.toggle("is-active", key === "body");
};
}