Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_6' and 'origin/sub_laptop_7' into sub_laptop_5
This commit is contained in:
@@ -100,6 +100,8 @@ export interface RoutePlanResponse {
|
||||
violation_count: number;
|
||||
/** 사용자가 고친 계획노선이 저장돼 있으면 true. */
|
||||
edited: boolean;
|
||||
/** [측점 스냅] 켬(요구 224-2) — [확인] 때 곡선표에 적은 값. 칸 없으면 끔. */
|
||||
station_snap?: boolean;
|
||||
/** 현재 초기값 — 서버가 [확인] 때 프로젝트에 적어 둔 초기노선 갈래(계획서 20). */
|
||||
current_initial?: { logic: string; weights: Record<string, number> | null };
|
||||
/** 전처리가 만든 초기 노선(예상노선 추적)의 노드 · 곡선 — [초기화]가 재계산 없이 놓는다. */
|
||||
@@ -220,6 +222,8 @@ export interface CrossPreviewRequest {
|
||||
station_interval_m: number;
|
||||
/** 횡단 반폭(m) — 없으면 서버 기본값. */
|
||||
cross_half_width_m?: number;
|
||||
/** [측점 스냅] 켬 — 호 안 측점 하나를 곡선 중점으로(요구 224-2). */
|
||||
station_snap?: boolean;
|
||||
}
|
||||
|
||||
/** 한 측점 횡단을 묻는다. 종·횡단을 한 번 돌리므로 **한두 초** 걸린다(사용자 확정: 괜찮음). */
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
viewPxPerMeter,
|
||||
} from "./B05_Profile_UI_RouteEdit_View";
|
||||
import { createCurveBar } from "./B05_Profile_UI_RouteEdit_CurveBar";
|
||||
import { curveSpans, snapCurveStations, stationsAlong } from "./B05_Profile_UI_RouteEdit_Frame";
|
||||
import {
|
||||
lockColumn,
|
||||
readRouteLocks,
|
||||
@@ -109,7 +110,7 @@ export async function openRouteEditModal(
|
||||
const stationIntervalM = options.stationIntervalM ?? 20;
|
||||
const chrome = createRouteEditChrome();
|
||||
const { overlay, canvas, status, busy, measureBox, measureText, measureButton } = chrome;
|
||||
const { minorContourButton, terrainSelect } = chrome;
|
||||
const { minorContourButton, terrainSelect, stationSnapButton } = chrome;
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
let expected: Vertex[] = [];
|
||||
@@ -138,6 +139,24 @@ export async function openRouteEditModal(
|
||||
let apexLock: boolean[] = [];
|
||||
/** 지금 고른 꺾임점 — 곡선 편집줄이 이 자리를 만진다. 없으면 -1. */
|
||||
let picked = -1;
|
||||
/** [측점 스냅](요구 224-2) — 보기 모드라 되돌리기 밖 · 열 때 서버 값 · [확인] 때 저장. */
|
||||
let stationSnap = false;
|
||||
let savedStationSnap = false;
|
||||
/** 편집 창 측점 한 곳 — 끔이면 null(간격마다) · 켬이면 호 안 하나를 곡선 중점으로.
|
||||
* 그리기 · 누름 · 기울기가 이것을 봄 · 셈은 서버 짝(`_Frame`). */
|
||||
const stations = (): number[] | null => {
|
||||
if (!stationSnap) return null;
|
||||
const line = plannedLine.length ? plannedLine : planned;
|
||||
return snapCurveStations(
|
||||
stationsAlong(line, stationIntervalM, false),
|
||||
curveSpans(line, curveInfo),
|
||||
);
|
||||
};
|
||||
const setStationSnap = (on: boolean): void => {
|
||||
stationSnap = on;
|
||||
stationSnapButton.classList.toggle("is-active", on);
|
||||
stationSnapButton.setAttribute("aria-pressed", String(on));
|
||||
};
|
||||
/** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */
|
||||
let history: RouteEditHistory | null = null;
|
||||
let meta: VWorldMeta | null = null;
|
||||
@@ -165,6 +184,7 @@ export async function openRouteEditModal(
|
||||
})),
|
||||
min_radius_m: minRadiusM || 12,
|
||||
station_interval_m: stationIntervalM,
|
||||
station_snap: stationSnap,
|
||||
}),
|
||||
onShown: () => draw(), // 횡단이 뜨면 그 측점 반폭 선을 그린다.
|
||||
});
|
||||
@@ -217,6 +237,11 @@ export async function openRouteEditModal(
|
||||
const slope = createSlopeLabels({ projectId, isClosed: () => closed, onChange: () => draw() });
|
||||
/** 1m 간격 등고선 켬끔 — 기본 꺼짐(계획서 18 ②). */
|
||||
const showMinorContours = bindMinorContourToggle(minorContourButton, () => draw());
|
||||
stationSnapButton.addEventListener("click", () => {
|
||||
setStationSnap(!stationSnap);
|
||||
void crossPreview.refresh(); // 떠 있는 횡단도 옮긴 측점 규칙으로.
|
||||
draw();
|
||||
});
|
||||
/** 보던 화면 기억(계획서 18 ③) — 자료를 읽고 화면을 되살린 **뒤부터** 적는다. */
|
||||
let viewReady = false;
|
||||
const viewMemory = createViewMemory(projectId, () =>
|
||||
@@ -307,6 +332,7 @@ export async function openRouteEditModal(
|
||||
toScreen,
|
||||
uprightRad: rotation.uprightRad(),
|
||||
dragging: pointer.dragging(),
|
||||
chainages: stations(),
|
||||
}),
|
||||
measure: measure.marks(),
|
||||
crossWidth: crossPreview.watched(),
|
||||
@@ -319,6 +345,7 @@ export async function openRouteEditModal(
|
||||
locked: apexLock,
|
||||
picked,
|
||||
stationIntervalM,
|
||||
stations: stations(),
|
||||
});
|
||||
// 라벨은 **그린 뒤** 자리를 맞춘다 — 확대·이동·창 크기가 바뀌어도 고른 노드에 붙어 있게.
|
||||
syncCurveBar();
|
||||
@@ -448,6 +475,7 @@ export async function openRouteEditModal(
|
||||
canvas,
|
||||
status,
|
||||
stationIntervalM,
|
||||
stations,
|
||||
state: () => ({
|
||||
planned,
|
||||
plannedLine,
|
||||
@@ -530,8 +558,12 @@ export async function openRouteEditModal(
|
||||
projectId,
|
||||
nodes: () => ({ planned, curveOn, curveRadius }),
|
||||
// 분석 지형만 바꿔도 [확인]이 설계 지형을 바꿔 다시 셈한다(PLAN 133-2).
|
||||
changed: () => (history?.isDirty() ?? false) || pickedTerrain() !== designTerrain,
|
||||
changed: () =>
|
||||
(history?.isDirty() ?? false) ||
|
||||
pickedTerrain() !== designTerrain ||
|
||||
stationSnap !== savedStationSnap,
|
||||
terrain: pickedTerrain,
|
||||
stationSnap: () => stationSnap,
|
||||
initial,
|
||||
profileShown: strip.settle,
|
||||
close,
|
||||
@@ -554,6 +586,8 @@ export async function openRouteEditModal(
|
||||
minRadiusM = plan.min_radius_m ?? 0;
|
||||
limitRadiusM = plan.limit_radius_m ?? 0;
|
||||
limitArcM = plan.limit_curve_length_m ?? 0;
|
||||
savedStationSnap = plan.station_snap === true;
|
||||
setStationSnap(savedStationSnap);
|
||||
// 곡선 성분을 편집할 수 있는 꼴로 편다 — 셈은 `_Edits` 몫(까닭도 그쪽에 적었다).
|
||||
const flat = flattenServerPlan(nodes, plan.curves ?? []);
|
||||
({ planned, curveOn, curveRadius, curveLock, curveArc } = flat);
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface RouteApplyParams {
|
||||
changed: () => boolean;
|
||||
/** 설계 지형으로 보낼 분석 지형(PLAN 133-2) — 고르기가 안 보이면 null(지금 설계 지형 그대로). */
|
||||
terrain: () => string | null;
|
||||
/** [측점 스냅] 켬끔(요구 224-2) — 곡선표에 적혀 측점 셈이 읽음. */
|
||||
stationSnap: () => boolean;
|
||||
/** 초기노선 갈래 — [확인]에 지금 초기값을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */
|
||||
initial: {
|
||||
current: () => unknown;
|
||||
@@ -97,6 +99,7 @@ export function bindRouteApply(params: RouteApplyParams): void {
|
||||
// 띠 응답 전에 눌렀으면 기다린다 — 서버는 같은 노선 · 같은 셈으로 띠 계획선을 다시 낸다.
|
||||
balanced_profile: await params.profileShown(),
|
||||
terrain: params.terrain(),
|
||||
station_snap: params.stationSnap(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface RouteEditChrome {
|
||||
measureText: HTMLElement;
|
||||
measureButton: HTMLButtonElement;
|
||||
minorContourButton: HTMLButtonElement;
|
||||
/** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */
|
||||
stationSnapButton: HTMLButtonElement;
|
||||
/** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */
|
||||
terrainSelect: HTMLSelectElement;
|
||||
}
|
||||
@@ -65,6 +67,9 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
title="1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)">1m 등고선</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost" data-act="measure"
|
||||
title="노선 위 두 점을 눌러 거리·기울기를 잽니다 (Shift+클릭도 같음)">거리 재기</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost" data-act="station-snap"
|
||||
aria-pressed="false"
|
||||
title="곡선(호) 안 측점 하나를 곡선 중점(MC)으로 옮깁니다 — [확인] 때 저장">측점 스냅</button>
|
||||
<i class="b05-routeedit__divider" aria-hidden="true"></i>
|
||||
<button type="button" class="ui-btn ui-btn--ghost" data-act="undo"
|
||||
title="되돌리기 (Ctrl+Z)" disabled>↶ 되돌리기</button>
|
||||
@@ -136,6 +141,7 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
'[data-act="contour-minor"]',
|
||||
)!;
|
||||
const terrainSelect = overlay.querySelector<HTMLSelectElement>('[data-act="terrain"]')!;
|
||||
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
|
||||
|
||||
return {
|
||||
overlay,
|
||||
@@ -146,6 +152,7 @@ export function createRouteEditChrome(): RouteEditChrome {
|
||||
measureText,
|
||||
measureButton,
|
||||
minorContourButton,
|
||||
stationSnapButton,
|
||||
terrainSelect,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface CrossPreviewParams {
|
||||
vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>;
|
||||
min_radius_m: number;
|
||||
station_interval_m: number;
|
||||
station_snap?: boolean;
|
||||
};
|
||||
/** 위 판에 횡단이 떴다 · 비었다 — 평면도의 반폭 선을 다시 그리라는 신호. */
|
||||
onShown?: () => void;
|
||||
|
||||
@@ -321,13 +321,15 @@ export function stationAtScreen(
|
||||
py: number,
|
||||
maxPx: number,
|
||||
labels: ReadonlyArray<StationLabelBox> = [],
|
||||
/** 측점 누가거리 — 안 주면 간격마다(측점 스냅 · 요구 224-2). */
|
||||
chainages: ReadonlyArray<number> | null = null,
|
||||
): number | null {
|
||||
// 라벨이 그려진 측점만 눈금도 보인다(`drawStationTicks` 가 성기게 솎는다) — 안 보이는 눈금이
|
||||
// 호·등고선 누름을 가로채지 않게 한다. 라벨 정보가 없으면 전부 본다.
|
||||
const shown = labels.length
|
||||
? new Set(labels.map((box) => Math.round(box.chainage / intervalM)))
|
||||
: null;
|
||||
const tick = stationTickAtScreen(line, toScreen, intervalM, px, py, maxPx, shown);
|
||||
const tick = stationTickAtScreen(line, toScreen, intervalM, px, py, maxPx, shown, chainages);
|
||||
// 눈금과 라벨이 겹치면 눈금이 먼저다 — 라벨은 눈금을 못 잡았을 때만 본다.
|
||||
return tick ?? stationLabelAtScreen(labels, px, py);
|
||||
}
|
||||
@@ -373,6 +375,7 @@ function stationTickAtScreen(
|
||||
py: number,
|
||||
maxPx: number,
|
||||
shown: ReadonlySet<number> | null,
|
||||
chainages: ReadonlyArray<number> | null,
|
||||
): number | null {
|
||||
if (line.length < 2 || !(intervalM > 0)) return null;
|
||||
const cumulative: number[] = [0];
|
||||
@@ -386,7 +389,10 @@ function stationTickAtScreen(
|
||||
let best: number | null = null;
|
||||
let bestDistance = maxPx;
|
||||
let cursor = 1;
|
||||
for (let chainage = 0; chainage <= total; chainage += intervalM) {
|
||||
const marks: number[] = [];
|
||||
if (chainages) marks.push(...chainages.filter((value) => value <= total));
|
||||
else for (let chainage = 0; chainage <= total; chainage += intervalM) marks.push(chainage);
|
||||
for (const chainage of marks) {
|
||||
if (shown && !shown.has(Math.round(chainage / intervalM))) continue;
|
||||
while (cursor < cumulative.length - 1 && cumulative[cursor] < chainage) cursor += 1;
|
||||
const back = line[cursor - 1];
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface RouteEditPointerParams {
|
||||
canvas: HTMLCanvasElement;
|
||||
status: HTMLElement;
|
||||
stationIntervalM: number;
|
||||
/** 편집 창 측점 — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */
|
||||
stations: () => ReadonlyArray<number> | null;
|
||||
state: () => RouteEditPointerState;
|
||||
setPicked: (index: number) => void;
|
||||
setPickedContour: (index: number) => void;
|
||||
@@ -194,6 +196,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
|
||||
py,
|
||||
pickPx(STATION_HIT_PX),
|
||||
stationLabelBoxes(canvas),
|
||||
params.stations(),
|
||||
);
|
||||
if (chainage === null) return false;
|
||||
void params.crossPreview.open(chainage);
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface RouteEditScene {
|
||||
picked: number;
|
||||
/** 규칙 측점 간격(m). */
|
||||
stationIntervalM: number;
|
||||
/** 그릴 측점 누가거리 — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */
|
||||
stations: ReadonlyArray<number> | null;
|
||||
/** 구간 재기로 찍은 점(0~2개) — 노선 위 자리와 그 노선의 누가거리(계획서 0-9 ⑤). */
|
||||
measure: ReadonlyArray<MeasureMark>;
|
||||
/** 오른쪽에 횡단이 떠 있는 측점과 좌·우 설계 반폭 — 없으면 null. */
|
||||
@@ -478,6 +480,7 @@ function drawStationMarks(
|
||||
toScreen: (x, y) => scene.toScreen([x, y]),
|
||||
uprightRad: scene.uprightRad,
|
||||
onLabel: (box) => labels.push(box),
|
||||
chainages: scene.stations ?? undefined,
|
||||
},
|
||||
);
|
||||
const total = polylineLengthM(line);
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface SlopeDrawArgs {
|
||||
uprightRad: number;
|
||||
/** 노드·손잡이를 끄는 중 — 이때는 묻지 않는다. */
|
||||
dragging: boolean;
|
||||
/** 측점 누가거리(종점 뺌) — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */
|
||||
chainages?: ReadonlyArray<number> | null;
|
||||
}
|
||||
|
||||
export interface SlopeLabels {
|
||||
@@ -91,6 +93,8 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels {
|
||||
/** 높이를 받아 둔 노선·간격과 그 결과. 노선이 바뀌면(새 배열) 버린다. */
|
||||
let cachedLine: ReadonlyArray<Vertex> | null = null;
|
||||
let cachedInterval = 0;
|
||||
/** 받아 둔 측점 목록 열쇠 — 스냅을 켜고 끄면 같은 노선이어도 다시 묻는다. */
|
||||
let cachedKey = "";
|
||||
let chainages: number[] = [];
|
||||
let heights: Array<number | null> = [];
|
||||
/** 측점 자리와 구간 가운데 자리 — 받아 올 때 한 번 셈해 둔다(그리기마다 다시 안 셈). */
|
||||
@@ -100,19 +104,27 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels {
|
||||
let asking: ReadonlyArray<Vertex> | null = null;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function schedule(line: ReadonlyArray<Vertex>, intervalM: number): void {
|
||||
function schedule(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
intervalM: number,
|
||||
marks: ReadonlyArray<number> | null,
|
||||
): void {
|
||||
if (asking === line) return;
|
||||
if (timer) clearTimeout(timer);
|
||||
asking = line;
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
const wanted = stationChainages(lengthOf(line), intervalM);
|
||||
const total = lengthOf(line);
|
||||
const wanted = marks
|
||||
? [...marks.filter((value) => value < total - 1e-6), total]
|
||||
: stationChainages(total, intervalM);
|
||||
const points = pointsAt(line, wanted);
|
||||
void fetchRouteElevations(params.projectId, points)
|
||||
.then((z) => {
|
||||
if (params.isClosed() || asking !== line) return; // 그 사이 노선이 또 바뀌었다.
|
||||
cachedLine = line;
|
||||
cachedInterval = intervalM;
|
||||
cachedKey = marks?.join() ?? "";
|
||||
chainages = wanted;
|
||||
heights = z;
|
||||
stationPoints = points;
|
||||
@@ -160,9 +172,11 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels {
|
||||
const { line, intervalM } = args;
|
||||
if (line.length < 2 || !(intervalM > 0)) return 0;
|
||||
if (intervalM * args.pxPerMeter < SLOPE_MIN_GAP_PX) return 0; // 멀리 보면 숨긴다.
|
||||
const fresh = cachedLine === line && cachedInterval === intervalM;
|
||||
const marks = args.chainages ?? null;
|
||||
const fresh =
|
||||
cachedLine === line && cachedInterval === intervalM && cachedKey === (marks?.join() ?? "");
|
||||
if (!fresh) {
|
||||
if (!args.dragging) schedule(line, intervalM);
|
||||
if (!args.dragging) schedule(line, intervalM, marks);
|
||||
return 0; // 낡은 높이로 그리면 엉뚱한 기울기가 적힌다 — 받아 올 때까지 비운다.
|
||||
}
|
||||
// 화면 안 판정은 **캔버스 변환 그대로**(회전·고해상도 배율 포함) 옮긴 자리로 한다 —
|
||||
|
||||
Reference in New Issue
Block a user