feat(771-1·771-2): B05 예정노선 수정 — 중간 노드에서 긋기 · 닿은 노드 사이 갈음 · 지움 뒤 긋기 Esc = 지움 전
- 771-2: 한 선 페이지(B05) 중간 노드 누름 = 그 노드에서 긋기 · 같은 선 다른 노드에 닿으면 그 사이 갈음(rejoinSpan · 거꾸로 됨 · 한 줄) · 같은 노드 · 안 닿고 끝냄 = 꼴 그대로 + 안내 · 되돌리기 한 걸음 · D03 는 중간 꺾임점 = 갈래 그대로 - 771-1: 긋기 Esc = 긋기 직전 꼴 · 지움(노드 · 토막 · 구역 · 전체 · 잇단 지움) 바로 뒤 긋기면 지움 전 꼴(D03 같은 부품) - 시험: test_771_2_rejoin_span 새로 · test_626_3 의 lineLimitBlock 줄 문자열 대조를 새 꼴(branches 변수)로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
* 닿아 끝나면 저절로 그 점의 갈래(No.0 = 닿은 점 · 599 · 요구서 463 3장 10).
|
||||
* 다시 긋기(599-3 · `respan`) = 고른 구간 앞 노드부터 긋고 원래 뒤 노드에 붙으면 끝(속 구간 흐리게 ·
|
||||
* Enter 로 끝내면 취소) — 끝나면 `replaceSpan` 한 번 = 되돌리기 한 걸음.
|
||||
* 중간 노드에서 긋기(771-2 · 갈래 숨긴 페이지 = B05) = 다시 긋기의 뒤 노드 열림 — 같은 선 다른 노드에 닿으면
|
||||
* 그 사이 갈음(`rejoinSpan` · 거꾸로 됨) · 같은 노드 · 안 닿고 끝냄 = 꼴 그대로 + 안내.
|
||||
* Esc(771-1) = 이번 긋기 직전 꼴 · 지움 바로 뒤 긋기면 지움 전 꼴(`dropBase`) · 다시 긋기 = 꼴 그대로.
|
||||
* 미리보기 = 마지막 점 → 마우스 길이(m) · 가상 종단 기울기(원지반 % · ° · + 오르막) — 제약 칸 넷:
|
||||
* 자유 · 길이 고정 · 기울기 고정(기울기 고정 · 길이 자유 · 마우스 방향 첫 자리) · 길이·기울기 고정(양각기 원 위 후보 중
|
||||
* 마우스에 가까운 것) · 제약 기울기 = 오르내림 같은 크기(562-1 짝이 |기울기|로 셈).
|
||||
@@ -46,7 +49,7 @@ import {
|
||||
type PlanNetwork,
|
||||
type Vertex,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { nodeRecord, replaceSpan, respanBlocked } from "./logic/D03_PlanAnalysis_Span";
|
||||
import { nodeRecord, rejoinSpan, replaceSpan, respanBlocked } from "./logic/D03_PlanAnalysis_Span";
|
||||
import {
|
||||
fmt,
|
||||
lineAt,
|
||||
@@ -66,6 +69,10 @@ import { featureOn, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import type { PlanMapLayer, ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
const START_HINT = "지점 · 선 끝 · 선 위를 눌러 시작합니다 — 빈 땅은 우클릭 「여기서 긋기 시작」.";
|
||||
/** 중간 노드에서 긋다 다른 노드에 안 닿고 끝냄(771-2). */
|
||||
const RESPAN_MISS = "다시 긋기는 뒤 노드에 붙어야 끝 — 취소했습니다.";
|
||||
const REJOIN_MISS =
|
||||
"다른 노드에 닿지 않아 반영하지 않았습니다 — 다른 노드에 닿으면 그 사이가 바뀝니다.";
|
||||
|
||||
export type DrawMode = "idle" | "draw";
|
||||
|
||||
@@ -75,7 +82,7 @@ export function lineLimitBlock(
|
||||
network: PlanNetwork,
|
||||
): string | null {
|
||||
if (kind === "line" || kind === "point")
|
||||
return "예정노선은 한 선입니다 — 선 끝에서 이어 긋습니다.";
|
||||
return "예정노선은 한 선입니다 — 선 끝에서 이어 긋거나 노드에서 시작해 다른 노드에 닿게 긋습니다.";
|
||||
if (kind === "empty" && network.lines.some((line) => line.vertices.length > 1))
|
||||
return "예정노선은 한 선입니다 — 지운 뒤 새로 긋습니다.";
|
||||
return null;
|
||||
@@ -107,6 +114,8 @@ export interface PlanDrawParams {
|
||||
commit: (next: PlanNetwork) => void;
|
||||
/** 모드 · 그리는 선이 바뀜 — 페이지가 단추 · 안내를 다시. */
|
||||
onMode: () => void;
|
||||
/** 지금 꼴이 지움 바로 뒤면 지움 전 꼴(771-1 — 지움 → 긋기 중 Esc = 지움까지 되살림) · 아니면 null. */
|
||||
dropBase?: () => PlanNetwork | null;
|
||||
}
|
||||
|
||||
export interface PlanDraw {
|
||||
@@ -138,17 +147,18 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
let active: number | null = null; // 그리는 선 차례(첫 누름 전 = null)
|
||||
let flip = false; // 끝에서 긋는 중 — 끝낼 때 뒤집음
|
||||
let strokeStart: PlanNetwork | null = null; // Esc 로 돌아갈 꼴
|
||||
let restore: PlanNetwork | null = null; // 지움 바로 뒤 긋기 = Esc 로 돌아갈 지움 전 꼴(771-1)
|
||||
let preview: DrawPreview | null = null;
|
||||
let fan: Fan | null = null;
|
||||
let kind: DrawConstraint = "both"; // 625-1 · 기억 안 함(B9)
|
||||
let hoverAt: { px: number; py: number; map: ScreenMap; shift: boolean } | null = null; // Tab · Shift 뒤 미리보기 다시
|
||||
let lengthM = DRAW_DEFAULT_LENGTH_M;
|
||||
let slopePct = DRAW_DEFAULT_SLOPE_PCT;
|
||||
// 다시 긋기 — 선 id · 앞 · 뒤 꺾임점 · 새로 찍은 속 점 · 그 기록
|
||||
// 다시 긋기 — 선 id · 앞 · 뒤 꺾임점(null = 중간 노드에서 긋기 · 같은 선 어느 노드든 · 771-2) · 새로 찍은 속 점 · 그 기록
|
||||
let span: {
|
||||
line: string;
|
||||
from: number;
|
||||
to: number;
|
||||
to: number | null;
|
||||
through: Vertex[];
|
||||
records: NodeRecord[];
|
||||
} | null = null;
|
||||
@@ -207,6 +217,20 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 중간 노드에서 긋기 붙을 자리(771-2) — 같은 선 노드 중 가장 가까운 것 · 시작 노드는 한 점 찍은 뒤만. */
|
||||
function nodeSnap(
|
||||
vertices: Vertex[],
|
||||
px: number,
|
||||
py: number,
|
||||
map: ScreenMap,
|
||||
): { point: Vertex; note: string; onLine: null } | null {
|
||||
const best = vertices
|
||||
.map((point, i) => ({ point, i, gap: screenGap(map, point, px, py) }))
|
||||
.filter((item) => item.gap <= SNAP_PX && (item.i !== span!.from || span!.through.length > 0))
|
||||
.sort((a, b) => a.gap - b.gap)[0];
|
||||
return best ? { point: best.point, note: "노드에 닿음", onLine: null } : null;
|
||||
}
|
||||
|
||||
/** 앞 구간 시작 — 정방향 부채꼴(625-2 B5) · 첫 구간 = null · 갈래 첫 구간 = 붙은 선 들어온 쪽. */
|
||||
function prevOf(): Vertex | null {
|
||||
const network = params.network();
|
||||
@@ -244,13 +268,15 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
const z = params.heightAt();
|
||||
const cursor = map.toMetric(px, py);
|
||||
fan = kind === "both" ? compassFan(from, lengthM, slopePct, z, prevOf(), cursor) : null;
|
||||
// 다시 긋기 = 원래 뒤 노드에만 붙음
|
||||
const end = spanVertices?.[span!.to];
|
||||
// 다시 긋기 = 원래 뒤 노드에만 붙음 · 중간 노드에서 긋기 = 같은 선 노드(시작 노드는 한 점 찍은 뒤부터)
|
||||
const end = span?.to === null ? undefined : spanVertices?.[span!.to];
|
||||
const snap = end
|
||||
? screenGap(map, end, px, py) <= SNAP_PX
|
||||
? { point: end, note: "뒤 노드에 붙음", onLine: null }
|
||||
: null
|
||||
: snapAt(px, py, map);
|
||||
: spanVertices
|
||||
? nodeSnap(spanVertices, px, py, map)
|
||||
: snapAt(px, py, map);
|
||||
let point: Vertex | null = cursor;
|
||||
let note: string | null = null;
|
||||
let lengthOnly = false;
|
||||
@@ -283,6 +309,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
// ── 모드 ────────────────────────────────────────────────
|
||||
/** 이번 긋기 마침 — 점 하나뿐인 선 버림 · 끝에서 그은 선은 뒤집고 머리 잇기 · 꼬리가 다른 선 머리면 잇기. */
|
||||
function stop(): void {
|
||||
if (span) showToast(span.to === null ? REJOIN_MISS : RESPAN_MISS, "info");
|
||||
let network = params.network();
|
||||
const line = active;
|
||||
const entry = line === null ? null : network.lines[line];
|
||||
@@ -315,6 +342,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
active = null;
|
||||
flip = false;
|
||||
strokeStart = null;
|
||||
restore = null;
|
||||
preview = null;
|
||||
fan = null;
|
||||
span = null;
|
||||
@@ -330,6 +358,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
}
|
||||
setMode("draw");
|
||||
span = { line: lineId, from, to, through: [], records: [] };
|
||||
restore = params.dropBase?.() ?? null;
|
||||
panelState();
|
||||
params.onMode();
|
||||
return true;
|
||||
@@ -383,12 +412,24 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
function begin(px: number, py: number, map: ScreenMap, free = false): boolean {
|
||||
const network = params.network();
|
||||
const target = aim(px, py, map);
|
||||
const single = featureOn(params.page, "branch") ? null : lineLimitBlock(target.kind, network);
|
||||
const branches = featureOn(params.page, "branch");
|
||||
// 한 선 페이지(B05) 중간 노드 = 그 노드에서 긋기 · 다른 노드에 닿으면 그 사이 갈음(771-2 · 닿기 전엔 꼴 그대로)
|
||||
const vertex = target.kind === "line" ? target.hit.vertex : null;
|
||||
if (!branches && target.kind === "line" && vertex !== null && vertex > 0) {
|
||||
const line = network.lines[target.hit.line];
|
||||
if (vertex < line.vertices.length - 1) {
|
||||
span = { line: line.id, from: vertex, to: null, through: [], records: [] };
|
||||
restore = params.dropBase?.() ?? null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const single = branches ? null : lineLimitBlock(target.kind, network);
|
||||
if (single) {
|
||||
showToast(single, "info");
|
||||
return false;
|
||||
}
|
||||
strokeStart = network;
|
||||
restore = params.dropBase?.() ?? null;
|
||||
if (target.kind === "tip") {
|
||||
if (target.head) {
|
||||
const turned = reverseLine(network, target.line);
|
||||
@@ -453,11 +494,17 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
span.records.push(record);
|
||||
return true;
|
||||
}
|
||||
const next = replaceSpan(
|
||||
const vertices = params.network().lines[spanLine()].vertices;
|
||||
const end =
|
||||
span.to ??
|
||||
(samePoint(vertices[span.from], preview.point)
|
||||
? span.from
|
||||
: vertices.findIndex((v) => samePoint(v, preview!.point)));
|
||||
const next = (span.to === null ? rejoinSpan : replaceSpan)(
|
||||
params.network(),
|
||||
spanLine(),
|
||||
span.from,
|
||||
span.to,
|
||||
end,
|
||||
span.through,
|
||||
span.records,
|
||||
);
|
||||
@@ -486,7 +533,10 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
let network = params.network();
|
||||
let stroke = active;
|
||||
const index = spanLine();
|
||||
const dim = span && index >= 0 ? { line: index, from: span.from, to: span.to } : null;
|
||||
const dim =
|
||||
span && span.to !== null && index >= 0
|
||||
? { line: index, from: span.from, to: span.to }
|
||||
: null;
|
||||
if (span && index >= 0) {
|
||||
// 다시 긋는 선 = 앞 노드 + 찍은 점(그리는 선 꼴)
|
||||
const head = network.lines[index].vertices[span.from];
|
||||
@@ -568,20 +618,24 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
return true;
|
||||
}
|
||||
if (mode === "draw" && keyIs(event, "plan.drawFinish")) {
|
||||
if (span) showToast("다시 긋기는 뒤 노드에 붙어야 끝 — 취소했습니다.", "info");
|
||||
setMode("idle");
|
||||
setMode("idle"); // 다시 긋기 · 중간 노드 긋기 = 안 닿음 알림(stop)
|
||||
return true;
|
||||
}
|
||||
// Esc = 이번 수정 직전 꼴(지움 바로 뒤 긋기 = 지움 전 꼴 · 771-1) · 다시 긋기 = 꼴 그대로
|
||||
if (mode === "draw" && event.key === "Escape") {
|
||||
if (mode === "draw" && strokeStart && strokeStart !== params.network())
|
||||
params.commit(strokeStart);
|
||||
const drew = span
|
||||
? span.through.length > 0
|
||||
: !!strokeStart && strokeStart !== params.network();
|
||||
const back = (drew && restore) || strokeStart;
|
||||
if (back && back !== params.network()) params.commit(back);
|
||||
active = null; // 되돌린 꼴이라 stop 의 버림 · 뒤집기는 안 함
|
||||
span = null;
|
||||
setMode("idle");
|
||||
return true;
|
||||
}
|
||||
if (mode === "draw" && span && keyIs(event, "plan.drawBack")) {
|
||||
span.records.pop();
|
||||
if (!span.through.pop()) setMode("idle");
|
||||
if (!span.through.pop()) ((span = null), setMode("idle"));
|
||||
return true;
|
||||
}
|
||||
if (mode === "draw" && active !== null && keyIs(event, "plan.drawBack")) {
|
||||
|
||||
Reference in New Issue
Block a user