@@ -1,39 +1,41 @@
/* =============================================================================
* B05_Profile_UI_ExpectedDraw.ts
* B05 평면 예정노선 변경 (PLAN 626-3 · 요구 530-11 · 12 · 535-1 · 536 · 결정 D3 · D4 · D9) — 도구줄 「예정노선 변경」 켬끔 .
* 켬 = 예정노선이 맨 위 · 예정노선 페이지(D03) 긋기 · 고르기 · 고치기를 **그대로**(같은 함수 · 단추 · 단축키):
* 긋기 칸 `createPlanDraw`(방식 칸 · 부채꼴 · +/− 색 · Shift · Tab · Enter · Esc · Backspace · 선 끝 이어 긋기) ·
* 고르기 겹 `createPlanEdit`(노드 → 선 → 구역 → 전체 · 끌어 고르기 · 노드 끌기 · 고른 토막 끌어 꺾임점 · Delete ) ·
* 우클릭 메뉴 `createPlanMenu`(여기서 긋기 시작 · 이어 긋기 · 노드 지움 · 다시 긋기) · 속성 판 `createPlanProps`([다시 긋기] ·
* [노드 지움] · [선 지움] · [구역 지움] · [전체 지움]) — 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features` · 갈래 · 둘째 선 · 연도 · 지점).
* 캔버스 누름 · 맴돎 · 두 번 클릭 · 오른쪽 클릭을 먼저 받음(키트 `bindPlanInput` capture) = 계획선 · 구조물 · 등고선 고르기 · 재기 · 노드 넣기 막음 ·
* B05 평면 예정노선 수정 (PLAN 626-3 · 635-3 · 요구 530 · 535-1 · 536 · 546) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음 .
* 왼쪽 칸 = 키트 「위치」(주소 · 장소 찾기 → 지도 이동만) · 「그리기」(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 ·
* 길이 비교 · [받은 노선으로]) — 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features`).
* 켬 = 지도 누름 · 키를 먼저 받음(키트 `bindPlanInput` capture) · 다른 계획노선 그림 어둡게(`--draw-map-dim` · 장면 `overAll.dim` ) ·
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창(직접 고친 평면 버림 · 예정노선 따라 통째 다시 추적) →
* `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` = 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과
* 같은 길 + 초안 `expected`(`expected_route_edit.csv`). 선이 없으면(다 지움) 평면 그대로 · 새로 그을 때까지 기다림.
* 예정노선 세 벌 = 파일입력(`expected_route.csv` · 안 덮음 · 도구줄 「예정노선(원본)」 켬끔 = 옅은 점선) · 저장본
* (`expected_edited`) · 이번 고침(`edit` · 걸음 사진에 실림 ).
* 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2) — 고친 뒤엔 고친 꼭짓점.
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창 → `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` =
* 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
* [받은 노선으로] = 받은 꼭짓점으로 다시 추적 · 초안 `expected` = [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 는 그대로.
* 예정노선 세 벌 = 받은 노선(`expected_route.csv` · 안 덮음 · 고쳤으면 옅은 점선) · 저장본 (`expected_edited`) · 이번 고침(`edit`).
* ========================================================================== */
import { keyIs } from "../A00_Common/key_registry" ;
import { listenWhileAlive } from "@ui/ui_template_window_watch" ;
import { showToast } from "@ui/ui_template_elements" ;
import { createFloatPanel } from "@ui/ui_template_float_panel" ;
import { createButton , showToast } from "@ui/ui_template_elements" ;
import { askConfirm } from "@ui/ui_template_modal" ;
import { createText } from "@ui/ui_template_text" ;
import { createToggle } from "@ui/ui_template_toggle" ;
import { themeColor } from "@ui/ui_template_palette" ;
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender" ;
import {
createTmNormalizer ,
normalizedToScreen ,
type ViewState ,
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender" ;
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch" ;
import { normOfLonLat } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare" ;
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler" ;
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan" ;
import { createPlanDraw } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw " ;
import { createPlanEdit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit " ;
import { bindPlanInput } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_K it" ;
import type { PlanMapLayer , ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map" ;
import { createPlanMenu } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Menu" ;
import { createPlanProps } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Props" ;
import { bindPlanInput , createPlanKit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit " ;
import type { ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map " ;
import { createPlaceFind } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_S ite " ;
import { requestJson , type RoutePlanResponse } from "./B05_Profile_Api_Replan" ;
import { loadDesignTerrainGrid , type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid" ;
import { fillPlanPanel } from "./B05_Profile_UI_Panel_Plan" ;
import { markShown } from "./B05_Profile_UI_RouteEdit_Marks" ;
import { polylineLengthM } from "./B05_Profile_UI_RouteEdit_Render" ;
import type { RouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome" ;
import type { MapRotation } from "./B05_Profile_UI_RouteEdit_Rotate" ;
import { flattenServerPlan , type FlattenedPlan } from "./logic/B05_Profile_UI_RouteEdit_Edits" ;
@@ -43,6 +45,7 @@ type Vertex = [number, number];
const HINT =
"[긋기] · 우클릭 「여기서 긋기 시작」 · 선 끝에서 이어 긋기 · 고르고 Delete · 노드 끌기 — 한 번 고칠 때마다 평면선을 다시 추적합니다." ;
const EMPTY_HINT = "예정노선이 없습니다 — 빈 땅 우클릭 「여기서 긋기 시작」으로 새로 긋습니다." ;
const OFF_HINT = "「예정노선 수정」을 켜면 지도에서 예정노선을 고칩니다." ;
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
type FollowResponse = Pick < RoutePlanResponse , "planned" | "nodes" | "curves" | "node_edits" > ;
@@ -50,40 +53,47 @@ type FollowResponse = Pick<RoutePlanResponse, "planned" | "nodes" | "curves" | "
export interface ExpectedDrawParams {
projectId : string ;
canvas : HTMLCanvasElement ;
/** 평면 뼈대 — 도구줄 칸(변경 켬끔 · 원본 켬끔) · 긋기 판 자리(도구 줄) · 속성 판 · 메뉴 자리(지도 칸). */
chrome : Pick < RouteEditChrome , "overlay" | "expectedDrawButton" | "expectedOriginalButton" > ;
/** 평면 뼈대 — 속성 판 · 메뉴 자리(지도 칸). */
chrome : Pick < RouteEditChrome , "overlay" > ;
toScreen : ( vertex : Vertex ) = > [ number , number ] ;
toMetric : ( px : number , py : number ) = > Vertex ;
/** 지도 돌림 — 보이는 px ↔ 그리기 px · 글자 세울 각(만든 뒤에 서므로 함수로). */
rotation : ( ) = > Pick < MapRotation , "unrotate" | "rerotate" | "uprightRad" > ;
pxPerMeter : ( ) = > number ;
view : ( ) = > ViewState ;
/** 바탕 틀(경위도 ↔ 화면) · 보기 바꿈 — 위치 찾기 → 지도 이동. */
frame : ( ) = > { meta : VWorldMeta | null ; setView : ( next : ViewState ) = > void } ;
redraw : ( ) = > void ;
/** 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
onOpen : ( ) = > void ;
/** 다시 추적한 평면 · 고친 예정노선 — 편집기 한 걸음. */
/** 다시 추적한 평면 · 고친 예정노선(빈 = 받은 노선으로) — 편집기 한 걸음. */
onFollow : ( plan : FlattenedPlan , expected : Vertex [ ] ) = > void ;
}
export interface ExpectedDraw {
/** 노선을 읽음 — 파일입력 예정노선 · 저장본(없으면 빈). */
/** 노선을 읽음 — 받은 예정노선 · 저장본(없으면 빈). */
load : ( original : Vertex [ ] , saved : Vertex [ ] ) = > void ;
/** 이번 고침(걸음 사진 칸) — null = 연 때 그대로. */
/** 이번 고침(걸음 사진 칸) — null = 연 때 그대로 · [] = 받은 노선으로 . */
edit : ( ) = > Vertex [ ] | null ;
setEdit : ( edit : Vertex [ ] | null ) = > void ;
/** 지금 보일 예정노선(같은 배열을 돌려줌 — 재기 높이 표가 배열로 낡음을 봄). */
line : ( ) = > Vertex [ ] ;
paint : ( context : CanvasRenderingContext2D ) = > void ;
/** 아래 그림 불투명(켠 동안 `--draw-map-dim`). */
dim : ( ) = > number ;
}
const sameLine = ( a : ReadonlyArray < Vertex > , b : ReadonlyArray < Vertex > ) : boolean = >
JSON . stringify ( a ) === JSON . stringify ( b ) ;
const meters = ( line : ReadonlyArray < Vertex > ) : string = >
Math . round ( polylineLengthM ( line ) ) . toLocaleString ( ) ;
export function createExpectedDraw ( params : ExpectedDrawParams ) : ExpectedDraw {
const { canvas } = params ;
const { expectedDrawButton : button , expectedOriginalButton : originalButton } = params . chrome ;
const viewport = params . chrome . overlay . querySelector < HTMLElement > ( ".b05-routeedit__canvas-wrap" ) ! ;
let original : Vertex [ ] = [ ] ;
/** 받은 원본 꼭짓점(635-2 · 조밀화 전) — 못 받으면 빈(= 조밀 점 `original`). */
let source : Vertex [ ] = [ ] ;
let saved : Vertex [ ] = [ ] ;
let edit : Vertex [ ] | null = null ;
let open = false ;
@@ -92,86 +102,100 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
/** 켠 뒤 확인창을 물었나 — 다시 추적 뒤 평면은 손 고침이 없음(켠 동안 계획선 안 집힘). */
let asked = false ;
let grid : DesignTerrainGrid | null = null ;
/** 고치는 판(D03 꼴 · 한 선) · 긋기 시작 때 판(끝나면 바뀜을 봄). */
/** 고치는 판(예정노선 꼴 · 한 선) · 판을 지은 꼭짓점 · 긋기 시작 때 판(끝나면 바뀜을 봄). */
let network : PlanNetwork = { lines : [ ] , pieces : [ ] } ;
let base : Vertex [ ] = [ ] ;
let strokeBase : PlanNetwork | null = null ;
const line = ( ) : Vertex [ ] = >
edit === null ? ( saved . length > 1 ? saved : original ) : edit . length > 1 ? edit : original ;
const received = ( ) : Vertex [ ] = > ( source . length > 1 ? source : original ) ;
const changed = ( ) : boolean = > line ( ) !== original && original . length > 1 ;
const z = ( x : number , y : number ) : number = > ( grid ? gridHeightAt ( grid . grid , x , y ) : NaN ) ;
const fromLine = ( ) : PlanNetwork = > {
const shown = line ( ) ;
const vertices = shown . map ( ( [ x , y ] ) : Vertex = > [ x , y ] ) ;
return { lines : shown.length > 1 ? [ { id : "main" , vertices , branch : null } ] : [ ] , pieces : [ ] } ;
base = ( shown === original ? received ( ) : shown ) . map ( ( [ x , y ] ) : Vertex = > [ x , y ] ) ;
const vertices = base . map ( ( [ x , y ] ) : Vertex = > [ x , y ] ) ;
return { lines : base.length > 1 ? [ { id : "main" , vertices , branch : null } ] : [ ] , pieces : [ ] } ;
} ;
// ── D03 긋기 · 고르기 · 메뉴 · 속성 판(같은 함수) ─────── ──
const draw = createPlanDraw ( {
page : "b05" ,
network : ( ) = > network ,
heightAt : ( ) = > z ,
commit ,
onMode ,
} ) ;
const show = ( next : PlanNetwork ) : void = > {
network = next ;
// ── 위치 칸 — 찾기 → 지도 가운데로(지형 받기 · 기준점 없음 · 기능 표 「terrain」 숨김) ──
// ponytail: 경위도 → 화면 = 바탕 `tm_grid` 늘려 씀(50 km 28 m · 180 km 1.7 km 어긋남) — 먼 곳까지 맞출 땐 서버 변환.
const find = createPlaceFind ( ( hit ) = > {
const { meta , setView } = params . frame ( ) ;
const view = params . view ( ) ;
if ( ! meta || view . width <= 0 ) return ;
const [ sx , sy ] = normalizedToScreen (
view ,
. . . normOfLonLat ( createTmNormalizer ( meta ) , hit . lon , hit . lat ) ,
) ;
setView ( {
. . . view ,
offsetX : view.offsetX + view . width / 2 - sx ,
offsetY : view.offsetY + view . height / 2 - sy ,
} ) ;
params . redraw ( ) ;
} ;
// `heightAt` = 625-3(놓인 노드 부호 색) 덧 칸 — 합치기 앞뒤 둘 다 맞게 칸 묶음을 따로 둠
const editParams = {
network : ( ) = > network ,
base : ( ) : Vertex [ ] = > [ ] ,
commit ,
show ,
commitPoints : ( _points : Vertex [ ] , next : PlanNetwork ) = > commit ( next ) ,
showPoints : ( _points : Vertex [ ] , next : PlanNetwork ) = > show ( next ) ,
enabled : ( ) = > open && draw . mode ( ) === "idle" ,
editable : ( ) = > open && ! busy ,
heightAt : ( ) = > z ,
onSelect : ( ) : void = > ( props . render ( ) , params . redraw ( ) ) ,
} ;
const editor = createPlanEdit ( editParams ) ;
const props = createPlanProps ( {
} ) ;
// ── 예정노선 키트(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 = 예정노선 페이지와 같은 한 벌) ──
const kit = createPlanKit ( {
page : "b05" ,
network : ( ) = > network ,
edit : editor ,
editable : ( ) = > ! busy ,
commit ,
show : ( next ) = > {
network = next ;
params . redraw ( ) ;
} ,
heightAt : ( ) = > z ,
commitPoints : ( _points , next ) = > commit ( next ) ,
respan : draw.respan ,
} ) ;
const menu = createPlanMenu ( {
page : "b05" ,
network : ( ) = > network ,
edit : editor ,
draw ,
editable : ( ) = > open && ! busy ,
respan : draw.respan ,
idle : ( ) = > open ,
onMode ,
onSelect : ( ) = > ( kit . props . render ( ) , params . redraw ( ) ) ,
cards : { site : { parts : [ find . root ] } } ,
} ) ;
const layers : PlanMapLayer [ ] = [ draw . layer , editor . layer , menu . layer ] ;
const { draw , edit : editor , props , menu } = kit ;
const layers = kit . layers ( [ ] , [ ] ) ;
props . panel . classList . add ( "b05-routeedit__expected-props" ) ;
viewport . append ( props . panel ) ;
menu . mount ( viewport ) ;
// ── 켬끔 판 ─────────────────────────────────────────── ──
const panel = createFloatPanel ( { title : "예정노선 변경" , onClose : ( ) = > setOpen ( false ) } ) ;
panel . root . classList . add ( "b05-routeedit__expected-draw" ) ;
panel . root . hidden = true ;
const hint = createText ( HINT , "muted" ) ;
panel . body . append ( draw . panel , hint ) ;
params . chrome . overlay . querySelector ( ".b05-routeedit__tools" ) ! . append ( panel . root ) ;
button . addEventListener ( "click" , ( ) = > setOpen ( ! open ) ) ;
const paintOriginalButton = ( ) : void = > {
originalButton . setAttribute ( "aria-pressed" , String ( showOriginal ) ) ;
originalButton . classList . toggle ( "is-active" , showOriginal ) ;
} ;
paintOriginalButton ( ) ;
originalButton . addEventListener ( "click" , ( ) = > {
showOriginal = ! showOriginal ;
paintOriginalButton ( ) ;
params . redraw ( ) ;
// ── 그리기 칸 — 머리 「예정노선 수정」 · 받은 노선 · 길이 비교 · [받은 노선으로] ──
const drawCard = kit . cards . find ( ( card ) = > card . contains ( draw . panel ) ) ! ;
const toggle = createToggle ( { label : "예정노선 수정" , size : 20 , onChange : ( on ) = > setOpen ( on ) } ) ;
toggle . root . addEventListener ( "click" , ( event ) = > event . stopPropagation ( ) ) ; // 칸 접기로 안 올라감
drawCard . firstElementChild ! . append ( toggle . root ) ;
const originalToggle = createToggle ( {
label : "받은 노선" ,
checked : true ,
size : 20 ,
onChange : ( on ) = > ( ( showOriginal = on ) , params . redraw ( ) ) ,
} ) ;
const resetButton = createButton ( {
label : "받은 노선으로" ,
variant : "ghost" ,
size : 24 ,
onClick : ( ) = > void follow ( received ( ) , [ ] ) ,
} ) ;
const compare = createText ( "" , "muted" ) ;
const hint = createText ( OFF_HINT , "muted" ) ;
const row = document . createElement ( "div" ) ;
row . className = "b05-routeedit__expected-row" ;
row . append ( originalToggle . root , resetButton ) ;
drawCard . append ( row , compare , hint ) ;
fillPlanPanel ( {
site : kit.cards.find ( ( card ) = > card . contains ( find . root ) ) ? ? null ,
draw : drawCard ,
} ) ;
function syncCard ( ) : void {
const edited = changed ( ) ;
resetButton . disabled = ! edited || busy ;
compare . textContent = edited
? ` 받은 노선 ${ meters ( received ( ) ) } m → 고친 노선 ${ meters ( line ( ) ) } m `
: "" ;
compare . hidden = ! edited ;
hint . textContent = ! open ? OFF_HINT : network.lines.length ? HINT : EMPTY_HINT ;
}
function setOpen ( next : boolean ) : void {
if ( draw . mode ( ) !== "idle" ) draw . setMode ( "idle" ) ;
@@ -180,21 +204,19 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
network = fromLine ( ) ;
editor . select ( [ ] ) ;
draw . setEditable ( open ) ;
button . setAttribute ( "aria-pressed" , String ( open ) ) ;
button . classList . toggle ( "is-active" , open ) ;
panel . root . hidden = ! open ;
hint . textContent = network . lines . length ? HINT : EMPTY_HINT ;
toggle . set ( open ) ;
if ( open ) {
params . onOpen ( ) ;
void loadDesignTerrainGrid ( params . projectId ) . then (
( loaded ) = > ( ( grid = loaded ) , params . redraw ( ) ) ,
) ;
}
syncCard ( ) ;
props . render ( ) ;
params . redraw ( ) ;
}
/** 꼴 한 걸음(D03 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
/** 꼴 한 걸음(예정노선 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
function commit ( next : PlanNetwork ) : void {
network = next ;
props . render ( ) ;
@@ -208,44 +230,49 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
strokeBase ? ? = network ;
return params . redraw ( ) ;
}
const base = strokeBase ;
const before = strokeBase ;
strokeBase = null ;
if ( base && JSON . stringify ( base . lines ) !== JSON . stringify ( network . lines ) ) void apply ( ) ;
if ( before && JSON . stringify ( before . lines ) !== JSON . stringify ( network . lines ) ) void apply ( ) ;
params . redraw ( ) ;
}
function revert ( ) : void {
network = fromLine ( ) ;
editor . sync ( ) ;
draw . sync ( ) ;
props . render ( ) ;
kit . sync ( ) ;
syncCard ( ) ;
params . redraw ( ) ;
}
/** 다시 추적 — 한 선 · 점 2 이상일 때만 · 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음 . */
/** 고친 판 → 다시 추적( 한 선 · 점 2 이상 · 판을 지은 꼭짓점과 다를 때만) . */
async function apply ( ) : Promise < void > {
if ( busy || ! open ) return ;
const drawn = network . lines . filter ( ( entry ) = > entry . vertices . length > 1 ) ;
hint . textContent = drawn . length ? HINT : EMPTY_HINT ;
syncCard ( ) ;
if ( ! drawn . length ) return ; // 다 지움 — 새로 그을 때까지 평면 그대로
if ( drawn . length > 1 ) {
showToast ( "예정노선은 한 선입니다." , "info" ) ;
return revert ( ) ;
}
const points = drawn [ 0 ] . vertices . map ( ( [ x , y ] ) : Vertex = > [ x , y ] ) ;
if ( sameLine ( points , line ( ) ) ) return ;
if ( ! sameLine ( points , base ) ) await follow ( points , points ) ;
}
/** 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). */
async function follow ( points : Vertex [ ] , expected : Vertex [ ] ) : Promise < void > {
if ( busy || points . length < 2 ) return ;
if ( ! asked ) {
const ok = await askConfirm ( {
title : "평면선 다시 추적" ,
message :
"고친 예정노선을 따라 평면선을 통째로 다시 추적합니다 — 직접 고친 평면(노드 · 곡선)은 버립니다. " +
"예정 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다." ,
"받은 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다." ,
okLabel : "다시 추적" ,
} ) ;
if ( ! ok ) return revert ( ) ;
asked = true ;
}
busy = true ;
syncCard ( ) ;
params . redraw ( ) ;
try {
const plan = await requestJson < FollowResponse > (
@@ -255,7 +282,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
) ;
params . onFollow (
flattenServerPlan ( plan . nodes ? ? [ ] , plan . curves ? ? [ ] , plan . node_edits ) ,
points ,
expected ,
) ;
} catch ( error ) {
showToast (
@@ -265,11 +292,12 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
revert ( ) ;
} finally {
busy = false ;
syncCard ( ) ;
params . redraw ( ) ;
}
}
// ── 누름 · 키 — 켠 동안 먼저 받아 D03 겹에 넘김(계획선 · 구조물 · 재기로 안 샘) ──
// ── 누름 · 키 — 켠 동안 먼저 받아 예정노선 겹에 넘김(계획선 · 구조물 · 재기로 안 샘) ──
const map = ( ) : ScreenMap = > {
const rotation = params . rotation ( ) ;
return {
@@ -294,10 +322,6 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
unrotate : rotation.unrotate ,
} ;
} ;
const claim = ( event : Event ) : void = > {
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
} ;
bindPlanInput ( {
canvas ,
at : ( event ) = > {
@@ -311,10 +335,11 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
} ) ;
const onKey = ( event : KeyboardEvent ) : void = > {
if ( ! open || ( event . target as HTMLElement | null ) ? . closest ? . ( "input, select, textarea" ) ) return ;
if ( ! open ) return ;
if ( keyIs ( event , "plan.undo" ) || keyIs ( event , "plan.redo" ) ) return ; // 되돌리기 = 페이지 사진첩
if ( ! ( draw . key ( event ) || editor . key ( event ) ) ) return ;
claim ( event ) ;
if ( ! kit . key ( event ) ) return ;
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
params . redraw ( ) ;
} ;
listenWhileAlive ( window , "keydown" , canvas , onKey , true ) ;
@@ -332,7 +357,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
context . globalAlpha = 0.7 ;
context . setLineDash ( [ 2 , 5 ] ) ;
context . lineWidth = 2 ;
context . strokeStyle = themeColor ( "--draw-text-secondary" , "#9ca3af" ) ; // 원본 = 옅은 점선
context . strokeStyle = themeColor ( "--draw-text-secondary" , "#9ca3af" ) ; // 받은 노선 = 옅은 점선
context . stroke ( ) ;
context . restore ( ) ;
}
@@ -345,7 +370,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
}
}
// 검증 창구 — 켬 · 긋기 모드 · 고친 판 · 보이는 예정노선 · 사업지 좌표 → 보이는 화면 px
// 검증 창구 — 켬 · 긋기 모드 · 고친 판 · 보이는 예정노선 · 받은 꼭짓점 · 사업지 좌표 → 보이는 화면 px
Object . assign ( canvas , {
expectedProbe : {
state : ( ) = > ( {
@@ -355,9 +380,11 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
preview : draw.preview ( ) ,
selected : editor.selected ( ) ? . kind ? ? null ,
busy ,
compare : compare.textContent ,
} ) ,
line ,
original : ( ) = > original ,
source : ( ) = > source ,
edit : ( ) = > edit ,
screen : ( vertex : Vertex ) = > params . rotation ( ) . rerotate ( . . . params . toScreen ( vertex ) ) ,
} ,
@@ -368,16 +395,31 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
original = nextOriginal ;
saved = nextSaved ;
network = fromLine ( ) ;
syncCard ( ) ;
void requestJson < { expected_source? : Vertex [ ] } > (
` /projects/ ${ params . projectId } /route/plan/source ` ,
{ } ,
30000 ,
)
. then ( ( body ) = > {
source = body . expected_source ? ? [ ] ;
if ( ! open ) network = fromLine ( ) ;
syncCard ( ) ;
} )
. catch ( ( ) = > undefined ) ; // 못 받음 = 조밀 점 그대로
} ,
edit : ( ) = > edit ,
setEdit ( next ) {
edit = next ;
syncCard ( ) ;
if ( draw . mode ( ) !== "idle" ) return ; // 긋는 중 되돌림 — 긋기가 끝나면 그 판으로 다시 추적
network = fromLine ( ) ;
editor . sync ( ) ;
props . render ( ) ;
syncCard ( ) ;
} ,
line ,
paint ,
dim : ( ) = > ( open ? parseFloat ( themeColor ( "--draw-map-dim" , "0.35" ) ) || 0.35 : 1 ) ,
} ;
}