채움 색이 --color-chart-0(파랑 rgb 91,141,239)이라 관 선 색(rgb 79,142,247)과 겹쳐 관 일부처럼 읽혔다. 사용자 확정에 따라 면 자체를 지운다 — 배수관 세트와 같이 관 위는 비워 둔다. - FordLayout.fill·FordSideLayout.roadFace와 전용 보조 함수 제거 - .b06-chart__ford-fill 스타일 삭제 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 lines
6.6 KiB
TypeScript
172 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Ford.ts
|
|
* 세월교 측점 횡단 카드의 구체(측벽·바닥판·관·성토 채움) **오버레이 그리기**.
|
|
*
|
|
* 기하 계산은 `B06_Section_UI_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리).
|
|
* 여기서는 계산 결과(`FordLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
|
|
*
|
|
* 그리는 순서 = 시공 순서: ① 바닥판 → ② 측벽(ㄴ형 집수정 재사용) → ③ 관 →
|
|
* ④ 계류측 성토부선·절토선. 관 위 성토 채움은 그리지 않는다(2026-08-25 사용자).
|
|
* ========================================================================== */
|
|
|
|
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}m·${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),
|
|
);
|
|
|
|
// ④ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계).
|
|
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);
|
|
}
|
|
};
|
|
}
|