Files
Aislo/B05_Profile/logic/B05_Profile_PlanContext.ts
T
eomsangdonandClaude Opus 5.5 23059f598b feat(635-4): B05 흐린 다른 세트 겹 · 이음점 셈 · 키트 붙기 칸(요구 546 · 547)
- 새 logic/B05_Profile_PlanContext.ts — 흐릴 세트(내 것 빼고) · 이음점(남 세트 끝) · 받을 때 이어진 이음점 · 벗어남 안내 · 이름표 자리
- 새 B05_Profile_UI_PlanContext.ts — plan-context 실시간 읽기 · 옅은 선 + 세트 이름표 · 누름 칸 없음(보기만)
- 키트 · 긋기 · 고르기 snaps 덧칸 — 긋기 · 노드 끌기가 이음점에 붙기만
- 기능 표 others 줄(B05 보기만)
- 시험 test_635_4(6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:23:28 +09:00

93 lines
3.5 KiB
TypeScript

/* =============================================================================
* B05_Profile_PlanContext.ts
* B05 흐린 다른 세트 셈(PLAN 635-4 · 요구 546 · 547 · 결정 5 · 6 · 7) — 순수 함수.
* 입력 = `GET /api/projects/{B}/plan-context`(같은 예정노선 세트 모두 · B 좌표 · 업체 이름 없음).
* 흐릴 세트 = 내 세트 아님 · 이음점 = 남 세트 꺾은선 끝 · 받을 때 이어진 이음점 = 내 받은 선 끝 1 m 안.
* 이음은 고정 안 함 — 벗어나면 안내 글 한 줄(판단 = 설계자).
* ========================================================================== */
import { lineLength, pointAtDistance } from "../../D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw";
export type Vertex = [number, number];
/** plan-context 세트 한 줄(635-2 꼴). */
export interface ContextSet {
key: string;
year: number;
no: number;
mine: boolean;
parts: number;
branch: boolean;
length_m: number;
lines: Vertex[][];
}
export interface PlanContext {
linked: boolean;
plan_name?: string;
mine?: string | null;
sets?: ContextSet[];
}
/** 이음점 · 끝 맞음 거리(m). */
export const JOINT_TOL_M = 1;
export interface Joint {
key: string;
point: Vertex;
}
const dist = (a: Vertex, b: Vertex): number => Math.hypot(a[0] - b[0], a[1] - b[1]);
const drawn = (line: Vertex[]): boolean => line.length > 1;
const ends = (line: Vertex[]): Vertex[] => [line[0], line[line.length - 1]];
/** 흐릴 세트 — 내 세트 아님 · 그릴 선 있음. */
export function otherSets(context: PlanContext | null): ContextSet[] {
if (!context?.linked) return [];
return (context.sets ?? []).filter((set) => !set.mine && set.lines.some(drawn));
}
/** 남 세트 꺾은선 끝 모두(붙을 자리). */
export function jointPoints(context: PlanContext | null): Joint[] {
return otherSets(context).flatMap((set) =>
set.lines.filter(drawn).flatMap((line) => ends(line).map((point) => ({ key: set.key, point }))),
);
}
/** 받을 때 이어진 이음점 — 남 세트 끝 중 내 받은 선 끝 1 m 안. */
export function linkedJoints(context: PlanContext | null): Joint[] {
const mine = (context?.sets ?? []).find((set) => set.mine);
const tips = (mine?.lines ?? []).filter(drawn).flatMap(ends);
return jointPoints(context).filter((joint) =>
tips.some((tip) => dist(tip, joint.point) <= JOINT_TOL_M),
);
}
/** 이어졌던 이음점마다 지금 선 끝까지 거리 — 1 m 밖만 · 선 없음 = 빈. */
export function jointGaps(
line: ReadonlyArray<Vertex>,
joints: ReadonlyArray<Joint>,
): Array<{ key: string; gapM: number }> {
if (line.length < 2) return [];
const tips = ends(line as Vertex[]);
return joints
.map((joint) => ({
key: joint.key,
gapM: Math.min(...tips.map((tip) => dist(tip, joint.point))),
}))
.filter((gap) => gap.gapM > JOINT_TOL_M);
}
/** 벗어남 안내 한 줄(없음 = null). */
export function jointNote(gaps: ReadonlyArray<{ key: string; gapM: number }>): string | null {
if (!gaps.length) return null;
const list = gaps.map((gap) => `${gap.key} ${gap.gapM.toFixed(1)} m`).join(" · ");
return `이웃 세트 이음점에서 벗어남(${list}) — 이음은 고정이 아님 · 끝을 이음점에 놓으면 붙습니다.`;
}
/** 세트 이름표 자리 — 가장 긴 꺾은선 가운데. */
export function labelPoint(set: ContextSet): Vertex | null {
const line = set.lines.filter(drawn).sort((a, b) => lineLength(b) - lineLength(a))[0];
return line ? pointAtDistance(line, lineLength(line) / 2) : null;
}