Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
import type { DrawPreview } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw";
|
||||
import { planColors } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
||||
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
||||
import type { MapRotation } from "./B05_Profile_UI_RouteEdit_Rotate";
|
||||
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||
import { flattenServerPlan, type FlattenedPlan } from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
||||
import {
|
||||
@@ -63,10 +64,9 @@ export interface ExpectedDrawParams {
|
||||
host: HTMLElement;
|
||||
toScreen: (vertex: Vertex) => [number, number];
|
||||
toMetric: (px: number, py: number) => Vertex;
|
||||
/** 보이는 화면 px → 그리기 px(돌린 지도). */
|
||||
unrotate: (px: number, py: number) => [number, number];
|
||||
/** 지도 돌림 — 보이는 px ↔ 그리기 px · 글자 세울 각(만든 뒤에 서므로 함수로). */
|
||||
rotation: () => Pick<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
|
||||
pxPerMeter: () => number;
|
||||
uprightRad: () => number;
|
||||
redraw: () => void;
|
||||
/** 판을 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
|
||||
onOpen: () => void;
|
||||
@@ -105,6 +105,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
let busy = false;
|
||||
let stroke: Stroke | null = null;
|
||||
let preview: DrawPreview | null = null;
|
||||
/** 확인창이 떠 있는 동안 보일 새 예정노선(묻는 동안 무엇이 바뀌는지 보이게). */
|
||||
let asking: Vertex[] | null = null;
|
||||
let fan: Fan | null = null;
|
||||
let hoverAt: { px: number; py: number; shift: boolean } | null = null;
|
||||
let grid: DesignTerrainGrid | null = null;
|
||||
@@ -155,7 +157,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
canvas.style.removeProperty("cursor");
|
||||
if (open) {
|
||||
params.onOpen();
|
||||
void loadDesignTerrainGrid(params.projectId).then((loaded) => (grid = loaded));
|
||||
void loadDesignTerrainGrid(params.projectId).then((loaded) => ((grid = loaded), rehover()));
|
||||
}
|
||||
params.redraw();
|
||||
}
|
||||
@@ -268,6 +270,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
/** 다시 추적 — 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음. `next` = [] 면 파일입력 예정노선. */
|
||||
async function follow(next: Vertex[]): Promise<void> {
|
||||
stop();
|
||||
asking = next.length ? next : original;
|
||||
params.redraw();
|
||||
const ok = await askConfirm({
|
||||
title: "평면선 다시 추적",
|
||||
message:
|
||||
@@ -276,6 +280,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
"[저장] 전까지는 되돌리기로 물릴 수 있습니다.",
|
||||
okLabel: "다시 추적",
|
||||
});
|
||||
asking = null;
|
||||
params.redraw();
|
||||
if (!ok) return;
|
||||
busy = true;
|
||||
try {
|
||||
@@ -308,7 +314,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
// ── 누름 · 키 — 판이 켜진 동안 먼저 받음(계획선 · 구조물 · 재기로 안 샘) ──
|
||||
const at = (event: MouseEvent): [number, number] => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return params.unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
};
|
||||
const claim = (event: Event): void => {
|
||||
event.stopImmediatePropagation();
|
||||
@@ -376,7 +382,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
toScreen: params.toScreen,
|
||||
pxPerMeter: 1,
|
||||
upright(context: CanvasRenderingContext2D, x: number, y: number, paint: () => void): void {
|
||||
const radians = params.uprightRad();
|
||||
const radians = params.rotation().uprightRad();
|
||||
if (!radians) return paint();
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
@@ -405,8 +411,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
path(context, line()); // 고치는 예정노선 = 맨 위(D9)
|
||||
strokeCased(context, colors.measure, 2.5, [8, 5]);
|
||||
const points = strokePoints();
|
||||
if (points.length > 1) {
|
||||
path(context, points);
|
||||
if ((asking ?? points).length > 1) {
|
||||
path(context, asking ?? points); // 묻는 동안 = 새 예정노선 통째
|
||||
strokeCased(context, colors.active, 3);
|
||||
}
|
||||
points.forEach((point, i) => {
|
||||
@@ -421,6 +427,22 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const state = (): ReturnType<ExpectedDraw["state"]> => ({
|
||||
open,
|
||||
stroke: stroke?.mode ?? null,
|
||||
points: strokePoints().length,
|
||||
preview,
|
||||
});
|
||||
// 검증 창구 — 판 상태 · 보이는 예정노선 · 사업지 좌표 → 보이는 화면 px
|
||||
Object.assign(canvas, {
|
||||
expectedProbe: {
|
||||
state,
|
||||
line,
|
||||
edit: () => edit,
|
||||
screen: (vertex: Vertex) => params.rotation().rerotate(...params.toScreen(vertex)),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
load(nextOriginal, nextSaved) {
|
||||
original = nextOriginal;
|
||||
@@ -432,11 +454,6 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
},
|
||||
line,
|
||||
paint,
|
||||
state: () => ({
|
||||
open,
|
||||
stroke: stroke?.mode ?? null,
|
||||
points: strokePoints().length,
|
||||
preview,
|
||||
}),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,9 +126,8 @@ export async function mountRouteEditor(
|
||||
host: overlay.querySelector<HTMLElement>(".b05-routeedit__tools")!,
|
||||
toScreen: (vertex) => toScreen(vertex),
|
||||
toMetric: (px, py) => toMetric(px, py),
|
||||
unrotate: (px, py) => rotation.unrotate(px, py),
|
||||
rotation: () => rotation,
|
||||
pxPerMeter: () => pxPerMeter(),
|
||||
uprightRad: () => rotation.uprightRad(),
|
||||
redraw: () => draw(),
|
||||
onOpen: () => ((picked = -1), syncCurveBar()),
|
||||
onFollow: (flat, line) =>
|
||||
|
||||
@@ -489,7 +489,10 @@ html.is-bm-route .b05-routeedit__tools {
|
||||
}
|
||||
|
||||
/* 626-3 예정노선 그리기 판 — 도구 줄 아래(떠 있는 판) · 단추 줄 */
|
||||
/* 도구 줄 칸(세로 줄) 안 차례대로 — 판 기본 absolute · 높이 100 % 를 풂 */
|
||||
.b05-routeedit__expected-draw {
|
||||
position: relative;
|
||||
max-height: none;
|
||||
pointer-events: auto;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import type { DrawPreview } from "./D03_PlanAnalysis_UI_Draw";
|
||||
export const SNAP_PX = 12;
|
||||
/** 선 위 누름으로 볼 반경(px). */
|
||||
export const LINE_HIT_PX = 8;
|
||||
/** 놓인 꺾임점 점 반지름(px) — 선 테(3 + 4 px)보다 굵게 · 고른 선 손잡이는 이 점을 감싸는 빈 네모. */
|
||||
export const NODE_DOT_PX = 5;
|
||||
/** 구간 길이 라벨을 보일 화면 길이(px). */
|
||||
const LABEL_MIN_PX = 70;
|
||||
|
||||
@@ -213,24 +215,33 @@ export function paintNetwork(
|
||||
);
|
||||
if (map.slopes) segmentLabels(context, map, line.vertices, z);
|
||||
});
|
||||
// 꺾임점 = 들어온 구간 기울기 부호 색(625-2 B4) · 기록과 어긋난 노드(옮김) = 경고 고리(599-2)
|
||||
context.strokeStyle = colors.warn;
|
||||
context.lineWidth = 2;
|
||||
for (const line of network.lines)
|
||||
paintNodes(context, map, network.lines, z);
|
||||
drawPieces(context, map, network);
|
||||
extra();
|
||||
}
|
||||
|
||||
/** 꺾임점 = 들어온 구간 기울기 부호 색(625-2 B4) · 선보다 굵은 점 + 검은 테 = 선 색 · 위성 위에서도 가름(625-3) ·
|
||||
* 기록과 어긋난 노드(옮김) = 경고 고리(599-2) · 고른 선은 고름 강조 위에 다시 그림(`D03_PlanAnalysis_UI_Edit`). */
|
||||
export function paintNodes(
|
||||
context: CanvasRenderingContext2D,
|
||||
map: Pick<ScreenMap, "toScreen">,
|
||||
lines: ReadonlyArray<PlanNetwork["lines"][number]>,
|
||||
z: HeightAt,
|
||||
): void {
|
||||
for (const line of lines)
|
||||
line.vertices.forEach((v, i) => {
|
||||
if (!i) return;
|
||||
const [sx, sy] = map.toScreen(v);
|
||||
context.beginPath();
|
||||
context.arc(sx, sy, 3, 0, Math.PI * 2);
|
||||
context.fillStyle = slopeColor(groundSlopePct(line.vertices[i - 1], v, z));
|
||||
context.fill();
|
||||
context.arc(sx, sy, NODE_DOT_PX, 0, Math.PI * 2);
|
||||
fillCased(context, slopeColor(groundSlopePct(line.vertices[i - 1], v, z)));
|
||||
if (!nodeOff(line, i, z)) return;
|
||||
context.beginPath();
|
||||
context.arc(sx, sy, 7, 0, Math.PI * 2);
|
||||
context.arc(sx, sy, NODE_DOT_PX + 4, 0, Math.PI * 2);
|
||||
context.lineWidth = 2;
|
||||
context.strokeStyle = planColors().warn;
|
||||
context.stroke();
|
||||
});
|
||||
drawPieces(context, map, network);
|
||||
extra();
|
||||
}
|
||||
|
||||
/** 지점 핀 겹(585-1) — 꼭지 = 지점 · 둥근 머리 · 「지점 N」 · 화면 px 고정 크기 · 돌린 지도에서도 섬. */
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { keyIs } from "../A00_Common/key_registry";
|
||||
import { distanceAlong, pointAtDistance } from "./logic/D03_PlanAnalysis_Draw";
|
||||
import { distanceAlong, pointAtDistance, type HeightAt } from "./logic/D03_PlanAnalysis_Draw";
|
||||
import {
|
||||
insertVertex,
|
||||
moveMark,
|
||||
@@ -46,7 +46,9 @@ import {
|
||||
} from "./logic/D03_PlanAnalysis_Select";
|
||||
import {
|
||||
lineAt,
|
||||
NODE_DOT_PX,
|
||||
paintMark,
|
||||
paintNodes,
|
||||
planLabel,
|
||||
screenGap,
|
||||
SNAP_PX,
|
||||
@@ -70,6 +72,8 @@ export interface PlanEditParams {
|
||||
/** 지점(긋기에 붙는 점 · 591-2). */
|
||||
base: () => Vertex[];
|
||||
commit: (next: PlanNetwork) => void;
|
||||
/** 원지반 높이(고른 선 꺾임점 부호 색 · 625-3). */
|
||||
heightAt: () => HeightAt;
|
||||
/** 끌기 중 — 화면만 바꿈(되돌리기 · 캐시 없음). */
|
||||
show: (next: PlanNetwork) => void;
|
||||
/** 지점 자리 노드를 끎 — 지점 · 노선 한 걸음 · 화면만. */
|
||||
@@ -329,18 +333,18 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit {
|
||||
context.stroke();
|
||||
strokeCased(context, colors.active, PICK_LINE_PX);
|
||||
}
|
||||
// 꺾임점 손잡이(고른 것이 든 선)
|
||||
// 꺾임점 손잡이(고른 것이 든 선) = 부호 색 점(고름 강조 위에 다시) + 감싸는 빈 네모(625-3)
|
||||
const lines = pickedLines(network);
|
||||
context.lineWidth = 2;
|
||||
context.fillStyle = "#ffffff";
|
||||
for (const line of network.lines) {
|
||||
if (!lines.has(line.id)) continue;
|
||||
const handled = network.lines.filter((line) => lines.has(line.id));
|
||||
paintNodes(context, map, handled, params.heightAt());
|
||||
const half = NODE_DOT_PX + 3;
|
||||
for (const line of handled)
|
||||
for (const point of line.vertices) {
|
||||
const [sx, sy] = map.toScreen(point);
|
||||
context.fillRect(sx - 4, sy - 4, 8, 8);
|
||||
context.strokeRect(sx - 4, sy - 4, 8, 8);
|
||||
context.beginPath();
|
||||
context.rect(sx - half, sy - half, half * 2, half * 2);
|
||||
strokeCased(context, colors.active, 2);
|
||||
}
|
||||
}
|
||||
// 고른 노드 · 마크 테
|
||||
const node = picked?.kind === "node" ? picked.node : null;
|
||||
const nodePoint = node
|
||||
|
||||
@@ -146,6 +146,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
network: () => network,
|
||||
base: () => points,
|
||||
commit,
|
||||
heightAt: () => z,
|
||||
show: (next) => {
|
||||
network = normalizePieces(next, yearStart);
|
||||
map.redraw();
|
||||
|
||||
@@ -159,3 +159,19 @@ def test_가파른_땅_지형_밖_고를_자리_없음(r):
|
||||
for key in ("steep", "offTerrain"):
|
||||
f = r[key]
|
||||
assert not f["flat"] and not f["nodes"] and not f["arcs"]
|
||||
|
||||
|
||||
def test_놓인_노드_부호_색_선_위_고름_위에도_보임():
|
||||
"""625-3 — 놓인 꺾임점 = 선 테(3 + 4 px)보다 굵은 부호 색 점 + 검은 테 · 고른 선은 노랑 강조 위에 점을 다시 그리고 손잡이 = 빈 네모."""
|
||||
paint = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_DrawPaint.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
edit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Edit.ts").read_text(encoding="utf-8")
|
||||
assert int(re.search(r"NODE_DOT_PX = (\d+)", paint).group(1)) * 2 > 3 + 4
|
||||
body = paint[paint.index("export function paintNodes") :]
|
||||
body = body[: body.index("\n}\n")]
|
||||
assert "fillCased(context, slopeColor(groundSlopePct(" in body
|
||||
assert "paintNodes(context, map, network.lines, z)" in paint
|
||||
draw = edit[edit.index("draw(context, map) {") :]
|
||||
assert draw.index("sel.paths()") < draw.index("paintNodes(") < draw.index("context.rect(")
|
||||
assert "fillRect" not in draw[: draw.index("고른 노드 · 마크 테")]
|
||||
|
||||
Reference in New Issue
Block a user