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>
175 lines
6.8 KiB
TypeScript
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}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),
|
|
);
|
|
|
|
// ④ 성토(노체) 채움 — 관 위 ~ 노면 아래, 두 측벽 도로측 면 사이.
|
|
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);
|
|
}
|
|
};
|
|
}
|