Files
Aislo/B06_Section/B06_Section_UI_Cross_Box.ts
T
eomsangdonandClaude Opus 5.5 5c0c8fd9c4 fix(B06): 횡단 세월교 · BOX 본체 클릭 = 좌측 본체 상자 고름 · 강조(PLAN 66-1)
- 세월교 바닥판 · 관 판정면 추가 → 「세월교」 상자 강조 · 스크롤 · 그림 바닥판 · 관 붉게
- 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
2026-10-06 23:36:17 +09:00

182 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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);
};
}