사용자 지시 5건. ⚠ **화면 검증은 아직 못 함** — 다른 창의 분배 조율로 git sync 를 먼저 하게 되어 미커밋으로 두지 않으려고 먼저 커밋함. 타입 검사·시험은 통과 상태임. - **1. 패널을 화면으로** — `document.body` 에 `position: fixed` 로 띄움. 모달이 `overflow: hidden` 이라 안에 두면 가장자리에서 잘렸음. 이제 모달 밖으로도 넘어감. - **2. 너무 가까움 + 위치 제어** — 여백 14 → 40px 로 넓히고 방위별 상자 반지름만큼 더 밀어냄. **머리를 잡아 손으로 옮길 수 있음**. 옮긴 자리는 꺾임점 기준으로 기억해 지도를 옮기거나 확대해도 따라오고, 다른 꺾임점을 고르면 자동 자리로 돌아감. - **3. 자동 위치를 16방위로** — 4방위는 대각 자리에서 곡선을 물었음. 곡선 중심의 반대 방향을 22.5° 단위로 맞춤. - **4. 「법정 하한」 삭제**, 「칸을 비우면 자동」을 **다음 줄**로 뺌. 대신 무엇을 붙들고 있는지(고정 없음 / 반지름 고정 / 곡선 길이 고정)를 그 줄에 냄. - **5. 반지름·곡선 길이 고정 단추** — 노드를 옮기면 교각이 바뀌어 R 과 길이 중 하나는 반드시 따라 움직임. 그래서 **셋 중 하나**임: 자동 / R 고정(길이가 따라감) / 길이 고정(R 을 L/Δ 로 다시 잡음). 붙들어 둔 곡선은 **손잡이로도 안 바뀜**(끌면 R 이 바뀌므로 집기에서 제외). 잠금·길이는 되돌리기 사진에도 담김. - 새 모듈 `_Edits.ts` — 잠금 반영·상태줄 요약·서버 응답 펴기. 본체 692줄로 700줄 유지. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
6.6 KiB
TypeScript
161 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Edits.ts
|
|
* 꺾임점마다의 **편집값**을 다루는 순수 함수 — 잠금과 상태줄 요약.
|
|
*
|
|
* **잠금이 왜 필요한가**(2026-09-07 사용자 지시) — 노드를 옮기면 앞뒤 직선의 교각(Δ)이
|
|
* 바뀐다. 반지름 R 과 곡선 길이 L 은 **L = R·Δ** 로 묶여 있으므로, 한쪽을 붙들면 다른 쪽은
|
|
* 반드시 따라 움직인다. 둘 다 붙들 수는 없다(그러면 Δ 를 못 바꾼다). 그래서 잠금은 셋 중 하나다.
|
|
*
|
|
* · `null` — 자동. R 은 법정 하한을 쓰고 L 은 따라온다.
|
|
* · `"radius"` — **R 고정**. 노드를 옮겨도 R 이 그대로고 L 이 바뀐다.
|
|
* · `"arc"` — **곡선 길이 고정**. 노드를 옮기면 R 을 L/Δ 로 다시 잡는다.
|
|
*
|
|
* ⚠ 접선 자리가 모자라면 그리기 단계에서 R 을 줄이는 것은 그대로다(`buildEditedPolyline`).
|
|
* 그것은 **그려지는 값**만 줄이고 잠가 둔 값은 안 건드린다 — 자리를 넓히면 되돌아온다.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
innerAngleDeg,
|
|
type EditedCurve,
|
|
type EditedNode,
|
|
type Vertex,
|
|
} from "./B05_Profile_UI_RouteEdit_Curve";
|
|
import { deflectionRad } from "./B05_Profile_UI_RouteEdit_Label";
|
|
|
|
/** 무엇을 붙들고 있나. */
|
|
export type CurveLock = "radius" | "arc" | null;
|
|
|
|
/**
|
|
* **곡선 길이를 잠근 자리**의 반지름을 지금 교각에 맞춰 다시 잡는다(`curveRadius` 를 고침).
|
|
*
|
|
* 노드를 옮길 때마다 부른다. 잠그지 않았거나 R 을 잠근 자리는 손대지 않는다.
|
|
* 교각이 0 에 가까우면(거의 직선) 길이를 지킬 방법이 없으므로 그대로 둔다.
|
|
*/
|
|
export function applyArcLocks(
|
|
planned: Vertex[],
|
|
curveLock: CurveLock[],
|
|
curveArc: Array<number | null>,
|
|
curveRadius: Array<number | null>,
|
|
): void {
|
|
for (let seat = 1; seat < planned.length - 1; seat += 1) {
|
|
if (curveLock[seat] !== "arc") continue;
|
|
const length = curveArc[seat];
|
|
if (length === null || length === undefined) continue;
|
|
const inner = innerAngleDeg(planned[seat - 1], planned[seat], planned[seat + 1]);
|
|
const deflection = deflectionRad(inner);
|
|
if (deflection <= 1e-9) continue;
|
|
curveRadius[seat] = length / deflection;
|
|
}
|
|
}
|
|
|
|
export interface CurveSummaryInput {
|
|
nodeCount: number;
|
|
curveOn: boolean[];
|
|
curveRadius: Array<number | null>;
|
|
curveLock: CurveLock[];
|
|
/** 그려 낸 곡선 수 — 아직 안 그렸으면 0. */
|
|
curveCount: number;
|
|
/** 법정 기준을 못 맞춘 자리 수. */
|
|
violationCount: number;
|
|
minRadiusM: number;
|
|
/** 아직 한 번도 안 그렸나(막 열었을 때). */
|
|
fresh: boolean;
|
|
}
|
|
|
|
/** 상태줄 꼬리 — 곡선 수·기준 미달·사용자가 손댄 자리를 한 줄로. */
|
|
export function curveSummary(input: CurveSummaryInput): string {
|
|
const off = input.curveOn.filter(
|
|
(on, index) => !on && index > 0 && index < input.nodeCount - 1,
|
|
).length;
|
|
const forced = input.curveRadius.filter((value) => value !== null).length;
|
|
const locked = input.curveLock.filter((lock) => lock !== null).length;
|
|
const edits = [
|
|
off ? `곡선 지움 ${off}곳` : "",
|
|
forced ? `R 지정 ${forced}곳` : "",
|
|
locked ? `고정 ${locked}곳` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ");
|
|
if (input.fresh) {
|
|
const base = input.minRadiusM ? `곡선 기준 R ${input.minRadiusM}m — [확인] 때 반영` : "";
|
|
return edits ? `${base}${base ? " · " : ""}${edits}` : base;
|
|
}
|
|
return (
|
|
`곡선 ${input.curveCount}곳(하한 R ${input.minRadiusM}m)` +
|
|
`${input.violationCount ? ` · 기준 미달 ${input.violationCount}곳` : ""}` +
|
|
`${edits ? ` · ${edits}` : ""}`
|
|
);
|
|
}
|
|
|
|
/** 서버가 준 노드·곡선을 **편집할 수 있는 꼴**로 편 결과. */
|
|
export interface FlattenedPlan {
|
|
planned: Vertex[];
|
|
nodes: EditedNode[];
|
|
curves: EditedCurve[];
|
|
curveOn: boolean[];
|
|
curveRadius: Array<number | null>;
|
|
curveLock: CurveLock[];
|
|
curveArc: Array<number | null>;
|
|
}
|
|
|
|
interface ServerNode {
|
|
x: number;
|
|
y: number;
|
|
radius_m: number | null;
|
|
inner_angle_deg: number | null;
|
|
violations?: string[];
|
|
}
|
|
|
|
/**
|
|
* 서버가 준 노드·곡선을 **꺾임점 하나 = 곡선 하나**로 편다(2026-09-07).
|
|
*
|
|
* 서버는 처음 만들 때 이어진 꺾임을 한 곡선으로 묶는다 — 그 곡선의 교각점은 앞뒤 직선을
|
|
* 늘려 만나는 자리라 **원본 꺾임점 중 어느 것도 아니다**. 묶인 채로 두면 한 번만 손대도
|
|
* 묶음이 낱개로 흩어지고 **맞춰 둔 반지름이 전부 법정 하한으로 되돌아간다**(실측: R
|
|
* 12~199m → 전부 12m). 그래서 **묶인 구간을 그 교각점 하나로 갈아 끼우고** 안쪽 꺾임점은
|
|
* 뺀다. 앞뒤 직선과 반지름이 그대로라 **그려지는 선은 똑같다**.
|
|
*
|
|
* ⚠ 서버가 **곡선을 안 둔 자리는 「곡선 없음」으로 연다**. 전부 켬으로 열면 아무것도 안
|
|
* 만지고 [확인]만 눌러도 그 자리에 곡선이 새로 생겨 노선이 조용히 바뀐다(서버의 편집
|
|
* 갈래는 켜진 자리마다 원호를 끼운다). 내각 179° 이상인 자리가 그렇다.
|
|
*/
|
|
export function flattenServerPlan(nodes: ServerNode[], curves: EditedCurve[]): FlattenedPlan {
|
|
const replaced = new Map<number, EditedCurve>();
|
|
const dropped = new Set<number>();
|
|
curves.forEach((curve) => {
|
|
replaced.set(curve.node_first, curve);
|
|
for (let index = curve.node_first + 1; index <= curve.node_last; index += 1) {
|
|
dropped.add(index);
|
|
}
|
|
});
|
|
|
|
const out: FlattenedPlan = {
|
|
planned: [],
|
|
nodes: [],
|
|
curves: [],
|
|
curveOn: [],
|
|
curveRadius: [],
|
|
curveLock: [],
|
|
curveArc: [],
|
|
};
|
|
nodes.forEach((node, index) => {
|
|
if (dropped.has(index)) return;
|
|
const curve = replaced.get(index);
|
|
const seat = out.planned.length;
|
|
out.planned.push(curve ? [curve.apex[0], curve.apex[1]] : [node.x, node.y]);
|
|
out.nodes.push({
|
|
radius_m: curve ? curve.radius_m : node.radius_m,
|
|
inner_angle_deg: curve ? curve.inner_angle_deg : node.inner_angle_deg,
|
|
tangent_m: null,
|
|
violations: curve ? (curve.violations ?? []) : (node.violations ?? []),
|
|
});
|
|
out.curveOn.push(curve !== undefined);
|
|
// 서버가 고른 반지름을 **그대로 들고 간다** — 안 그러면 편집 한 번에 하한으로 눌린다.
|
|
out.curveRadius.push(curve ? Math.round(curve.radius_m * 100) / 100 : null);
|
|
out.curveLock.push(null);
|
|
out.curveArc.push(null);
|
|
if (curve) out.curves.push({ ...curve, node_first: seat, node_last: seat });
|
|
});
|
|
return out;
|
|
}
|