From 141bf31731eee544b6d638a30d673a3bacf7ed0b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 11:55:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05=20=ED=8F=89=EB=A9=B4):=20366-3=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EC=8A=A4=EB=83=85=20=EC=BC=AC=20=3D=20?= =?UTF-8?q?=EA=B3=A1=EC=84=A0=20=EA=B0=80=EC=9A=B4=EB=8D=B0(MC)=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=BC=EB=B0=98=EC=B8=A1=EC=A0=90=EC=97=90=20=E2=80=94=20?= =?UTF-8?q?=EC=83=88=20logic=20StationFit(=EA=B3=A0=EC=B9=9C=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=95=9E=20=EA=B3=A1=EC=84=A0=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=C2=B7=20=EA=B3=A0=EC=A0=95=20=EC=95=84=EB=8B=8C=20IP=20?= =?UTF-8?q?=EB=A5=BC=20=EB=93=A4=EC=96=B4=EC=98=A4=EB=8A=94=20=EC=A0=91?= =?UTF-8?q?=EC=84=A0=20=EB=94=B0=EB=9D=BC=20=C2=B1=EA=B0=84=EA=B2=A9/2=20?= =?UTF-8?q?=C2=B7=20=ED=95=98=ED=95=9C=20=EC=83=88=EB=A1=9C=20=EA=B9=B8=20?= =?UTF-8?q?=C2=B7=20=EC=95=9E=20MC=20=ED=9D=94=EB=93=A6=20=3D=20=EB=B2=84?= =?UTF-8?q?=EB=A6=BC)=20=C2=B7=20=EB=86=93=EC=9D=84=20=EB=95=8C=20=C2=B7?= =?UTF-8?q?=20applyEdit=20=C2=B7=20=EC=BC=A4=20=EB=95=8C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=95=9C=20=EA=B1=B8=EC=9D=8C=20=C2=B7=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=C2=B7=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=C2=B7=20=EC=B4=88=EC=95=88=20=EB=90=98?= =?UTF-8?q?=EC=82=B4=EB=A6=BC=EC=9D=80=20=EC=95=88=20=EB=A7=9E=EC=B6=A4=20?= =?UTF-8?q?=C2=B7=20=EC=83=81=ED=83=9C=EC=A4=84=20=E3=80=8C=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EB=B0=96=20=EA=B3=A1=EC=84=A0=20N=EA=B0=9C?= =?UTF-8?q?=E3=80=8D=20=C2=B7=20=EB=8B=A8=EC=B6=94=20title=20=EC=83=88=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=C2=B7=20Node=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- B05_Profile/B05_Profile_UI_RouteEdit.ts | 16 +- .../B05_Profile_UI_RouteEdit_Chrome.ts | 2 +- .../logic/B05_Profile_RouteEdit_StationFit.ts | 171 ++++++++++++++++++ .../logic/B05_Profile_UI_RouteEdit_Edits.ts | 3 + resources/tester/helper_366_3_station_fit.cjs | 114 ++++++++++++ .../tester/test_345_4_recommend_panel.py | 4 +- resources/tester/test_366_3_station_fit.py | 85 +++++++++ 7 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 B05_Profile/logic/B05_Profile_RouteEdit_StationFit.ts create mode 100644 resources/tester/helper_366_3_station_fit.cjs create mode 100644 resources/tester/test_366_3_station_fit.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 2a9cc77b8..964be9050 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -91,6 +91,7 @@ import { import type { DesignPageHost } from "../A00_Common/design/design_page"; import * as draftLink from "./B05_Profile_UI_RouteEdit_Draft"; import { bindRouteShift } from "./B05_Profile_UI_RouteEdit_Shift"; +import * as fit from "./logic/B05_Profile_RouteEdit_StationFit"; import "./B05_Profile_UI_Style_RouteEdit.css"; /** 등고선 눈금을 고를 때 쓸 라벨 예산 — B04 지도 기본값(350)보다 **넉넉히** 둔다 @@ -159,7 +160,7 @@ export async function mountRouteEditor( let apexLock: boolean[] = []; /** 지금 고른 꺾임점 — 곡선 편집줄이 이 자리를 만진다. 없으면 -1. */ let picked = -1; - /** [측점 스냅](요구 224-2) — 보기 모드라 되돌리기 밖 · 열 때 서버 값 · [확인] 때 저장. */ + /** [측점 스냅](224-2 · 366-3) — 켬 = 곡선 MC 를 일반측점에 · 켬끔 자체는 되돌리기 밖 · [저장] 때 저장. */ let stationSnap = false; let savedStationSnap = false; /** 편집 창 측점 한 곳 — 끔이면 null(간격마다) · 켬이면 호 안 하나를 곡선 중점으로. @@ -265,6 +266,7 @@ export async function mountRouteEditor( const showMinorContours = bindMinorContourToggle(minorContourButton, () => draw()); stationSnapButton.addEventListener("click", () => { setStationSnap(!stationSnap); + if (stationSnap && history) applyEdit("곡선 가운데(MC)를 일반측점에 맞췄습니다."); // 전체 · 한 걸음 void crossPreview.refresh(); // 떠 있는 횡단도 옮긴 측점 규칙으로. draw(); }); @@ -415,6 +417,7 @@ export async function mountRouteEditor( violationCount: nodeInfo.filter((node) => node.violations.length).length, minRadiusM, fresh: nodeInfo.length === 0, + offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, stationIntervalM) : 0, }); // ── 곡선 라벨 — 고른 꺾임점 옆에 뜨는 조작 패널. 배선은 `_CurveBar` 몫 ── @@ -445,8 +448,12 @@ export async function mountRouteEditor( const curveLabelBox = curveBar.label; const syncCurveBar = curveBar.sync; - /** 한 번의 편집을 마무리한다 — 다시 그리고, 라벨·상태줄을 맞추고, 되돌리기에 쌓는다. */ - function applyEdit(message: string, record = true): void { + /** 한 번의 편집을 마무리한다 — 다시 그리고, 라벨·상태줄을 맞추고, 되돌리기에 쌓는다. + * 측점 스냅 켬이면 고친 자리 앞 곡선부터 MC 를 일반측점에(366-3 · 되살림 `fitting=false` 은 안 맞춤). */ + function applyEdit(message: string, record = true, fitting = true): void { + const limits = { minRadiusM, limitRadiusM, limitArcM, intervalM: stationIntervalM }; + const from = fit.fitStart(history?.current?.(), snapshotNow()); + if (stationSnap && fitting) planned = fit.fitCurveStations(snapshotNow(), limits, from); markEdited(); writeRouteLocks(projectId, planned, apexLock); // 고정은 [취소] · 다시 열기에도 남는다(22-5). syncCurveBar(); @@ -476,7 +483,7 @@ export async function mountRouteEditor( const locks = record ? routeLocksOf(planned, apexLock) : null; ({ planned, curveOn, curveRadius, curveLock, curveArc, apexLock, picked } = snapshot); if (locks) apexLock = lockColumn(planned, locks); - applyEdit(message, record); + applyEdit(message, record, false); // 되돌리기 · 추천 [적용] · 초안 되살림은 안 맞춤(366-3) } const historyControls = bindHistoryControls({ @@ -532,6 +539,7 @@ export async function mountRouteEditor( curveHint, contourHint, commit: () => { + if (stationSnap) return applyEdit("곡선 가운데(MC)를 일반측점에 맞췄습니다."); // 놓을 때(366-3) history?.commit(snapshotNow()); historyControls.sync(); }, diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index 5e8ed273a..13021c9e8 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -85,7 +85,7 @@ export function createRouteEditChrome( + title="켜면 곡선 가운데(MC)를 가까운 일반측점에 맞춥니다 — 놓을 때 고친 자리부터 교각점을 간격 절반 안에서 밂 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장">측점 스냅