Files
Aislo/B06_Section/B06_Section_UI_Cross_Ford.ts
T
eomsangdonandClaude Opus 5 81bb505cc4 feat(B05·B06): 세월교 횡단 구체·조정창·3D 예상형상
B05 세월교 옵션에 BOX암거 날개벽 한 벌(설치·짧은쪽 높이 1m·길이 2m·각도 45°)을
그대로 붙이고, B06 횡단도에 세월교 구체를 그린다. 구체는 양측 ㄴ형 집수정 측벽 +
그 사이를 잇는 바닥판 + 바닥판 상면에 안힌 관 + 관 위 성토(노체) 채움이다.

- 바닥판 편측 연장 = 날개벽 길이 × cos(각도) — 각도는 관축 기준 벌어짐각
- 측벽은 buildBasin 재사용이라 1:0.3 기움·좌우·상하 대각 이동·계류측 성토부선이
  집수정과 같은 규칙으로 따라온다 (기슭막이 다단·재질과는 연계하지 않음)
- 구체는 월류 폭만큼 도로 방향으로 이어져 기준 측점 전후 절반까지 붙는다
- 조정창: 측벽 높이·좌우·상하, 관경·수량(정본 pipe_points 되쓰기).
  높이·좌우·상하는 세션 + design.ford_adjust에 저장
- 3D 예상형상: 구체 부재는 월류 폭 구간 스윕, 관은 련수만큼 폭 안 등간격
- 부재 두께: 바닥판 0.3m(교본 물넘이 물받이 최소 30cm), 측벽 0.2m(집수정 승계)

700줄 제한으로 카드 렌더러·측점 제어기·페이지에서 배선을 분리했다
(_Cross_View_Ford, _Page_Ford_Controls, _Page_Patches, Culvert_Const 구간값 헬퍼).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:05:42 +09:00

175 lines
6.8 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Ford.ts
* 세월교 측점 횡단 카드의 구체(측벽·바닥판·관·성토 채움) **오버레이 그리기**.
*
* 기하 계산은 `B06_Section_UI_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리).
* 여기서는 계산 결과(`FordLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
*
* 그리는 순서 = 시공 순서: ① 바닥판 → ② 측벽(ㄴ형 집수정 재사용) → ③ 관 →
* ④ 성토(노체) 채움 → ⑤ 계류측 성토부선·절토선.
* ========================================================================== */
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
export {
computeFordLayout,
DEFAULT_FORD_ADJUST,
DEFAULT_FORD_WALL_ADJUST,
} 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";
/** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */
export type FordHighlightSetter = (key: FordWallRole | null) => void;
/**
* 세월교 구체 오버레이 — computeFordLayout 결과를 그린다.
*
* `onSelectWall`이 오면 측벽이 **선택 대상**이 된다(집수정과 같은 규칙).
* 클릭은 여기서 멈춘다 — 밑에 깔린 면적 밴드로 번지면 강조가 딴 것으로 바뀐다.
*/
export function appendFordOverlay(
layer: SVGElement,
layout: FordLayout,
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
onSelectWall?: (key: FordWallRole) => void,
): FordHighlightSetter {
const { ford } = layout;
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}${wing.angle_deg ?? 0}°` : "없음");
// ① 바닥판 — 두 측벽 사이를 잇는 판. 각 측 바깥 연장은 측벽 부재로 함께 그린다.
layer.append(
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);
});
}
shapes.push(shape);
layer.append(shape);
}
walls.set(side.role, shapes);
}
// ③ 관 — 바닥판 상면에 안힌다. 단면엔 1개만, 련수는 라벨로 알린다.
layer.append(
polygon(
layout.pipe.outer,
"b06-chart__culvert-pipe b06-chart__culvert-pipe--outer",
`${layout.label.text} — 관 밑이 바닥판 상면`,
),
polygon(layout.pipe.inner, "b06-chart__culvert-pipe", layout.label.text),
);
// ④ 성토(노체) 채움 — 관 위 ~ 노면 아래, 두 측벽 도로측 면 사이.
layer.append(polygon(layout.fill, "b06-chart__ford-fill", "세월교 관 위 성토(노체) 채움"));
// ⑤ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계).
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.from, side.cutLine.to], `${role} 측벽 절토선(계획선)`);
}
}
// 관·채움이 벽 위에 겹쳐 클릭을 가로챈다 — 투명 겹면을 맨 위에 얹어 벽을 고른다
// (배수관 기슭막이 `culvert-revet-hit`와 같은 수법).
if (onSelectWall) {
for (const side of layout.sides) {
const wall = side.parts.find((part) => part.kind === "wall");
if (!wall) continue;
const hit = polygon(wall.points, "b06-chart__culvert-revet-hit", "");
hit.addEventListener("click", (event) => {
event.stopPropagation();
onSelectWall(side.role);
});
layer.append(hit);
}
}
const label = document.createElementNS(SVG_NS, "text");
const [labelX, labelY] = toXY(layout.label.at);
label.setAttribute("x", String(labelX));
label.setAttribute("y", String(labelY));
label.setAttribute("text-anchor", "middle");
label.setAttribute("class", "b06-chart__culvert-label");
label.textContent = layout.label.text;
layer.append(label);
return (key: FordWallRole | null): void => {
for (const [role, shapes] of walls) {
for (const shape of shapes) shape.classList.toggle("is-active", role === key);
}
};
}