Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Design.ts
T
2026-07-21 20:12:44 +09:00

156 lines
5.7 KiB
TypeScript

/* =============================================================================
* B06_wf3_ProfileCross_UI_Cross_Design.ts
* 측점 표준횡단 설계 지정 컨트롤(지반유형·단면유형·측구위치)과 설계선 오버레이.
*
* 카드 헤더 아래에 세그먼트 버튼을 배치하고, 지반유형·단면유형이 모두 선택되면
* onChange로 계산을 요청한다. 계산 결과(section.design)는 상위에서 다시 렌더될 때
* 절·성토 단면적 표시와 설계선 오버레이로 반영된다. 편절편성은 측구위치가 자동
* 결정되어 컨트롤을 숨기고, 양절·양성에서만 배수 방향 선택을 노출한다.
* ========================================================================== */
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import type {
CrossDesign,
CrossSection,
DitchSide,
GroundType,
SectionMode,
} from "./B06_wf3_ProfileCross_Api_Fetch";
const SVG_NS = "http://www.w3.org/2000/svg";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
const GROUND_OPTIONS: Array<[GroundType, keyof typeof ui_locales]> = [
["soil", "B06_Design_Ground_Soil"],
["ripping_rock", "B06_Design_Ground_Ripping"],
["blasting_rock", "B06_Design_Ground_Blasting"],
];
const MODE_OPTIONS: Array<[SectionMode, keyof typeof ui_locales]> = [
["left_cut", "B06_Design_Mode_LeftCut"],
["right_cut", "B06_Design_Mode_RightCut"],
["both_cut", "B06_Design_Mode_BothCut"],
["both_fill", "B06_Design_Mode_BothFill"],
];
const DITCH_OPTIONS: Array<[DitchSide, keyof typeof ui_locales]> = [
["left", "B06_Design_Ditch_Left"],
["right", "B06_Design_Ditch_Right"],
];
export interface CrossDesignChange {
ground_type: GroundType;
section_mode: SectionMode;
ditch_side: DitchSide | null;
}
function segment<T extends string>(
legend: string,
options: Array<[T, keyof typeof ui_locales]>,
selected: T | null,
onPick: (value: T) => void,
): HTMLElement {
const wrap = document.createElement("div");
wrap.className = "b06-design__seg";
const legendEl = document.createElement("span");
legendEl.className = "b06-design__seg-legend";
legendEl.textContent = legend;
wrap.append(legendEl);
const group = document.createElement("div");
group.className = "b06-design__seg-buttons";
for (const [value, labelKey] of options) {
const button = document.createElement("button");
button.type = "button";
button.className = `b06-design__btn${value === selected ? " b06-design__btn--active" : ""}`;
button.textContent = L(labelKey);
button.setAttribute("aria-pressed", value === selected ? "true" : "false");
button.addEventListener("click", () => onPick(value));
group.append(button);
}
wrap.append(group);
return wrap;
}
/** 카드 헤더용 설계 지정 컨트롤 바를 만든다. */
export function buildDesignControls(
section: CrossSection,
onChange: (chainageM: number, change: CrossDesignChange) => void,
): HTMLElement {
const design = section.design;
// 기본값: 토사(soil) + 좌절토(left_cut). 미지정 측점은 확정 시 이 기본값으로 채워진다.
const state: { ground: GroundType; mode: SectionMode; ditch: DitchSide | null } = {
ground: design?.ground_type ?? "soil",
mode: design?.section_mode ?? "left_cut",
ditch: design?.ditch_side ?? null,
};
const bar = document.createElement("div");
bar.className = "b06-design";
// 컨트롤 상호작용이 카드 선택 클릭으로 전파되지 않게 한다.
bar.addEventListener("click", (event) => event.stopPropagation());
const needsDitch = (): boolean => state.mode === "both_cut" || state.mode === "both_fill";
const emit = (): void => {
if (!state.ground || !state.mode) return;
onChange(section.chainage_m, {
ground_type: state.ground,
section_mode: state.mode,
ditch_side: needsDitch() ? (state.ditch ?? "left") : null,
});
};
bar.append(
segment(L("B06_Design_Ground_Legend"), GROUND_OPTIONS, state.ground, (value) => {
state.ground = value;
emit();
}),
segment(L("B06_Design_Mode_Legend"), MODE_OPTIONS, state.mode, (value) => {
state.mode = value;
emit();
}),
);
if (needsDitch()) {
bar.append(
segment(L("B06_Design_Ditch_Legend"), DITCH_OPTIONS, state.ditch, (value) => {
state.ditch = value;
emit();
}),
);
}
const readout = document.createElement("div");
readout.className = "b06-design__areas";
if (design) {
const cut = document.createElement("span");
cut.className = "b06-design__area b06-design__area--cut";
cut.textContent = `${L("B06_Design_Cut_Area")} ${design.cut_area_m2.toFixed(2)}㎡`;
const fill = document.createElement("span");
fill.className = "b06-design__area b06-design__area--fill";
fill.textContent = `${L("B06_Design_Fill_Area")} ${design.fill_area_m2.toFixed(2)}㎡`;
readout.append(cut, fill);
} else {
const unset = document.createElement("span");
unset.className = "b06-design__area b06-design__area--unset";
unset.textContent = L("B06_Design_Unset");
readout.append(unset);
}
bar.append(readout);
return bar;
}
/** 횡단 SVG에 표준단면 설계선을 겹쳐 그린다. */
export function appendCrossDesignOverlay(
svg: SVGSVGElement,
design: CrossDesign,
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
): void {
const line = design.design_line;
if (!line || line.length < 2) return;
const points = line.map((point) => `${x(point.offset_m)},${toDisplayY(point.elevation_m)}`);
const polyline = document.createElementNS(SVG_NS, "polyline");
polyline.setAttribute("points", points.join(" "));
polyline.setAttribute("class", "b06-chart__design-cross");
svg.append(polyline);
}