Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -9,9 +9,9 @@
|
||||
* 미리보기 = 마지막 점 → 마우스 길이(m) · 가상 종단 기울기(원지반 % · ° · + 오르막) — 제약 칸 넷:
|
||||
* 자유 · 길이 고정 · 기울기 고정(기울기 고정 · 길이 자유 · 마우스 방향 첫 자리) · 둘 다(양각기 원 위 후보 중
|
||||
* 마우스에 가까운 것) · 제약 기울기 = 오르내림 같은 크기(562-1 짝이 |기울기|로 셈).
|
||||
* 긋는 해 = 새 조각 처음 해 · 긋는 중 해를 바꾸면 다음 점부터 그 해(지금 끝 자리에 연도 구분 마크).
|
||||
* 연도 구분 = 선 위 누름(마크 · 뒤 조각 고름 → 페이지가 연도 고름을 엶) · 오른쪽 = 지움.
|
||||
* 고르기 · 고치기(쉴 때) = `D03_PlanAnalysis_UI_Edit`.
|
||||
* 긋기는 해를 안 물음(585-3) — 새 선 = 사업연도 시작 해 · 갈래 = 붙은 자리 구간 해 · 이어 긋기 = 끝 구간이 늘어남 ·
|
||||
* 여러 해 = 다 그은 뒤 도구줄 「연도 마커」를 선 위로 끌어 구역을 나누고 속성 판에서 해를 고름.
|
||||
* 고르기 · 고치기 · 마커(쉴 때) = `D03_PlanAnalysis_UI_Edit`.
|
||||
* 키 = Enter 끝 · Esc 이번 긋기 취소 · Backspace 마지막 점 빼기(설정 창 단축키 표 D03 줄).
|
||||
* 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`) · 꼴 다루기 = `logic/D03_PlanAnalysis_Plan` ·
|
||||
* 값을 바꾸면 `commit(다음 꼴)` 한 번 = 되돌리기 한 걸음(스택 · 캐시 = 페이지).
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
fixedLengthPoint,
|
||||
fixedSlopePoint,
|
||||
groundSlopePct,
|
||||
lineLength,
|
||||
segmentLength,
|
||||
} from "./logic/D03_PlanAnalysis_Draw";
|
||||
import {
|
||||
@@ -37,13 +36,9 @@ import {
|
||||
dropLastVertex,
|
||||
insertVertex,
|
||||
newLine,
|
||||
normalizePieces,
|
||||
removeLine,
|
||||
removeMark,
|
||||
reverseLine,
|
||||
samePoint,
|
||||
setPieceYear,
|
||||
splitPiece,
|
||||
type PlanNetwork,
|
||||
type Vertex,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
@@ -63,7 +58,7 @@ export const DRAW_DEFAULT_LENGTH_M = 20;
|
||||
export const DRAW_DEFAULT_SLOPE_PCT = 9;
|
||||
|
||||
export type DrawConstraint = "free" | "length" | "slope" | "both";
|
||||
export type DrawMode = "idle" | "draw" | "mark";
|
||||
export type DrawMode = "idle" | "draw";
|
||||
|
||||
export interface DrawPreview {
|
||||
point: Vertex;
|
||||
@@ -83,12 +78,8 @@ export interface PlanDrawParams {
|
||||
network: () => PlanNetwork;
|
||||
heightAt: () => (x: number, y: number) => number;
|
||||
commit: (next: PlanNetwork) => void;
|
||||
/** 긋는 해 — 새 조각 처음 해 · 긋는 중 바꾸면 다음 점부터. */
|
||||
year: () => number;
|
||||
/** 모드 · 그리는 선이 바뀜 — 페이지가 단추 · 안내를 다시. */
|
||||
onMode: () => void;
|
||||
/** 연도 구분 마크를 놓음 — 뒤 조각(id) 고름 · 연도 고름. */
|
||||
onMark: (piece: string) => void;
|
||||
}
|
||||
|
||||
export interface PlanDraw {
|
||||
@@ -128,15 +119,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
size: 28,
|
||||
onClick: () => setMode(mode === "draw" ? "idle" : "draw"),
|
||||
});
|
||||
const markButton = createButton({
|
||||
label: "연도 구분",
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: () => setMode(mode === "mark" ? "idle" : "mark"),
|
||||
});
|
||||
const modeRow = document.createElement("div");
|
||||
modeRow.className = "d03-plan__row";
|
||||
modeRow.append(drawButton, markButton);
|
||||
modeRow.append(drawButton);
|
||||
const kindSegment = createSegment({
|
||||
options: [
|
||||
{ value: "free", label: "자유" },
|
||||
@@ -203,12 +188,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
}
|
||||
|
||||
function panelState(): void {
|
||||
const network = params.network();
|
||||
drawButton.querySelector(".ui-btn__label")!.textContent = mode === "draw" ? "긋기 끝" : "긋기";
|
||||
drawButton.disabled = !editable;
|
||||
markButton.disabled = !editable || !network.lines.some((line) => lineLength(line.vertices) > 0);
|
||||
drawButton.setAttribute("aria-pressed", String(mode === "draw"));
|
||||
markButton.setAttribute("aria-pressed", String(mode === "mark"));
|
||||
panel.dataset.mode = mode;
|
||||
hint.textContent = !editable
|
||||
? "보기만 — 산림청 계정에서 긋습니다."
|
||||
@@ -216,9 +198,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
? active === null
|
||||
? "지점 · 선 끝 · 선 위를 눌러 시작합니다."
|
||||
: "누름 = 점 · Enter 끝 · Esc 취소 · Backspace 마지막 점 빼기"
|
||||
: mode === "mark"
|
||||
? "선 위를 눌러 연도를 나눕니다 · 오른쪽 누름 = 마크 지움"
|
||||
: "[긋기]로 지점 · 선에서 긋습니다 · 선을 누르면 고릅니다.";
|
||||
: "[긋기]로 지점 · 선에서 긋습니다 · 선 누름 = 고름 · 「연도 마커」를 선 위로 끌면 구역을 나눕니다.";
|
||||
}
|
||||
fieldsState();
|
||||
|
||||
@@ -395,21 +375,6 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 긋는 해가 끝 조각 해와 다르면 `fromM`(이번 점 앞 길이)에서 나눠 새 조각 = 긋는 해. */
|
||||
function yearSplit(network: PlanNetwork, line: number, fromM: number): PlanNetwork {
|
||||
const year = params.year();
|
||||
if (!(fromM > 0)) return network;
|
||||
let next = normalizePieces(network, year);
|
||||
const id = next.lines[line].id;
|
||||
const tail = next.pieces.filter((piece) => piece.line === id).at(-1);
|
||||
if (!tail || tail.year === year) return next;
|
||||
next = splitPiece(next, id, fromM);
|
||||
const index = next.pieces.findIndex(
|
||||
(piece) => piece.line === id && Math.abs(piece.from_m - fromM) < 1e-3,
|
||||
);
|
||||
return index >= 0 ? setPieceYear(next, index, year) : next;
|
||||
}
|
||||
|
||||
function addPoint(): boolean {
|
||||
if (active === null || !preview) return false;
|
||||
if (preview.blocked) {
|
||||
@@ -419,11 +384,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
const line = active;
|
||||
const ended = preview.snapped;
|
||||
let next = params.network();
|
||||
const before = Math.round(lineLength(next.lines[line].vertices) * 1000) / 1000;
|
||||
if (preview.onLine)
|
||||
next = insertVertex(next, preview.onLine.line, preview.onLine.segment, preview.point);
|
||||
next = yearSplit(appendVertex(next, line, preview.point), line, before);
|
||||
params.commit(next);
|
||||
params.commit(appendVertex(next, line, preview.point));
|
||||
if (ended) {
|
||||
showToast(`${preview.note ?? "붙음"} — 긋기 끝`, "success");
|
||||
setMode("idle");
|
||||
@@ -458,21 +421,6 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
preview = previewAt(px, py, map);
|
||||
return addPoint();
|
||||
}
|
||||
if (mode === "mark") {
|
||||
const network = params.network();
|
||||
const hit = lineAt(network, px, py, map);
|
||||
if (!hit) return false;
|
||||
const id = network.lines[hit.line].id;
|
||||
const next =
|
||||
button === 2
|
||||
? removeMark(network, id, hit.atM, SNAP_PX / map.pxPerMeter)
|
||||
: splitPiece(network, id, hit.atM);
|
||||
if (next === network) return true;
|
||||
const added = next.pieces.find((piece) => !network.pieces.some((p) => p.id === piece.id));
|
||||
params.commit(next);
|
||||
if (added) params.onMark(added.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hover(px, py, map) {
|
||||
@@ -500,7 +448,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
setMode("idle");
|
||||
return true;
|
||||
}
|
||||
if (mode !== "idle" && event.key === "Escape") {
|
||||
if (mode === "draw" && event.key === "Escape") {
|
||||
if (mode === "draw" && strokeStart && strokeStart !== params.network())
|
||||
params.commit(strokeStart);
|
||||
active = null; // 되돌린 꼴이라 stop 의 버림 · 뒤집기는 안 함
|
||||
|
||||
@@ -186,24 +186,43 @@ function drawPieces(context: CanvasRenderingContext2D, map: ScreenMap, network:
|
||||
const vertices = network.lines.find((line) => line.id === piece.line)?.vertices;
|
||||
if (!vertices || vertices.length < 2) return;
|
||||
if (piece.from_m > 0) {
|
||||
const [sx, sy] = map.toScreen(pointAtDistance(vertices, piece.from_m));
|
||||
context.beginPath();
|
||||
context.moveTo(sx, sy - 8);
|
||||
context.lineTo(sx + 6, sy);
|
||||
context.lineTo(sx, sy + 8);
|
||||
context.lineTo(sx - 6, sy);
|
||||
context.closePath();
|
||||
context.fillStyle = colors.active;
|
||||
context.fill();
|
||||
context.lineWidth = 1.5;
|
||||
context.strokeStyle = "#ffffff";
|
||||
context.stroke();
|
||||
// 앞 구간과 같은 해 = 구역만 나눔(속 빈 마름모 · 585-3)
|
||||
const before = network.pieces.find(
|
||||
(p) => p.line === piece.line && Math.abs(p.to_m - piece.from_m) < 1e-3,
|
||||
);
|
||||
const hollow = before?.year === piece.year;
|
||||
paintMark(
|
||||
context,
|
||||
map.toScreen(pointAtDistance(vertices, piece.from_m)),
|
||||
colors.active,
|
||||
hollow,
|
||||
);
|
||||
}
|
||||
const [mx, my] = map.toScreen(pointAtDistance(vertices, (piece.from_m + piece.to_m) / 2));
|
||||
planLabel(context, map, mx, my + 16, `${piece.year}년`, "bold 12px");
|
||||
});
|
||||
}
|
||||
|
||||
/** 연도 구역 마커 마름모 — 속 빈 = 같은 해 구역 나눔. */
|
||||
export function paintMark(
|
||||
context: CanvasRenderingContext2D,
|
||||
[sx, sy]: [number, number],
|
||||
color: string,
|
||||
hollow = false,
|
||||
): void {
|
||||
context.beginPath();
|
||||
context.moveTo(sx, sy - 8);
|
||||
context.lineTo(sx + 6, sy);
|
||||
context.lineTo(sx, sy + 8);
|
||||
context.lineTo(sx - 6, sy);
|
||||
context.closePath();
|
||||
context.fillStyle = hollow ? "#ffffff" : color;
|
||||
context.fill();
|
||||
context.lineWidth = hollow ? 2.5 : 1.5;
|
||||
context.strokeStyle = hollow ? color : "#ffffff";
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
/** 미리보기 — 점선 · 둘 다(양각기) 원 · 후보 · 글(거리 · 가상 종단 기울기 % · °). */
|
||||
export function paintPreview(
|
||||
context: CanvasRenderingContext2D,
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* 고른 선 위 끌기 = 꺾임점 더해 옮김(누름만은 넓힘) · 꺾임점 끌기 = 옮김(붙은 갈래 첫 점 같이) ·
|
||||
* 마크 누름 = 뒤 구간 고름 + 마크 밝힘 · 끌기 = 연도 구분 옮김 · 오른쪽 누름 = 꺾임점 · 마크 지움 ·
|
||||
* Delete = 고른 마크 지움 · 고른 구간이 선 통째면 그 선 지움.
|
||||
* 연도 구역 마커(585-3) = 도구줄 「연도 마커」를 선 위로 끌어 놓음 = 그 자리 구간을 둘로(해 그대로 · 구역만 나눔 ·
|
||||
* 해는 고른 뒤 속성 판) · 끄는 동안 마름모 미리보기(못 놓음 = 경고 색) · 놓인 마크를 선 밖으로 끌어냄 = 지움(합침).
|
||||
* 잠금 = 지점에 놓인 점(1단계 몫) · 갈래 첫 점(붙은 선 점을 끌면 같이 감).
|
||||
* 끌기 중 = 화면만(`show`) · 놓을 때 `commit` 한 번 = 되돌리기 한 걸음 · 고름 · 넓이 = 화면 기억(저장 안 함).
|
||||
* 속성 판 = `D03_PlanAnalysis_UI_Props`(고름을 읽고 `select` · `setScope` · 동작 셋을 부름).
|
||||
@@ -32,15 +34,19 @@ import {
|
||||
removeVertex,
|
||||
reverseLine,
|
||||
samePoint,
|
||||
splitPiece,
|
||||
branchFromEnd,
|
||||
type PlanNetwork,
|
||||
type Vertex,
|
||||
type ViewBox,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { lineAt, screenGap, SNAP_PX } from "./D03_PlanAnalysis_UI_DrawPaint";
|
||||
import { lineAt, paintMark, planLabel, screenGap, SNAP_PX } from "./D03_PlanAnalysis_UI_DrawPaint";
|
||||
import { planColors, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
/** 끌기로 볼 마우스 움직임(px) — 이보다 적으면 누름. */
|
||||
const DRAG_PX = 4;
|
||||
/** 마크를 선에서 이만큼(px) 넘게 끌어내면 지움. */
|
||||
const MARK_OFF_PX = 3 * SNAP_PX;
|
||||
|
||||
export type PlanScope = "piece" | "line" | "year";
|
||||
const NEXT_SCOPE: Record<PlanScope, PlanScope> = { piece: "line", line: "year", year: "piece" };
|
||||
@@ -79,6 +85,8 @@ export interface PlanEdit {
|
||||
flipLine: () => void;
|
||||
dropLine: () => void;
|
||||
dropMark: () => void;
|
||||
/** 갈래 시점 지정 — 다른 선에 닿은 끝을 시점으로 그 선의 갈래(585-3). */
|
||||
branchLine: () => void;
|
||||
/** 고른 구간이 모두 한 선이면 그 선 차례(아니면 −1). */
|
||||
lineIndex: () => number;
|
||||
/** 키 — 받았으면 참(Delete · Esc). */
|
||||
@@ -107,6 +115,8 @@ type Drag =
|
||||
export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
let selected: PlanSelection | null = null;
|
||||
let drag: Drag | null = null;
|
||||
/** 「연도 마커」 끄는 중 미리보기 — 놓일 자리 · 놓을 수 있음 · 글. */
|
||||
let markDrop: { point: Vertex; ok: boolean; text: string } | null = null;
|
||||
|
||||
const pieceIds = (network: PlanNetwork): Set<string> =>
|
||||
new Set(network.pieces.map((piece) => piece.id));
|
||||
@@ -183,8 +193,9 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
apply((net, i) => removeLine(net, i));
|
||||
}
|
||||
|
||||
/** 사라진 구간 뺌 — 부르는 쪽(페이지 `refresh`)이 다시 그림(여기서 onSelect 를 부르면 돌고 돎). */
|
||||
function sync(): void {
|
||||
if (!selected) return params.onSelect();
|
||||
if (!selected) return;
|
||||
const ids = pieceIds(params.network());
|
||||
const pieces = selected.pieces.filter((id) => ids.has(id));
|
||||
selected = pieces.length
|
||||
@@ -195,7 +206,6 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
mark: selected.mark && ids.has(selected.mark) ? selected.mark : null,
|
||||
}
|
||||
: null;
|
||||
params.onSelect();
|
||||
}
|
||||
|
||||
const pieceAt = (network: PlanNetwork, line: string, atM: number): string | null =>
|
||||
@@ -242,6 +252,20 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 「연도 마커」를 이 자리에 놓으면 — 다음 꼴 · 새 뒤 구간 id(못 놓음 = null). */
|
||||
function markPlace(
|
||||
px: number,
|
||||
py: number,
|
||||
map: ScreenMap,
|
||||
): { point: Vertex; next: PlanNetwork | null; piece: string | null } {
|
||||
const network = params.network();
|
||||
const hit = lineAt(network, px, py, map);
|
||||
if (!hit) return { point: map.toMetric(px, py), next: null, piece: null };
|
||||
const next = splitPiece(network, network.lines[hit.line].id, hit.atM);
|
||||
const added = next.pieces.find((piece) => !network.pieces.some((p) => p.id === piece.id));
|
||||
return { point: hit.point, next: added ? next : null, piece: added?.id ?? null };
|
||||
}
|
||||
|
||||
/** 구간 누름 — Shift = 더함 · 뺌 · 이미 고른 곳 = 넓힘(놓을 때) · 아니면 그 구간. */
|
||||
function pressPiece(
|
||||
piece: string,
|
||||
@@ -317,6 +341,11 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
context.strokeStyle = colors.active;
|
||||
context.stroke();
|
||||
}
|
||||
if (markDrop) {
|
||||
const at = map.toScreen(markDrop.point);
|
||||
paintMark(context, at, markDrop.ok ? colors.active : colors.warn);
|
||||
planLabel(context, map, at[0], at[1] - 20, markDrop.text, "bold 12px");
|
||||
}
|
||||
// 끌어 고르기 상자 — 보이는 화면 네모를 그림 px 로 되돌려 그림
|
||||
if (drag?.kind === "box") {
|
||||
const { box } = drag;
|
||||
@@ -421,10 +450,41 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
} else {
|
||||
const piece = drag.before.pieces[drag.index];
|
||||
const vertices = drag.before.lines.find((line) => line.id === piece.line)!.vertices;
|
||||
drag.last = moveMark(drag.before, drag.index, distanceAlong(vertices, cursor));
|
||||
const atM = distanceAlong(vertices, cursor);
|
||||
const off = screenGap(map, pointAtDistance(vertices, atM), px, py) > MARK_OFF_PX;
|
||||
drag.last = off
|
||||
? removeMark(drag.before, piece.line, piece.from_m, 1e-3)
|
||||
: moveMark(drag.before, drag.index, atM);
|
||||
}
|
||||
params.show(drag.last);
|
||||
},
|
||||
dragOver(px, py, map) {
|
||||
if (!params.enabled() || !params.editable()) return false;
|
||||
const place = markPlace(px, py, map);
|
||||
markDrop = {
|
||||
point: place.point,
|
||||
ok: !!place.next,
|
||||
text: place.next
|
||||
? "여기서 구역 나눔"
|
||||
: lineAt(params.network(), px, py, map)
|
||||
? "선 끝 · 다른 마커 1 m 안 — 못 놓음"
|
||||
: "선 위에 놓으세요",
|
||||
};
|
||||
return true;
|
||||
},
|
||||
drop(px, py, map) {
|
||||
markDrop = null;
|
||||
if (!params.enabled() || !params.editable()) return false;
|
||||
const place = markPlace(px, py, map);
|
||||
if (!place.next || !place.piece)
|
||||
return (void showToast("선 위(끝 · 다른 마커 1 m 밖)에 놓으세요.", "info"), true);
|
||||
params.commit(place.next);
|
||||
select([place.piece], place.piece);
|
||||
return true;
|
||||
},
|
||||
leave() {
|
||||
markDrop = null;
|
||||
},
|
||||
release(map) {
|
||||
const done = drag;
|
||||
drag = null;
|
||||
@@ -463,6 +523,7 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
flipLine: () => apply((network, index) => reverseLine(network, index)),
|
||||
dropLine: () => apply((network, index) => removeLine(network, index)),
|
||||
dropMark,
|
||||
branchLine: () => apply((network, index) => branchFromEnd(network, index)),
|
||||
lineIndex,
|
||||
key(event) {
|
||||
if (!selected || !params.enabled()) return false;
|
||||
|
||||
@@ -32,6 +32,9 @@ import { fitCanvas, screenToMetric } from "../B05_Profile/B05_Profile_UI_RouteEd
|
||||
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { localTmFrame, tmParams } from "./logic/D03_PlanAnalysis_Tm";
|
||||
|
||||
/** 도구줄 「연도 마커」 끌어 놓기 자료 꼴(585-3). */
|
||||
export const YEAR_MARK_DRAG = "application/x-aislo-year-mark";
|
||||
|
||||
/** 측점 간격(m) — B 규칙 측점과 같음. */
|
||||
export const PLAN_STATION_INTERVAL_M = 20;
|
||||
/** 겹 그림 색(그림 전용 값 `--draw-*` · 밝음 · 어두움 같음) — 선 · 그리는 중 · 경고 · 글 테두리. */
|
||||
@@ -84,6 +87,9 @@ export interface PlanMapLayer {
|
||||
press?: (px: number, py: number, map: ScreenMap, button: 0 | 2, shift: boolean) => boolean;
|
||||
hover?: (px: number, py: number, map: ScreenMap) => void;
|
||||
release?: (map: ScreenMap) => void;
|
||||
/** 「연도 마커」를 끌고 지나감(참 = 받을 겹) · 놓음(참 = 받음) — 585-3. */
|
||||
dragOver?: (px: number, py: number, map: ScreenMap) => boolean;
|
||||
drop?: (px: number, py: number, map: ScreenMap) => boolean;
|
||||
leave?: () => void;
|
||||
}
|
||||
|
||||
@@ -100,6 +106,8 @@ export interface PlanTools {
|
||||
view3d: boolean;
|
||||
/** 거리재기 켜짐(눌림). */
|
||||
measure: boolean;
|
||||
/** 「연도 마커」 끌 수 있음(고칠 수 있고 선 있음). */
|
||||
yearMark: boolean;
|
||||
}
|
||||
|
||||
export interface PlanMap {
|
||||
@@ -169,6 +177,12 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
const redoButton = mapToolButton("redo", { onClick: events.onRedo });
|
||||
const viewButton = mapToolButton("view3d", { onClick: events.on3d });
|
||||
const measureButton = mapToolButton("measure", { onClick: events.onMeasure });
|
||||
const yearMarkButton = mapToolButton("year-mark", { disabled: true });
|
||||
yearMarkButton.draggable = true;
|
||||
yearMarkButton.addEventListener("dragstart", (event) => {
|
||||
event.dataTransfer?.setData(YEAR_MARK_DRAG, "1");
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy";
|
||||
});
|
||||
const tools = createMapTools({
|
||||
label: "평면 도구",
|
||||
hint: {
|
||||
@@ -182,7 +196,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
"Shift+누름 = 구간 더함 · 뺌",
|
||||
"빈 곳 오른쪽으로 끌기 = 창 안 구간 · 왼쪽으로 = 걸친 구간까지",
|
||||
"고른 선 위 끌기 = 꺾임점 더해 옮김",
|
||||
"꺾임점 · 마크 끌기 = 옮김",
|
||||
"「연도 마커」 선 위로 끌어 놓기 = 구역 나눔",
|
||||
"꺾임점 · 마크 끌기 = 옮김 · 마크를 선 밖으로 = 지움(앞 구간과 합침)",
|
||||
"오른쪽 누름 = 꺾임점 · 마크 지움",
|
||||
"Delete = 고른 선 · 마크 지움",
|
||||
"가운데 끌기 = 옮기기",
|
||||
@@ -194,6 +209,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
[undoButton, redoButton],
|
||||
[viewButton, "rotate-ccw", "rotate-cw"],
|
||||
[markButton("stations", "측점"), markButton("slopes", "구간 길이 · 기울기"), measureButton],
|
||||
[yearMarkButton],
|
||||
],
|
||||
});
|
||||
const toolbar = tools.root;
|
||||
@@ -376,6 +392,30 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
drawTop();
|
||||
});
|
||||
const markDrag = (event: DragEvent): boolean =>
|
||||
!!meta && !!event.dataTransfer?.types.includes(YEAR_MARK_DRAG);
|
||||
canvas.addEventListener("dragover", (event) => {
|
||||
if (!markDrag(event)) return;
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.dragOver?.(px, py, map))) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = "copy";
|
||||
}
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("drop", (event) => {
|
||||
if (!markDrag(event)) return;
|
||||
event.preventDefault();
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
layers.some((layer) => layer.drop?.(px, py, map));
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("dragleave", () => {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if (!meta) return;
|
||||
@@ -414,6 +454,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
if (tools.view3d !== undefined) viewButton.disabled = !tools.view3d;
|
||||
if (tools.measure !== undefined)
|
||||
measureButton.setAttribute("aria-pressed", String(tools.measure));
|
||||
if (tools.yearMark !== undefined) yearMarkButton.disabled = !tools.yearMark;
|
||||
},
|
||||
redraw: drawTop,
|
||||
redrawBase: draw,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* 메인 = 평면(바탕 겹 켜고 끔 — 위성 · 등고선 · 세류 · 지적 · 도로 · 급경사 + 그은 선 · 측점 · 연도 구분) + 도구 여섯.
|
||||
* 좌 = 지형(들어올 때 받은 범위가 지점을 못 덮으면 바로 받기 · [범위 밖 더 받기]는 지점 · 선이
|
||||
* 받은 범위 밖일 때만) · 그리기([긋기] · [연도 구분] · 제약 칸 · 긋는 해) ·
|
||||
* 받은 범위 밖일 때만) · 그리기([긋기] · 제약 칸 · 연도 = 도구줄 「연도 마커」 끌어 놓기 585-3) ·
|
||||
* 구역 · 연도(해 · 선 · 구간 줄 = 고르기 · 고른 줄 밝힘) · [저장] [초기화].
|
||||
* 고른 구간 속성(연도 · 넓이 · 방향 · 지움) = 지도 위 속성 판 `D03_PlanAnalysis_UI_Props`(585-2).
|
||||
* 지점 = D02 지점 목록(핀 · 늘 맨 위 · 585-1) · 고르기 · 고치기 = `D03_PlanAnalysis_UI_Edit`.
|
||||
@@ -18,7 +18,7 @@
|
||||
import "./D03_PlanAnalysis_UI_Style.css";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { editMap } from "@ui/ui_template_kakao_map";
|
||||
import { askConfirm, openModal } from "@ui/ui_template_modal";
|
||||
import { createProgressBar } from "@ui/ui_template_progress";
|
||||
@@ -85,8 +85,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
let editable = false;
|
||||
let points: Vertex[] = []; // 기준 지점(D02 · 585-1)
|
||||
let epsg = 0;
|
||||
let yearStart = new Date().getFullYear();
|
||||
let drawYear = yearStart; // 긋는 해 — 새 조각 처음 해
|
||||
let yearStart = new Date().getFullYear(); // 새 선 처음 해
|
||||
let network: PlanNetwork = emptyNetwork();
|
||||
let savedJson = JSON.stringify(network); // 못 읽은 채 떠남 = 고침 없음
|
||||
let undoStack: PlanNetwork[] = [];
|
||||
@@ -104,19 +103,17 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
network: () => network,
|
||||
heightAt: () => z,
|
||||
commit,
|
||||
year: () => drawYear,
|
||||
onMode: () => {
|
||||
if (draw.mode() !== "idle") setMeasure(false);
|
||||
refresh();
|
||||
},
|
||||
onMark: (piece) => edit.select([piece]),
|
||||
});
|
||||
const edit = createPlanEdit({
|
||||
network: () => network,
|
||||
base: () => points,
|
||||
commit,
|
||||
show: (next) => {
|
||||
network = normalizePieces(next, drawYear);
|
||||
network = normalizePieces(next, yearStart);
|
||||
map.redraw();
|
||||
},
|
||||
enabled: () => draw.mode() === "idle" && !measuring,
|
||||
@@ -166,16 +163,8 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
const terrainCard = createPanelCard("지형", false);
|
||||
terrainCard.append(terrainText, bar.root, extentButton, moreButton);
|
||||
const drawYearPick = createSelectField({
|
||||
label: "긋는 해",
|
||||
options: [],
|
||||
size: 24,
|
||||
ariaLabel: "긋는 해",
|
||||
onChange: (value) => (drawYear = Number(value)),
|
||||
});
|
||||
drawYearPick.select.dataset.role = "draw-year";
|
||||
const drawCard = createPanelCard("그리기", false);
|
||||
drawCard.append(draw.panel, drawYearPick.root);
|
||||
drawCard.append(draw.panel);
|
||||
const pieceCard = createPanelCard("구역 · 연도", false);
|
||||
pieceCard.append(props.list);
|
||||
const saveButton = createButton({ label: "저장", variant: "filled", onClick: () => void save() });
|
||||
@@ -212,7 +201,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
function commit(next: PlanNetwork): void {
|
||||
undoStack = [...undoStack.slice(1 - UNDO_MAX), network];
|
||||
redoStack = [];
|
||||
network = normalizePieces(next, drawYear);
|
||||
network = normalizePieces(next, yearStart);
|
||||
keepDraft();
|
||||
refresh();
|
||||
}
|
||||
@@ -265,6 +254,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
redo: editable && redoStack.length > 0,
|
||||
view3d: !!surface && network.lines.some((line) => line.vertices.length > 1),
|
||||
measure: measuring,
|
||||
yearMark: editable && network.lines.some((line) => line.vertices.length > 1),
|
||||
});
|
||||
map.redraw();
|
||||
props.render();
|
||||
@@ -507,7 +497,6 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
select: edit.select,
|
||||
setScope: edit.setScope,
|
||||
box: edit.box,
|
||||
drawYear: () => drawYear,
|
||||
constraint: draw.constraint,
|
||||
setConstraint: draw.setConstraint,
|
||||
mode: draw.mode,
|
||||
@@ -554,8 +543,6 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
points = base.points.map((p): Vertex => [p.x, p.y]);
|
||||
epsg = base.epsg;
|
||||
takeServer(view);
|
||||
drawYear = yearStart;
|
||||
drawYearPick.setOptions(props.yearOptions(), String(drawYear));
|
||||
const draft = readState<Draft>("d03-plan", projectId);
|
||||
if (draft?.version === DRAFT_VERSION && draft.network?.lines?.length) network = draft.network;
|
||||
map.update({ fit: [...points, ...allVertices()], epsg });
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* 속성 판 = 지도 위 떠 있는 판(공용 `createFloatPanel` · 머리 끌어 옮김 · 오른쪽 위) · 고름 있을 때만 —
|
||||
* 머리 「구간 N · 선 M · L m」 · ✕ = 풂 · 넓이 [구간][선][같은 해] · 연도(모두 같으면 그 해 · 다르면 「여러 해」 →
|
||||
* 고르면 고른 구간 모두 그 해 = 되돌리기 한 걸음) · 고른 구간 줄(누름 = 그 구간만) ·
|
||||
* [방향 바꾸기] [선 지움](선 하나일 때) · [마크 지움](마크 고름) · 보기만 계정 = 값만.
|
||||
* [방향 바꾸기] [갈래 시점 지정] [선 지움](선 하나일 때) · [마크 지움](마크 고름) · 보기만 계정 = 값만.
|
||||
* 측점 방향(585-3 · 요구 446 3) = 사용자가 정함 — 본선 = [방향 바꾸기] · 다른 선에 닿은 끝 = [갈래 시점 지정]
|
||||
* (그 점이 시점 · 꼬리에 닿은 머리 = 한 선으로 이음) · 자동 판정 없음.
|
||||
* 좌측 목록 = 보기 + 고르기만(해 고침은 속성 판 한 곳) — 해 줄(같은 해) · 선 머리 줄(선) · 구간 줄(구간).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -141,7 +143,16 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
onClick: edit.dropLine,
|
||||
});
|
||||
drop.dataset.role = "props-drop";
|
||||
actions.append(flip, drop);
|
||||
const branch = createButton({
|
||||
label: "갈래 시점 지정",
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: edit.branchLine,
|
||||
});
|
||||
branch.title = "다른 선에 닿은 끝을 시점(No.0)으로 · 그 선의 갈래로 붙입니다.";
|
||||
branch.hidden = !!network.lines[index].branch;
|
||||
branch.dataset.role = "props-branch";
|
||||
actions.append(flip, branch, drop);
|
||||
}
|
||||
if (editable && picked.mark) {
|
||||
const dropMark = createButton({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_Plan.ts
|
||||
* D 예정노선 2단계 손 긋기 꼴(PLAN 562-3 · 580) — 그은 선 · 갈래 · 조각(구역 × 연도) 다루기 ·
|
||||
* 선 뒤집기 · 잇기 · 꺾임점 옮김 · 지움 · 선 지움 · 선 통째 연도 · 마크 옮김 · 여러 조각 연도 · 끌어 고르기 판정(585-2).
|
||||
* 선 뒤집기 · 잇기 · 꺾임점 옮김 · 지움 · 선 지움 · 선 통째 연도 · 마크 옮김 · 여러 조각 연도 · 끌어 고르기 판정(585-2) ·
|
||||
* 갈래 시점 지정 · 갈래 처음 해 = 붙은 자리 구간 해(585-3).
|
||||
* DOM · fetch · sessionStorage 없음 · 길이 셈 = 562-1 짝(`D03_PlanAnalysis_Draw`).
|
||||
* [저장] 은 서버가 선 · 조각으로 연도 행을 다시 셈(562-2) — 여기 값을 받아 적지 않음.
|
||||
* ========================================================================== */
|
||||
@@ -70,6 +71,19 @@ export function newLine(network: PlanNetwork, point: Vertex): PlanNetwork {
|
||||
return { ...network, lines: [...network.lines, line] };
|
||||
}
|
||||
|
||||
/** 새 갈래 처음 해(585-3) = 붙은 자리 구간 해 · 붙은 자리 못 찾음 = null. */
|
||||
function branchYear(network: PlanNetwork, line: PlanLine): number | null {
|
||||
const parent = network.lines.find((other) => other.id === line.branch?.line);
|
||||
if (!parent || !line.branch) return null;
|
||||
const atM = lineLength(parent.vertices.slice(0, line.branch.vertex + 1));
|
||||
return (
|
||||
network.pieces.find(
|
||||
(piece) =>
|
||||
piece.line === parent.id && atM >= piece.from_m - SAME_M && atM <= piece.to_m + SAME_M,
|
||||
)?.year ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** 선마다 조각이 0 ~ 길이를 빈틈없이 덮게 맞춤 — 경계 = 조각 시작(0 · 끝 근처 빼고) · 연도 = 그 자리 조각 ·
|
||||
* 조각 없는 선 = 한 조각(`year`). 길이 0 선은 조각 없음. */
|
||||
export function normalizePieces(network: PlanNetwork, year: number): PlanNetwork {
|
||||
@@ -83,7 +97,10 @@ export function normalizePieces(network: PlanNetwork, year: number): PlanNetwork
|
||||
const starts = own
|
||||
.filter((piece) => piece.from_m > MARK_GAP_M / 2 && piece.from_m < length - MARK_GAP_M / 2)
|
||||
.map((piece) => ({ id: piece.id, from: round3(piece.from_m), year: piece.year }));
|
||||
const head = own.find((piece) => piece.from_m <= MARK_GAP_M / 2) ?? { id: "", year };
|
||||
const head = own.find((piece) => piece.from_m <= MARK_GAP_M / 2) ?? {
|
||||
id: "",
|
||||
year: own.length ? year : (branchYear(network, line) ?? year),
|
||||
};
|
||||
const cuts = [{ id: head.id, from: 0, year: head.year }, ...starts];
|
||||
cuts.forEach((cut, i) =>
|
||||
pieces.push({
|
||||
@@ -573,3 +590,27 @@ export function piecesInBox(
|
||||
})
|
||||
.map((piece) => piece.id);
|
||||
}
|
||||
|
||||
/** 갈래 시점 지정(585-3) — 본선 한 끝이 다른 선 꺾임점에 닿았으면 그 점을 시점(No.0)으로 그 선의 갈래로 ·
|
||||
* 머리가 다른 선 꼬리면 한 선으로 이음(연속 구간 = 측점 차례). 닿은 끝 없음 · 이미 갈래 = 까닭 글. */
|
||||
export function branchFromEnd(network: PlanNetwork, line: number): PlanNetwork | string {
|
||||
const entry = network.lines[line];
|
||||
if (!entry || entry.vertices.length < 2) return "선을 고르세요.";
|
||||
if (entry.branch) return "이미 갈래입니다 — 갈라지는 점이 시점입니다.";
|
||||
const own = family(network, entry.id);
|
||||
const touches = (point: Vertex): boolean =>
|
||||
network.lines.some(
|
||||
(other) => !own.has(other.id) && other.vertices.some((v) => samePoint(v, point)),
|
||||
);
|
||||
if (touches(entry.vertices[0])) {
|
||||
const next = attachHead(network, line);
|
||||
if (next !== network) return next;
|
||||
}
|
||||
if (!touches(entry.vertices[entry.vertices.length - 1]))
|
||||
return "다른 선에 닿은 끝이 없습니다 — 끝을 다른 선 위에 놓고 지정하세요.";
|
||||
const turned = reverseLine(network, line);
|
||||
if (typeof turned === "string") return turned;
|
||||
const index = turned.lines.findIndex((other) => other.id === entry.id);
|
||||
const next = attachHead(turned, index);
|
||||
return next === turned ? "다른 선에 닿은 끝이 없습니다." : next;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ def test_branch_order_and_guard(tmp_path: Path) -> None:
|
||||
got = _run_ts(tmp_path)
|
||||
branch = got["branched"]["lines"][1]
|
||||
assert branch["branch"] == {"line": "main", "vertex": 1} and branch["vertices"][0] == [100, 0]
|
||||
assert _spans(got["branched"], branch["id"]) == [(0, 60, 2026)]
|
||||
# 585-3 갈래 처음 해 = 붙은 자리(본선 100 m = 2027 구간) 해
|
||||
assert _spans(got["branched"], branch["id"]) == [(0, 60, 2027)]
|
||||
# 본선 앞 구간에 꺾임점을 끼우면 갈래가 붙은 차례가 한 칸 밀림
|
||||
assert got["inserted"]["lines"][0]["vertices"][1] == [40, 0]
|
||||
assert got["inserted"]["lines"][1]["branch"]["vertex"] == 2
|
||||
@@ -193,7 +194,7 @@ def test_server_accepts_screen_body(tmp_path: Path) -> None:
|
||||
assert [(p["line"], p["from_m"], p["to_m"], p["year"]) for p in derived["pieces"]] == [
|
||||
("main", 0.0, 50.0, 2026),
|
||||
("main", 50.0, 200.0, 2027),
|
||||
("branch-1", 0.0, 60.0, 2026),
|
||||
("branch-1", 0.0, 60.0, 2027),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,9 @@ def test_d03_parts():
|
||||
], keys
|
||||
assert ".d03-plan__layers" in css and ".d03-plan__legend" in css
|
||||
assert "createSegment({" in draw and draw.count("createInputField(") == 2
|
||||
assert "createSelectField(" in ts # 조각 연도
|
||||
props = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Props.ts") # 585-2 조각 연도 = 속성 판
|
||||
assert "createSelectField(" in props and "createFloatPanel(" in props
|
||||
assert 'createElement("button")' not in props and 'createElement("select")' not in props
|
||||
assert 'createElement("select")' not in three and "createSelectField" in three
|
||||
assert "createCheckbox" not in three and "buildCorridor" not in three # 예상 코리도 걷음
|
||||
for dead in (".d03-plan__bar", ".d03-plan__mode", "is-picked"):
|
||||
|
||||
@@ -55,6 +55,7 @@ def test_단추_목록_아이콘_차례():
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
"year-mark",
|
||||
]
|
||||
names = _icon_names()
|
||||
assert all(icon in names for _, icon, _ in rows)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""585-2 예정노선 끌어 고르기 판정 · 여러 구간 연도(요구 445 A4 · A5) — TS `logic/D03_PlanAnalysis_Plan.ts`.
|
||||
"""585-2 · 585-3 예정노선 끌어 고르기 판정 · 여러 구간 연도 · 연도 구역 마커 나눔 · 합침 · 갈래 시점 지정
|
||||
(요구 445 A4~A6 · 446) — TS `logic/D03_PlanAnalysis_Plan.ts`.
|
||||
|
||||
창(오른쪽으로) = 구간 꺾은선 점이 모두 상자 안 · 걸침(왼쪽으로) = 점 하나라도 안 또는 토막이 상자 변과 만남 ·
|
||||
돌린 화면도 보이는 px 로 셈 · 여러 구간 연도 한 번에.
|
||||
@@ -42,7 +43,34 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
all: pick(box(-10, -110, 310, 10), false),
|
||||
rotated: pick(box(-110, -110, -40, -90), false, turned),
|
||||
years: plan.setPiecesYear(n, ["p1", "p3"], 2028).pieces.map((p) => [p.id, p.year]),
|
||||
...marks(),
|
||||
}));
|
||||
|
||||
/** 585-3 — 마커 = 구역 나누기만(해 그대로) · 지움 = 앞 구간이 먹음 · 갈래 처음 해 · 갈래 시점 지정. */
|
||||
function marks() {
|
||||
const span = (net) => net.pieces.filter((p) => p.line === "main").map((p) => [p.id, p.from_m, p.to_m, p.year]);
|
||||
let m = plan.newLine(plan.emptyNetwork(), [0, 0]);
|
||||
m = plan.normalizePieces(plan.appendVertex(m, 0, [100, 0]), Y);
|
||||
const cut = plan.normalizePieces(plan.splitPiece(m, "main", 40), Y);
|
||||
const edge = plan.splitPiece(cut, "main", 40.5); // 다른 마커 1 m 안 = 그대로
|
||||
const yeared = plan.setPiecesYear(cut, ["p2"], 2028);
|
||||
const merged = plan.normalizePieces(plan.removeMark(yeared, "main", 40, 1e-3), Y);
|
||||
// 갈래 처음 해 = 붙은 자리(60 m · p2 = 2028) 구간 해
|
||||
let b = plan.normalizePieces(plan.insertVertex(yeared, 0, 0, [60, 0]), Y);
|
||||
b = plan.normalizePieces(plan.appendVertex(plan.addBranch(b, 0, 1), 1, [60, 50]), Y);
|
||||
// 갈래 시점 지정 — (60,90) 에서 그어 (60,0) 꺾임점에 닿은 본선 → 닿은 끝이 시점인 갈래
|
||||
let t = plan.normalizePieces(plan.insertVertex(m, 0, 0, [60, 0]), Y);
|
||||
t = plan.normalizePieces(plan.appendVertex(plan.newLine(t, [60, 90]), 1, [60, 0]), Y);
|
||||
const branched = plan.branchFromEnd(t, 1);
|
||||
const lone = plan.normalizePieces(plan.appendVertex(plan.newLine(m, [0, 50]), 1, [30, 80]), Y);
|
||||
return {
|
||||
cut: span(cut), edgeSame: edge === cut, merged: span(merged),
|
||||
branchYear: b.pieces.filter((p) => p.line !== "main").map((p) => p.year),
|
||||
branched: typeof branched === "string" ? branched : branched.lines.map((l) => [l.id, l.vertices[0], l.branch]),
|
||||
again: typeof branched === "string" ? null : plan.branchFromEnd(branched, 1),
|
||||
lone: plan.branchFromEnd(lone, 1),
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -88,3 +116,16 @@ def test_여러_구간_연도(tmp_path: Path) -> None:
|
||||
got = _run_ts(tmp_path)
|
||||
years = dict(got["years"])
|
||||
assert years["p1"] == years["p3"] == 2028 and years["p2"] == 2026
|
||||
|
||||
|
||||
def test_마커_구역_나눔_합침_갈래(tmp_path: Path) -> None:
|
||||
got = _run_ts(tmp_path)
|
||||
assert got["cut"] == [["p1", 0, 40, 2026], ["p2", 40, 100, 2026]] # 구역만 나눔 · 해 그대로
|
||||
assert got["edgeSame"] # 다른 마커 1 m 안 = 안 놓음
|
||||
assert got["merged"] == [["p1", 0, 100, 2026]] # 지움 = 앞 구간 해 · id 가 먹음
|
||||
assert got["branchYear"] == [2028] # 갈래 처음 해 = 붙은 자리 구간 해
|
||||
lines = {line_id: (head, branch) for line_id, head, branch in got["branched"]}
|
||||
head, branch = lines[next(k for k in lines if k != "main")]
|
||||
assert head == [60, 0] and branch == {"line": "main", "vertex": 1} # 닿은 끝 = 시점
|
||||
assert got["again"] == "이미 갈래입니다 — 갈라지는 점이 시점입니다."
|
||||
assert got["lone"].startswith("다른 선에 닿은 끝이 없습니다")
|
||||
|
||||
@@ -151,6 +151,8 @@ const PATHS = {
|
||||
// 584-3 — 지도 겹(산사태취약지역)
|
||||
alertOctagon:
|
||||
"M12.802 2.165l5.575 2.389c.48 .206 .863 .589 1.07 1.07l2.388 5.574c.22 .512 .22 1.092 0 1.604l-2.389 5.575c-.206 .48 -.589 .863 -1.07 1.07l-5.574 2.388c-.512 .22 -1.092 .22 -1.604 0l-5.575 -2.389a2.036 2.036 0 0 1 -1.07 -1.07l-2.388 -5.574a2.036 2.036 0 0 1 0 -1.604l2.389 -5.575c.206 -.48 .589 -.863 1.07 -1.07l5.574 -2.388a2.036 2.036 0 0 1 1.604 0z|M12 8v4|M12 16h.01",
|
||||
// 585-3 — 연도 구역 마커(D03 도구줄 끌어 놓기)
|
||||
flag: "M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0v9a5 5 0 0 1 -7 0a5 5 0 0 0 -7 0v-9z|M5 21v-7",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
@@ -31,6 +31,7 @@ export const MAP_TOOLS = {
|
||||
footprint: { icon: "road", title: "절/성토선", toggle: true },
|
||||
measure: { icon: "ruler", title: "거리 재기", toggle: true },
|
||||
"station-snap": { icon: "magnet", title: "측점 스냅", toggle: true },
|
||||
"year-mark": { icon: "flag", title: "연도 구역 마커 — 선 위로 끌어 놓기" },
|
||||
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
|
||||
|
||||
export type MapToolAct = keyof typeof MAP_TOOLS;
|
||||
|
||||
Reference in New Issue
Block a user