- 세월교 바닥판 · 관 판정면 추가 → 「세월교」 상자 강조 · 스크롤 · 그림 바닥판 · 관 붉게 - BOX 구체 가운데 1/3 = 본체 · 양쪽 = 그 끝(유입 · 유출 상자 그대로) - 고름 값에 body 추가(FordPick · BoxPick) · 조정창은 본체에서 안 뜸 - B05 넘김 키 body 도 같은 길 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
182 lines
7.4 KiB
TypeScript
182 lines
7.4 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Box.ts
|
||
* BOX암거 측점 횡단 카드의 구체(에이프런·저판·상판·내공·성토선) **오버레이 그리기**.
|
||
*
|
||
* 기하 계산은 `B06_Section_UI_Cross_Box_Geom.ts`가 맡는다(700줄 제한 분리).
|
||
* 여기서는 계산 결과(`BoxLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
|
||
*
|
||
* 그리는 순서 = 시공 순서: ① 에이프런 → ② 저판 → ③ 상판 → ④ 내공 윤곽 → ⑤ 성토선.
|
||
* 콘크리트 색은 세월교 바닥판 클래스를 그대로 쓴다.
|
||
* ========================================================================== */
|
||
|
||
import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
|
||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||
|
||
export {
|
||
computeBoxLayout,
|
||
DEFAULT_BOX_ADJUST,
|
||
DEFAULT_BOX_SIDE_ADJUST,
|
||
} from "./B06_Section_UI_Cross_Box_Geom";
|
||
export type {
|
||
BoxAdjust,
|
||
BoxLayout,
|
||
BoxSideAdjust,
|
||
BoxSideLayout,
|
||
BoxSideRole,
|
||
} from "./B06_Section_UI_Cross_Box_Geom";
|
||
|
||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||
|
||
/** 고르는 부재 — 좌 · 우 끝 또는 본체(구체 가운데 1/3 · 좌측 「본체」 상자 · 66-1). */
|
||
export type BoxPick = BoxSideRole | "body";
|
||
|
||
/** 선택 강조를 카드 재생성 없이 갈아 끼우는 setter. */
|
||
export type BoxHighlightSetter = (role: BoxPick | null) => void;
|
||
|
||
/**
|
||
* BOX암거 구체 오버레이 — computeBoxLayout 결과를 그린다.
|
||
* `onSelectSide`가 오면 좌·우 절반이 선택 대상이 된다(조정창이 그 측을 잡는다).
|
||
*/
|
||
export function appendBoxOverlay(
|
||
layer: SVGElement,
|
||
layout: BoxLayout,
|
||
x: (offset: number) => number,
|
||
toDisplayY: (elevation: number) => number,
|
||
onSelectSide?: (role: BoxPick) => void,
|
||
): BoxHighlightSetter {
|
||
const { box } = 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 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 slopeText = layout.slopeRatio
|
||
? ` · 구체 물매 1:${layout.slopeRatio.toFixed(1)}`
|
||
: " · 구체 수평";
|
||
|
||
// ① 에이프런 — 날개벽 구간 바닥. 각 끝 표고에서 수평이다(2026-08-25 사용자).
|
||
for (const side of layout.sides) {
|
||
if (!side.apron.length) continue;
|
||
const mark = side.role === "left" ? "좌" : "우";
|
||
layer.append(
|
||
polygon(
|
||
side.apron,
|
||
"b06-chart__ford-slab",
|
||
`${mark}측 날개벽 구간 바닥 — 수평 · 두께 ${box.slab_thickness_m.toFixed(2)}m` +
|
||
` (날개벽 ${wingText(side.role === "left" ? box.wing_in : box.wing_out)} 투영 연장)`,
|
||
),
|
||
);
|
||
}
|
||
|
||
// ② 저판 · ③ 상판 — 좌·우 표고가 다르면 함께 기운다.
|
||
layer.append(
|
||
polygon(
|
||
layout.bottomSlab,
|
||
"b06-chart__ford-slab",
|
||
`BOX암거 저판 — 두께 ${box.slab_thickness_m.toFixed(2)}m${slopeText}` +
|
||
(layout.bedGapM < -0.05
|
||
? ` · 계류 하상보다 ${Math.abs(layout.bedGapM).toFixed(2)}m 낮음(터파기)`
|
||
: layout.bedGapM > 0.05
|
||
? ` · 계류 하상보다 ${layout.bedGapM.toFixed(2)}m 높음`
|
||
: ""),
|
||
),
|
||
polygon(
|
||
layout.topSlab,
|
||
"b06-chart__ford-slab",
|
||
`BOX암거 상판 — 두께 ${box.top_thickness_m.toFixed(2)}m · 윗면이 노면 −${box.cover_m.toFixed(2)}m(복토)${slopeText}`,
|
||
),
|
||
);
|
||
|
||
// ④ 내공(유로) — 면은 비우고 윤곽만 점선으로 두른다.
|
||
layer.append(
|
||
polygon(
|
||
layout.barrel,
|
||
"b06-chart__box-barrel",
|
||
`BOX암거 내공(유로) ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}m` +
|
||
` · 측벽 두께 ${box.wall_thickness_m.toFixed(2)}m · 구체 길이 ${layout.bodyLengthM.toFixed(2)}m`,
|
||
),
|
||
);
|
||
|
||
// ⑤ 성토선 — 노견에서 (연장 후) 구체 최상단 모서리까지, 물매 1:1.2.
|
||
for (const side of layout.sides) {
|
||
if (side.fillLine.length < 2) continue;
|
||
const polyline = document.createElementNS(SVG_NS, "polyline");
|
||
polyline.setAttribute("points", side.fillLine.map((point) => toXY(point).join(",")).join(" "));
|
||
polyline.setAttribute("class", "b06-chart__design-cross");
|
||
const title = document.createElementNS(SVG_NS, "title");
|
||
const drop = side.fillLine[0].elevation - side.fillLine[side.fillLine.length - 1].elevation;
|
||
title.textContent = `BOX암거 성토선 — 물매 1:1.2 · 성토고 ${drop.toFixed(2)}m (구체 최상단에서 끝)`;
|
||
polyline.append(title);
|
||
layer.append(polyline);
|
||
}
|
||
|
||
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);
|
||
|
||
// 투명 겹면 — 구체 가운데 1/3 = 본체, 그 바깥 양쪽 = 그 끝(66-1). 어느 것을 조정할지 클릭으로 고른다.
|
||
const hits = new Map<BoxPick, SVGPolygonElement>();
|
||
if (onSelectSide) {
|
||
const midOffset = (layout.sides[0].offset + layout.sides[1].offset) / 2;
|
||
const third = (layout.sides[0].offset - layout.sides[1].offset) / 6;
|
||
const top = Math.max(...layout.topSlab.map((point) => point.elevation));
|
||
const bottom = Math.min(...layout.bottomSlab.map((point) => point.elevation));
|
||
const band = (from: number, to: number): OffsetPoint[] => [
|
||
{ offset: from, elevation: top },
|
||
{ offset: to, elevation: top },
|
||
{ offset: to, elevation: bottom },
|
||
{ offset: from, elevation: bottom },
|
||
];
|
||
const parts: Array<[BoxPick, OffsetPoint[]]> = [
|
||
["body", band(midOffset - third, midOffset + third)],
|
||
...layout.sides.map((side): [BoxPick, OffsetPoint[]] => {
|
||
const outer = side.apron.length ? side.apron[1].offset : side.offset;
|
||
const inner = midOffset + Math.sign(side.offset - midOffset) * Math.abs(third);
|
||
return [side.role, band(inner, outer)];
|
||
}),
|
||
];
|
||
for (const [role, points] of parts) {
|
||
const hit = polygon(
|
||
points,
|
||
// 강조용 클래스를 하나 더 단다 — 고른 끝을 옅게 칠한다(2026-08-30 사용자:
|
||
// BOX암거는 선택해도 아무 표시가 없었다). 구체 부재는 좌·우가 한 몸이라
|
||
// 그쪽을 칠하면 어느 끝을 고른 것인지 되레 흐려진다.
|
||
"b06-chart__culvert-revet-hit b06-chart__box-hit",
|
||
"",
|
||
);
|
||
hit.addEventListener("click", (event) => {
|
||
event.stopPropagation();
|
||
onSelectSide(role);
|
||
});
|
||
hits.set(role, hit);
|
||
layer.append(hit);
|
||
}
|
||
}
|
||
|
||
return (role: BoxPick | null): void => {
|
||
for (const [side, hit] of hits) hit.classList.toggle("is-active", side === role);
|
||
};
|
||
}
|