- 새 D03_PlanAnalysis_UI_DrawPanel.ts — createDrawKindFields(노드 판 598-4 가 다시 씀) · 그리기 칸 몸 - 「둘 다」 → 「길이·기울기 고정」 · 칸 설명 글 걷음(요구 463-6) - 부품 마디 고름에 columns 덧(여러 줄 꼴) — 한 줄 꼴 그대로 - 지도 도구줄 연도 마커 단추 · 공용 표 줄 걷음 → 그리기 칸 끌기 단추 - 시험 553_4 · 568_1 고침(마커 자리 옮김 · 방식 칸 새 파일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_DrawPanel.ts
|
|
* D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — [긋기] · 연도 구역 마커 · 긋기 방식 칸.
|
|
*
|
|
* 방식 칸 `createDrawKindFields` = 마디 고름 2 × 2(자유 · 길이 고정 · 기울기 고정 · 길이·기울기 고정) +
|
|
* 길이 · 기울기 두 열(기울기 아래 ≈ °) — 그리기 칸 · 노드 판(598-4)이 같은 부품을 씀.
|
|
* 연도 구역 마커 = 끌어 선 위에 놓기(`YEAR_MARK_DRAG` · 받음 = `D03_PlanAnalysis_UI_Edit` drop).
|
|
* 설명 글 없음(요구 463-6) — 칸 오류 · ≈ ° 값만.
|
|
* ========================================================================== */
|
|
|
|
import type { UiSize } from "@ui/ui_template_button";
|
|
import { createButton } from "@ui/ui_template_elements";
|
|
import { createInputField } from "@ui/ui_template_input";
|
|
import { createSegment } from "@ui/ui_template_segment";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { fmt, slopeDeg } from "./D03_PlanAnalysis_UI_DrawPaint";
|
|
import { YEAR_MARK_DRAG } from "./D03_PlanAnalysis_UI_Map";
|
|
|
|
/** 제약 칸 처음 값 — 길이 = 측점 간격(지식DB 설계제원_총괄) · 기울기 = 간선 20 km/h 일반지형 상한(종단선형). */
|
|
export const DRAW_DEFAULT_LENGTH_M = 20;
|
|
export const DRAW_DEFAULT_SLOPE_PCT = 9;
|
|
|
|
export type DrawConstraint = "free" | "length" | "slope" | "both";
|
|
|
|
export interface DrawKindValue {
|
|
kind: DrawConstraint;
|
|
lengthM: number;
|
|
slopePct: number;
|
|
}
|
|
|
|
export const DRAW_KIND_LABELS: Record<DrawConstraint, string> = {
|
|
free: "자유",
|
|
length: "길이 고정",
|
|
slope: "기울기 고정",
|
|
both: "길이·기울기 고정",
|
|
};
|
|
|
|
export interface DrawKindFields {
|
|
root: HTMLElement;
|
|
get: () => DrawKindValue;
|
|
/** 값을 바꿈(onChange 는 안 부름). */
|
|
set: (value: Partial<DrawKindValue>) => void;
|
|
setDisabled: (disabled: boolean) => void;
|
|
}
|
|
|
|
/** 긋기 방식 칸 — 고름 · 길이 · 기울기가 바뀔 때마다 `onChange(지금 값)`(빈 · 틀린 수는 칸 오류만). */
|
|
export function createDrawKindFields(opts: {
|
|
value: DrawKindValue;
|
|
onChange: (value: DrawKindValue) => void;
|
|
size?: UiSize;
|
|
}): DrawKindFields {
|
|
const size = opts.size ?? 24;
|
|
let value = { ...opts.value };
|
|
let disabled = false;
|
|
const changed = (): void => opts.onChange({ ...value });
|
|
const segment = createSegment({
|
|
options: (Object.keys(DRAW_KIND_LABELS) as DrawConstraint[]).map((kind) => ({
|
|
value: kind,
|
|
label: DRAW_KIND_LABELS[kind],
|
|
})),
|
|
value: value.kind,
|
|
size,
|
|
columns: 2,
|
|
ariaLabel: "긋기 방식",
|
|
onChange: (next) => {
|
|
value.kind = next as DrawConstraint;
|
|
fieldsState();
|
|
changed();
|
|
},
|
|
});
|
|
segment.root.dataset.role = "draw-constraint";
|
|
const lengthField = createInputField({
|
|
label: "길이",
|
|
type: "number",
|
|
value: String(value.lengthM),
|
|
min: 1,
|
|
unit: "m",
|
|
size,
|
|
onInput: (text) => {
|
|
const next = Number(text);
|
|
lengthField.setError(next > 0 ? undefined : "0 보다 큰 수");
|
|
if (!(next > 0)) return;
|
|
value.lengthM = next;
|
|
changed();
|
|
},
|
|
});
|
|
lengthField.input.dataset.role = "draw-length";
|
|
const slopeField = createInputField({
|
|
label: "기울기",
|
|
type: "number",
|
|
value: String(value.slopePct),
|
|
unit: "%",
|
|
size,
|
|
onInput: (text) => {
|
|
const next = Number(text);
|
|
const ok = text.trim() !== "" && Number.isFinite(next);
|
|
slopeField.setError(ok ? undefined : "숫자");
|
|
if (!ok) return;
|
|
value.slopePct = next;
|
|
degText();
|
|
changed();
|
|
},
|
|
});
|
|
slopeField.input.dataset.role = "draw-slope";
|
|
const slopeDegText = createText("", "muted");
|
|
slopeDegText.dataset.role = "draw-slope-deg";
|
|
const degText = (): void => {
|
|
slopeDegText.textContent = `≈ ${fmt(slopeDeg(Math.abs(value.slopePct)))}°`;
|
|
};
|
|
const slopeColumn = document.createElement("div");
|
|
slopeColumn.append(slopeField.root, slopeDegText);
|
|
const fields = document.createElement("div");
|
|
fields.className = "d03-plan__draw-fields";
|
|
fields.append(lengthField.root, slopeColumn);
|
|
const root = document.createElement("div");
|
|
root.className = "d03-plan__draw-kind";
|
|
root.append(segment.root, fields);
|
|
|
|
function fieldsState(): void {
|
|
for (const button of segment.root.querySelectorAll("button")) button.disabled = disabled;
|
|
lengthField.input.disabled = disabled || value.kind === "free" || value.kind === "slope";
|
|
slopeField.input.disabled = disabled || value.kind === "free" || value.kind === "length";
|
|
}
|
|
degText();
|
|
fieldsState();
|
|
|
|
return {
|
|
root,
|
|
get: () => ({ ...value }),
|
|
set(next) {
|
|
value = { ...value, ...next };
|
|
segment.set(value.kind);
|
|
lengthField.input.value = String(value.lengthM);
|
|
slopeField.input.value = String(value.slopePct);
|
|
lengthField.setError(undefined);
|
|
slopeField.setError(undefined);
|
|
degText();
|
|
fieldsState();
|
|
},
|
|
setDisabled(next) {
|
|
disabled = next;
|
|
fieldsState();
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface DrawPanel {
|
|
root: HTMLElement;
|
|
/** 단추 글 · 막힘 — 긋는 중 · 고칠 수 있음 · 마커 놓을 선 있음. */
|
|
state: (state: { drawing: boolean; editable: boolean; canMark: boolean }) => void;
|
|
}
|
|
|
|
/** 좌측 「그리기」 칸 — [긋기] · 연도 구역 마커(끌기) · 방식 칸. */
|
|
export function createDrawPanel(opts: { kind: DrawKindFields; onDraw: () => void }): DrawPanel {
|
|
const drawButton = createButton({
|
|
label: "긋기",
|
|
variant: "filled",
|
|
size: 28,
|
|
onClick: opts.onDraw,
|
|
});
|
|
const markButton = createButton({
|
|
label: "연도 구역 마커",
|
|
variant: "ghost",
|
|
size: 28,
|
|
iconStart: "flag",
|
|
});
|
|
markButton.title = "선 위로 끌어 놓으면 구역을 나눕니다";
|
|
markButton.dataset.role = "draw-year-mark";
|
|
markButton.draggable = true;
|
|
markButton.addEventListener("dragstart", (event) => {
|
|
event.dataTransfer?.setData(YEAR_MARK_DRAG, "1");
|
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy";
|
|
});
|
|
const row = document.createElement("div");
|
|
row.className = "d03-plan__row";
|
|
row.append(drawButton, markButton);
|
|
const root = document.createElement("div");
|
|
root.className = "d03-plan__draw";
|
|
root.append(row, opts.kind.root);
|
|
return {
|
|
root,
|
|
state({ drawing, editable, canMark }) {
|
|
drawButton.querySelector(".ui-btn__label")!.textContent = drawing ? "긋기 끝" : "긋기";
|
|
drawButton.disabled = !editable;
|
|
drawButton.setAttribute("aria-pressed", String(drawing));
|
|
markButton.disabled = !editable || drawing || !canMark;
|
|
root.dataset.mode = drawing ? "draw" : "idle";
|
|
},
|
|
};
|
|
}
|