refactor(B05 평면 · 지도 공용): RouteEdit 마우스 배선 · 재기 한 벌 떼어냄 · 긋기 방식 칸 공용 자리로 (PLAN 626-3 · 요구 530 · 531)
- B05_Profile_UI_RouteEdit.ts 684 → 655 줄 — 두 번 클릭 삽입 · 오른쪽 클릭 삭제 · 지도 이동 · 휠 = 새 `_RouteEdit_Mouse.ts` · 재기 도구 + 단추 한 벌 = `_Measure.ts` `createRouteMeasure` (동작 그대로) - 긋기 방식 칸(`createDrawKindFields` · 기본값 · 이름) = 새 `A00_Common/map/map_draw_kind.ts` · `.css` — D03 DrawPanel 은 다시 내보냄(D03 화면 · 동작 같음 · 칸 꼴 선택자만 공용 이름) - D03 `paintPreview` · `planLabel` 받는 지도 꼴을 쓰는 칸만(Pick)으로 — B05 평면이 그대로 부름 - 시험 test_553_4 — 방식 칸 대조를 공용 파일로 옮김(낡은 자리 대조) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/* 공용 긋기 방식 칸(626-3 · `map_draw_kind.ts`) — 고름 아래 길이 · 기울기 두 열 */
|
||||
.map-draw-kind {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.map-draw-kind__fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.map-draw-kind__fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.map-draw-kind__fields .ui-field {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.map-draw-kind__fields .ui-input-unit,
|
||||
.map-draw-kind__fields .ui-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/* =============================================================================
|
||||
* map_draw_kind.ts
|
||||
* 공용 긋기 방식 칸(PLAN 626-3 · 599-1 · 625-1) — 예정노선(D03) 그리기 칸 · 노드 판 · B05 평면 예정노선 그리기가 같이 씀.
|
||||
* 꼴 = 마디 고름 2 × 2(자유지정 · 길이·기울기 고정 / 길이 고정 · 기울기 고정) + 길이 · 기울기 두 열(기울기 아래 ≈ °) ·
|
||||
* 방식에 안 쓰는 칸은 숨김(막힘 = 권한 없음만) · 설명 글 없음(요구 463-6).
|
||||
* 쓰는 법 = `createDrawKindFields({ value, onChange })` → { root, get, set, setDisabled }.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { UiSize } from "@ui/ui_template_button";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import "./map_draw_kind.css";
|
||||
|
||||
/** 기울기 % → 각(°) 한 자리 글. */
|
||||
const slopeDegText1 = (pct: number): string => ((Math.atan(pct / 100) * 180) / Math.PI).toFixed(1);
|
||||
|
||||
/** 제약 칸 처음 값 — 길이 = 측점 간격(지식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: "자유지정",
|
||||
both: "길이·기울기 고정",
|
||||
length: "길이 고정",
|
||||
slope: "기울기 고정",
|
||||
};
|
||||
|
||||
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 = `≈ ${slopeDegText1(Math.abs(value.slopePct))}°`;
|
||||
};
|
||||
const slopeColumn = document.createElement("div");
|
||||
slopeColumn.append(slopeField.root, slopeDegText);
|
||||
const fields = document.createElement("div");
|
||||
fields.className = "map-draw-kind__fields";
|
||||
fields.append(lengthField.root, slopeColumn);
|
||||
const root = document.createElement("div");
|
||||
root.className = "map-draw-kind";
|
||||
root.append(segment.root, fields);
|
||||
|
||||
function fieldsState(): void {
|
||||
for (const button of segment.root.querySelectorAll("button")) button.disabled = disabled;
|
||||
lengthField.input.disabled = slopeField.input.disabled = disabled;
|
||||
fields.hidden = value.kind === "free";
|
||||
lengthField.root.hidden = value.kind === "slope";
|
||||
slopeColumn.hidden = 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
type ViewState,
|
||||
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
bindMinorContourToggle,
|
||||
createSheetFollow,
|
||||
@@ -41,12 +40,12 @@ import {
|
||||
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
||||
import type { EditedCurve, EditedNode } from "./logic/B05_Profile_UI_RouteEdit_Curve";
|
||||
import { drawRouteEditScene, routeHeadText, watchTheme } from "./B05_Profile_UI_RouteEdit_Render";
|
||||
import { bindNodeSplice, bindRouteEditNavigation } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import { bindRouteEditMouse } from "./B05_Profile_UI_RouteEdit_Mouse";
|
||||
import { bindRouteEditPointer } from "./B05_Profile_UI_RouteEdit_Pointer";
|
||||
import { createCrossPreview } from "./B05_Profile_UI_RouteEdit_Cross";
|
||||
import { createMapRotation } from "./B05_Profile_UI_RouteEdit_Rotate";
|
||||
import { createRouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome";
|
||||
import { bindMeasureControls, createMeasureTool } from "./B05_Profile_UI_RouteEdit_Measure";
|
||||
import { createRouteMeasure } from "./B05_Profile_UI_RouteEdit_Measure";
|
||||
import { createSlopeLabels } from "./B05_Profile_UI_RouteEdit_Slope";
|
||||
import {
|
||||
captureView,
|
||||
@@ -191,22 +190,13 @@ export async function mountRouteEditor(
|
||||
// 횡단이 뜨면 그 측점 반폭 선을 그린다 — 새로 고침 뒤 고른 측점은 만드는 도중 불려 아래 선언이 아직이라 한 박자 뒤에(505-4).
|
||||
onShown: () => queueMicrotask(draw),
|
||||
});
|
||||
/** 구간 재기 — 계획·예상 노선 위 두 점. 셈·서버 묻기는 `_Measure` 몫(계획서 0-9 ⑤). */
|
||||
const measure = createMeasureTool({
|
||||
/** 구간 재기 — 계획·예상 노선 위 두 점 · 재기 단추·작은 창. 셈·서버 묻기 · 배선은 `_Measure` 몫(계획서 0-9 ⑤). */
|
||||
const { measure, measureControls } = createRouteMeasure({
|
||||
projectId,
|
||||
stationIntervalM,
|
||||
lines: () => ({ planned: plannedLine.length ? plannedLine : planned, expected }),
|
||||
toScreen: (vertex) => toScreen(vertex),
|
||||
isClosed: () => closed,
|
||||
onChange: () => {
|
||||
measureControls.sync();
|
||||
draw();
|
||||
},
|
||||
onHint: () => measureControls.sync(),
|
||||
});
|
||||
/** 재기 단추·작은 창 — 배선은 `_Measure` 몫. */
|
||||
const measureControls = bindMeasureControls({
|
||||
measure,
|
||||
box: measureBox,
|
||||
text: measureText,
|
||||
button: measureButton,
|
||||
@@ -547,37 +537,18 @@ export async function mountRouteEditor(
|
||||
offStation,
|
||||
});
|
||||
|
||||
bindNodeSplice({
|
||||
canvas,
|
||||
unrotate: rotation.unrotate,
|
||||
segmentAt: pointer.segmentAt,
|
||||
nodeAt: pointer.nodeAt,
|
||||
insert: (segment, px, py) => {
|
||||
edits.spliceNode(snapshotNow(), segment + 1, toMetric(px, py)); // 편집값도 같은 자리에 끼운다.
|
||||
picked = segment + 1;
|
||||
applyEdit("새 노드를 넣었습니다(직선 추가).");
|
||||
},
|
||||
remove: (index) => {
|
||||
if (planned.length <= 2) return showToast("노선은 노드가 2개 이상이어야 합니다.", "error");
|
||||
if (apexLock[index])
|
||||
return showToast("고정한 점은 못 지웁니다 — 고정을 먼저 푸세요.", "error");
|
||||
edits.spliceNode(snapshotNow(), index);
|
||||
picked = -1;
|
||||
applyEdit("노드를 지웠습니다(직선 삭제).");
|
||||
},
|
||||
});
|
||||
|
||||
bindRouteEditNavigation({
|
||||
// 두 번 클릭 삽입 · 오른쪽 클릭 삭제 · 지도 이동 · 확대 · 돌리기 — 배선은 `_Mouse` 몫.
|
||||
bindRouteEditMouse({
|
||||
canvas,
|
||||
rotation,
|
||||
pointer,
|
||||
toMetric,
|
||||
getView: () => view,
|
||||
setView: (next) => {
|
||||
view = next;
|
||||
},
|
||||
setView: (next) => (view = next),
|
||||
getMeta: () => meta,
|
||||
// 돌린 지도에서는 손이 민 방향과 그림이 움직일 방향이 다르다 — 거꾸로 돌려 넘긴다.
|
||||
unrotateDelta: rotation.unrotateDelta,
|
||||
rotateWheel: rotation.wheel, // Ctrl+휠 = 5° 돌리기(390-3)
|
||||
claimed: pointer.claimed, // 왼쪽 옮기기 = 노드 · 손잡이 · 마커 · 재기 아닌 누름만(588-4)
|
||||
snapshot: snapshotNow,
|
||||
setPicked: (index) => (picked = index),
|
||||
applyEdit: (message) => applyEdit(message),
|
||||
draw,
|
||||
});
|
||||
|
||||
|
||||
@@ -183,6 +183,22 @@ export function bindMeasureControls(params: MeasureControlsParams): {
|
||||
return { sync, mode: () => mode };
|
||||
}
|
||||
|
||||
/** 재기 도구 + 단추 · 작은 창 한 벌(626-3 · `_RouteEdit` 에서 뗌) — 상태 바뀜 = 창 맞춤 · 다시 그림 · 미리보기 = 창 글만. */
|
||||
export function createRouteMeasure(
|
||||
params: Omit<MeasureToolParams, "onChange" | "onHint"> & Omit<MeasureControlsParams, "measure">,
|
||||
): { measure: MeasureTool; measureControls: ReturnType<typeof bindMeasureControls> } {
|
||||
const measure = createMeasureTool({
|
||||
...params,
|
||||
onChange: () => {
|
||||
measureControls.sync();
|
||||
params.redraw();
|
||||
},
|
||||
onHint: () => measureControls.sync(),
|
||||
});
|
||||
const measureControls = bindMeasureControls({ ...params, measure });
|
||||
return { measure, measureControls };
|
||||
}
|
||||
|
||||
export function createMeasureTool(params: MeasureToolParams): MeasureTool {
|
||||
/** 찍은 두 점. 셋째를 찍으면 새 구간의 시작이 된다. */
|
||||
let picked: MeasurePoint[] = [];
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Mouse.ts
|
||||
* 계획노선 편집(평면) 마우스 배선 — 선 두 번 클릭 = 노드 넣기 · 노드 오른쪽 클릭 = 지움 · 가운데(휠) 버튼 끌기 =
|
||||
* 지도 이동 · 휠 = 확대 · Ctrl+휠 = 돌리기. `B05_Profile_UI_RouteEdit.ts` 에서 뗌(626-3 · 동작 그대로).
|
||||
* 노드 끌기 · 집기는 `_Pointer` 몫 · 집기 셈 · 듣기는 `_Input` 몫.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { bindNodeSplice, bindRouteEditNavigation } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import type { RouteEditSnapshot } from "./B05_Profile_UI_RouteEdit_History";
|
||||
import type { MapRotation } from "./B05_Profile_UI_RouteEdit_Rotate";
|
||||
import * as edits from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
export interface RouteEditMouseParams {
|
||||
canvas: HTMLCanvasElement;
|
||||
rotation: Pick<MapRotation, "unrotate" | "unrotateDelta" | "wheel">;
|
||||
pointer: {
|
||||
segmentAt: (px: number, py: number) => number;
|
||||
nodeAt: (px: number, py: number) => number;
|
||||
claimed: () => boolean;
|
||||
};
|
||||
toMetric: (px: number, py: number) => Vertex;
|
||||
getView: () => ViewState;
|
||||
setView: (next: ViewState) => void;
|
||||
getMeta: () => VWorldMeta | null;
|
||||
/** 지금 편집값(같은 배열 — 넣고 빼면 그대로 바뀜). */
|
||||
snapshot: () => RouteEditSnapshot;
|
||||
setPicked: (index: number) => void;
|
||||
applyEdit: (message: string) => void;
|
||||
draw: () => void;
|
||||
}
|
||||
|
||||
export function bindRouteEditMouse(params: RouteEditMouseParams): void {
|
||||
const { canvas, rotation, pointer } = params;
|
||||
bindNodeSplice({
|
||||
canvas,
|
||||
unrotate: rotation.unrotate,
|
||||
segmentAt: pointer.segmentAt,
|
||||
nodeAt: pointer.nodeAt,
|
||||
insert: (segment, px, py) => {
|
||||
edits.spliceNode(params.snapshot(), segment + 1, params.toMetric(px, py)); // 편집값도 같은 자리에 끼운다.
|
||||
params.setPicked(segment + 1);
|
||||
params.applyEdit("새 노드를 넣었습니다(직선 추가).");
|
||||
},
|
||||
remove: (index) => {
|
||||
const snapshot = params.snapshot();
|
||||
if (snapshot.planned.length <= 2)
|
||||
return showToast("노선은 노드가 2개 이상이어야 합니다.", "error");
|
||||
if (snapshot.apexLock[index])
|
||||
return showToast("고정한 점은 못 지웁니다 — 고정을 먼저 푸세요.", "error");
|
||||
edits.spliceNode(snapshot, index);
|
||||
params.setPicked(-1);
|
||||
params.applyEdit("노드를 지웠습니다(직선 삭제).");
|
||||
},
|
||||
});
|
||||
|
||||
bindRouteEditNavigation({
|
||||
canvas,
|
||||
getView: params.getView,
|
||||
setView: params.setView,
|
||||
getMeta: params.getMeta,
|
||||
// 돌린 지도에서는 손이 민 방향과 그림이 움직일 방향이 다르다 — 거꾸로 돌려 넘긴다.
|
||||
unrotateDelta: rotation.unrotateDelta,
|
||||
rotateWheel: rotation.wheel, // Ctrl+휠 = 5° 돌리기(390-3)
|
||||
claimed: pointer.claimed, // 왼쪽 옮기기 = 노드 · 손잡이 · 마커 · 재기 아닌 누름만(588-4)
|
||||
draw: params.draw,
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* 미리보기(거리 · 가상 종단 기울기 % · °) · 집기 = 화면 px 둘레 선 위 자리.
|
||||
* 길이·기울기 고정 부채꼴(625-2) — 정방향 = 채움 · 역방향 = 테두리 · 노드 · 놓인 꺾임점 · 미리보기 고리 =
|
||||
* 기울기 부호 색(+ 오르막 `--draw-map-slope-up` 따뜻한 색 · − 내리막 `--draw-map-slope-down` 찬 색 · 막힘 = 경고 빨강).
|
||||
* `paintFan` = ScreenMap 의 `toScreen` 만 씀 — B05 평면(626-3)이 그대로 씀.
|
||||
* `paintFan` · `paintPreview` = ScreenMap 의 `toScreen` · `pxPerMeter` · `upright` 만 씀 — B05 평면(626-3)이 그대로 씀.
|
||||
* 지점 핀(585-1) = 따로 겹(`pointLayer` · 겹 목록 맨 끝 = 늘 맨 위) · 색 = 목록 줄과 같은 `pointColor(i)` · 끌기 · 메뉴 = `D03_PlanAnalysis_UI_Points`.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -330,7 +330,7 @@ export function paintMark(
|
||||
* 고리 = 고른 점 기울기 부호 색(놓인 뒤 꺾임점과 같은 색). */
|
||||
export function paintPreview(
|
||||
context: CanvasRenderingContext2D,
|
||||
map: ScreenMap,
|
||||
map: Pick<ScreenMap, "toScreen" | "pxPerMeter" | "upright">,
|
||||
from: Vertex,
|
||||
preview: DrawPreview,
|
||||
compass: { lengthM: number; fan: Fan } | null,
|
||||
@@ -368,7 +368,7 @@ export function paintPreview(
|
||||
|
||||
export function planLabel(
|
||||
context: CanvasRenderingContext2D,
|
||||
map: ScreenMap,
|
||||
map: Pick<ScreenMap, "upright">,
|
||||
x: number,
|
||||
y: number,
|
||||
text: string,
|
||||
|
||||
@@ -2,150 +2,26 @@
|
||||
* D03_PlanAnalysis_UI_DrawPanel.ts
|
||||
* D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — [긋기] · 연도 구역 마커 · 긋기 방식 칸.
|
||||
*
|
||||
* 방식 칸 `createDrawKindFields` = 마디 고름 2 × 2(자유지정 · 길이·기울기 고정 / 길이 고정 · 기울기 고정 ·
|
||||
* 625-1) + 길이 · 기울기 두 열(기울기 아래 ≈ °) — 방식에 안 쓰는 칸은 숨김(막힘 = 권한 없음만) ·
|
||||
* 그리기 칸 · 노드 판(598-4)이 같은 부품을 씀.
|
||||
* 방식 칸 `createDrawKindFields` = 공용 `A00_Common/map/map_draw_kind`(626-3) — 그리기 칸 · 노드 판(598-4) ·
|
||||
* B05 평면 예정노선 그리기가 같은 부품을 씀(여기서 다시 내보냄).
|
||||
* 연도 구역 마커 = 끌어 선 위에 놓기(`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";
|
||||
import type { DrawKindFields } from "../A00_Common/map/map_draw_kind";
|
||||
|
||||
/** 제약 칸 처음 값 — 길이 = 측점 간격(지식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: "자유지정",
|
||||
both: "길이·기울기 고정",
|
||||
length: "길이 고정",
|
||||
slope: "기울기 고정",
|
||||
};
|
||||
|
||||
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 = slopeField.input.disabled = disabled;
|
||||
fields.hidden = value.kind === "free";
|
||||
lengthField.root.hidden = value.kind === "slope";
|
||||
slopeColumn.hidden = 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
// 방식 칸 = 공용(`A00_Common/map/map_draw_kind` · 626-3 · B05 평면 예정노선 그리기도 씀)
|
||||
export {
|
||||
createDrawKindFields,
|
||||
DRAW_DEFAULT_LENGTH_M,
|
||||
DRAW_DEFAULT_SLOPE_PCT,
|
||||
DRAW_KIND_LABELS,
|
||||
type DrawConstraint,
|
||||
type DrawKindFields,
|
||||
type DrawKindValue,
|
||||
} from "../A00_Common/map/map_draw_kind";
|
||||
|
||||
export interface DrawPanel {
|
||||
root: HTMLElement;
|
||||
|
||||
@@ -185,34 +185,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 599 그리기 칸 — 방식 칸(마디 고름 2 × 2 · 길이 · 기울기 두 열) */
|
||||
.d03-plan__draw-kind {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields .ui-field {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields .ui-input-unit,
|
||||
.d03-plan__draw-fields .ui-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 599 그리기 칸 방식 칸 = 공용 `A00_Common/map/map_draw_kind.css`(626-3) */
|
||||
|
||||
/* 597 구역 · 연도 목록 — 구역 줄(이름 칸 · 연도 고름 · 지움) · 구간 줄(측점 글 작게) */
|
||||
.d03-plan__line-tail {
|
||||
|
||||
@@ -98,10 +98,14 @@ def test_d03_parts():
|
||||
], keys
|
||||
assert ".d03-plan__layers" in css and ".d03-plan__legend" in css
|
||||
# 599-1 — 방식 칸 = 마디 고름 2 × 2 · 길이 · 기울기 두 열 · 노드 판(598-4)과 같은 함수
|
||||
assert "createSegment({" in draw_panel and draw_panel.count("createInputField(") == 2
|
||||
assert "columns: 2" in draw_panel and "export function createDrawKindFields(" in draw_panel
|
||||
assert ".d03-plan__draw-fields" in css and "createDrawKindFields(" in draw
|
||||
assert '"길이·기울기 고정"' in draw_panel and '"둘 다"' not in draw_panel
|
||||
# 626-3 — 방식 칸은 공용 `A00_Common/map/map_draw_kind`(B05 평면도 씀) · D03 칸은 다시 내보냄
|
||||
kind = _src("A00_Common/map/map_draw_kind.ts")
|
||||
kind_css = _src("A00_Common/map/map_draw_kind.css")
|
||||
assert "createSegment({" in kind and kind.count("createInputField(") == 2
|
||||
assert "columns: 2" in kind and "export function createDrawKindFields(" in kind
|
||||
assert "createDrawKindFields," in draw_panel and 'from "../A00_Common/map/map_draw_kind"' in draw_panel
|
||||
assert ".map-draw-kind__fields" in kind_css and "createDrawKindFields(" in draw
|
||||
assert '"길이·기울기 고정"' in kind and '"둘 다"' not in kind
|
||||
assert "d03-plan__hint" not in draw # 칸 설명 글 걷음(요구 463-6)
|
||||
# 599-4 — 연도 구역 마커 = 그리기 칸 단추(끌어 놓기) · 지도 도구줄에서 뺌
|
||||
assert "YEAR_MARK_DRAG" in draw_panel and '"year-mark"' not in tools
|
||||
|
||||
Reference in New Issue
Block a user