좌·우 끝을 골라도 횡단도에 아무 표시가 없었다. 강조 토글(`is-active`)은 이미 클릭 판정용 **투명 겹면**에 걸리고 있었는데, 그 클래스에는 강조 규칙이 없어 그대로 투명했다. 겹면에 `b06-chart__box-hit`를 하나 더 달고 그 클래스에만 강조를 준다 — 기슭막이· 집수정과 같은 색 계열이되 면이 넓어 채도를 낮췄다(danger 16% + 점선 테두리). 구체 부재(저판·상판·내공)는 좌·우가 한 몸이라 그쪽을 칠하면 어느 끝을 고른 것인지 되레 흐려진다. 자체검증(공용 브라우저 5174, 측점 10+0.9 BOX암거): - tsc --noEmit 통과. - 우측 끝 선택 → 그 겹면만 `is-active`, 계산 스타일 fill `color(srgb 0.909804 0.411765 0.352941 / 0.16)` · stroke `rgb(232, 105, 90)`. 수정 전에는 `fill: transparent`라 아무것도 안 보였다. - 스크린샷 `tmp/browser/shots/110_boxhl.png` — 구체 우측 절반이 붉게 칠해진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
6.8 KiB
TypeScript
170 lines
6.8 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";
|
||
|
||
/** 선택 강조를 카드 재생성 없이 갈아 끼우는 setter. */
|
||
export type BoxHighlightSetter = (role: BoxSideRole | null) => void;
|
||
|
||
/**
|
||
* BOX암거 구체 오버레이 — computeBoxLayout 결과를 그린다.
|
||
* `onSelectSide`가 오면 좌·우 절반이 선택 대상이 된다(조정창이 그 측을 잡는다).
|
||
*/
|
||
export function appendBoxOverlay(
|
||
layer: SVGElement,
|
||
layout: BoxLayout,
|
||
x: (offset: number) => number,
|
||
toDisplayY: (elevation: number) => number,
|
||
onSelectSide?: (role: BoxSideRole) => 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);
|
||
|
||
// 좌·우 절반을 덮는 투명 겹면 — 어느 끝을 조정할지 클릭으로 고른다.
|
||
const hits = new Map<BoxSideRole, SVGPolygonElement>();
|
||
if (onSelectSide) {
|
||
const midOffset = (layout.sides[0].offset + layout.sides[1].offset) / 2;
|
||
const top = Math.max(...layout.topSlab.map((point) => point.elevation));
|
||
const bottom = Math.min(...layout.bottomSlab.map((point) => point.elevation));
|
||
for (const side of layout.sides) {
|
||
const outer = side.apron.length ? side.apron[1].offset : side.offset;
|
||
const hit = polygon(
|
||
[
|
||
{ offset: midOffset, elevation: top },
|
||
{ offset: outer, elevation: top },
|
||
{ offset: outer, elevation: bottom },
|
||
{ offset: midOffset, elevation: bottom },
|
||
],
|
||
// 강조용 클래스를 하나 더 단다 — 고른 끝을 옅게 칠한다(2026-08-30 사용자:
|
||
// BOX암거는 선택해도 아무 표시가 없었다). 구체 부재는 좌·우가 한 몸이라
|
||
// 그쪽을 칠하면 어느 끝을 고른 것인지 되레 흐려진다.
|
||
"b06-chart__culvert-revet-hit b06-chart__box-hit",
|
||
"",
|
||
);
|
||
hit.addEventListener("click", (event) => {
|
||
event.stopPropagation();
|
||
onSelectSide(side.role);
|
||
});
|
||
hits.set(side.role, hit);
|
||
layer.append(hit);
|
||
}
|
||
}
|
||
|
||
return (role: BoxSideRole | null): void => {
|
||
for (const [side, hit] of hits) hit.classList.toggle("is-active", side === role);
|
||
};
|
||
}
|