앞 커밋에서 남긴 두 파일을 마저 갈랐다. 상태를 모듈로 옮기면 나머지 참조가 전부
바뀌므로 상태는 제자리에 두고 **접근자만 넘기는** 방식으로 동작·공개 인터페이스를
보존했다.
- Profile_Panel 1152 → 649
- _Profile_Layout: X축 배치(측점 칸 폭·캔버스 폭·chainage↔x 매핑). 순수 함수
- _Profile_Heights: 그래프·유토곡선·테이블 높이 배분. 드래그 플래그·상세 유무는
본체가 계속 들고 접근자로 읽는다(저장 높이 기준 판정 규칙 그대로)
- _Profile_Render: 본문 재구성(캔버스·그래프·구조물 레인·테이블·유토곡선).
그리기 시작 시 상태를 스냅숏하되 이벤트 핸들러 안에서만 현재값을 다시 읽는다
- _Profile_Balance: 상단 균형 표시줄(절·성토·불균형·위반 경고·초기선 복원)
- Page 1031 → 685
- _Page_Helpers: 설계폭 조회·모델 경계 변환·마커 복원·비정규 측점 보간 +
시설 표시 이름
- _Page_Structures: 구조물 정본과 관 지점 정본을 사이드 목록·그래프·3D에 맞추는
다리. 두 정본을 섞는 지점이라 여기만 상태(비정규 측점 목록·판번호·저장 큐·
타입 사전)를 팩토리 안으로 옮겼고, 본체는 bridge.irregularStations()로 읽는다
B05_Profile 전 파일이 700줄 이하가 됐다(최대 695).
검증: npm run typecheck 무오류, npm run build 성공(374 modules),
pytest tmp/tests 107 passed·7 skipped, prettier 정합.
프론트 테스트 러너가 없어 실제 화면 동작 확인은 남는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
5.6 KiB
TypeScript
154 lines
5.6 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Page_Helpers.ts
|
|
* B05 화면의 순수 도우미 — 설계폭 조회, 모델 경계 변환, 저장분 → 마커 복원,
|
|
* 비정규 측점의 3D 좌표 선형보간.
|
|
*
|
|
* 화면 본체(B05_Profile_UI_Page)가 700줄 한계에 닿아 분리했다.
|
|
* 전부 상태를 갖지 않는 변환 함수라 화면 흐름과 독립적이다.
|
|
* ========================================================================== */
|
|
|
|
import type { RoutePanelValues } from "./B05_Profile_UI_Panel";
|
|
import type {
|
|
ModelBounds,
|
|
PlacedRoutePoint,
|
|
RouteDesignPoints,
|
|
RoutePointKind,
|
|
} from "./B05_Profile_UI_Markers";
|
|
import type { CirclePoint, RouteLatestResponse, RoutePoint } from "./B05_Profile_Api_Fetch";
|
|
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { SectionStation } from "../B06_Section/B06_Section_Api_Fetch";
|
|
import {
|
|
irregularLabel,
|
|
irregularStationId,
|
|
type IrregularStation,
|
|
} from "./B05_Profile_UI_IrregularStations";
|
|
|
|
type GradeClass = RoutePanelValues["gradeClass"];
|
|
export type RoadWidths = Record<GradeClass, number>;
|
|
|
|
export const DEFAULT_ROAD_WIDTHS: RoadWidths = { trunk: 3, branch: 3, work: 2.5 };
|
|
|
|
export async function fetchRoadWidths(projectId: string): Promise<RoadWidths> {
|
|
const response = await fetch(`/api/projects/${projectId}/sections/road-widths`);
|
|
if (!response.ok) return DEFAULT_ROAD_WIDTHS;
|
|
const payload = (await response.json()) as { forest_road_min_width_m?: Partial<RoadWidths> };
|
|
return { ...DEFAULT_ROAD_WIDTHS, ...payload.forest_road_min_width_m };
|
|
}
|
|
|
|
export function toBounds(bounds: {
|
|
x_min: number;
|
|
x_max: number;
|
|
y_min: number;
|
|
y_max: number;
|
|
z_min: number;
|
|
z_max: number;
|
|
}): ModelBounds {
|
|
return {
|
|
x: [bounds.x_min, bounds.x_max],
|
|
y: [bounds.y_min, bounds.y_max],
|
|
z: [bounds.z_min, bounds.z_max],
|
|
};
|
|
}
|
|
|
|
export function placed(
|
|
type: RoutePointKind,
|
|
point: RoutePoint | CirclePoint,
|
|
index = 0,
|
|
): PlacedRoutePoint {
|
|
return {
|
|
id: `${type}-restored-${index}`,
|
|
type,
|
|
x: point.x,
|
|
y: point.y,
|
|
// 표고를 모르면 모르는 채로 넘긴다 — 0으로 눕히면 마커가 지형 한참 아래 평면에 깔린다.
|
|
z: point.z ?? null,
|
|
...(type === "ap" || type === "fp" ? { radius_m: (point as CirclePoint).radius_m ?? 25 } : {}),
|
|
};
|
|
}
|
|
|
|
export function restorePoints(latest: RouteLatestResponse): RouteDesignPoints {
|
|
const points = latest.route_params?.points;
|
|
return {
|
|
bp: points?.bp ? placed("bp", points.bp) : null,
|
|
ep: points?.ep ? placed("ep", points.ep) : null,
|
|
cp: (points?.cp ?? []).map((point, index) => placed("cp", point, index)),
|
|
ap: (points?.ap ?? []).map((point, index) => placed("ap", point, index)),
|
|
fp: (points?.fp ?? []).map((point, index) => placed("fp", point, index)),
|
|
};
|
|
}
|
|
|
|
export function routePoint(point: PlacedRoutePoint): RoutePoint {
|
|
// 서버 스키마는 표고를 빼면 "모름"으로 받는다 — null은 undefined로 바꿔 보낸다.
|
|
return { x: point.x, y: point.y, z: point.z ?? undefined };
|
|
}
|
|
|
|
export function circlePoint(point: PlacedRoutePoint): CirclePoint {
|
|
return { ...routePoint(point), radius_m: point.radius_m ?? 25 };
|
|
}
|
|
|
|
/**
|
|
* 비정규 측점을 규칙 측점 좌표 사이 chainage로 선형보간해 `SectionStation`(월드 좌표·프레임 포함)으로
|
|
* 만든다. 백엔드가 아직 이 측점의 횡단을 생성하지 않으므로, 3D 표시에 필요한 위치만 근사한다.
|
|
* 노선 범위를 벗어난 chainage는 제외한다.
|
|
*/
|
|
export function interpolateIrregularStations(
|
|
base: SectionStation[],
|
|
list: IrregularStation[],
|
|
maxChainage: number,
|
|
): SectionStation[] {
|
|
const sorted = [...base].sort((a, b) => a.chainage_m - b.chainage_m);
|
|
if (!sorted.length) return [];
|
|
const anchorAt = (chainage: number): SectionStation => {
|
|
if (chainage <= sorted[0].chainage_m) return sorted[0];
|
|
const last = sorted[sorted.length - 1];
|
|
if (chainage >= last.chainage_m) return last;
|
|
let lo = sorted[0];
|
|
let hi = last;
|
|
for (let index = 1; index < sorted.length; index += 1) {
|
|
if (sorted[index].chainage_m >= chainage) {
|
|
lo = sorted[index - 1];
|
|
hi = sorted[index];
|
|
break;
|
|
}
|
|
}
|
|
const span = hi.chainage_m - lo.chainage_m;
|
|
const t = span > 1e-9 ? (chainage - lo.chainage_m) / span : 0;
|
|
const lerp = (a: number, b: number): number => a + (b - a) * t;
|
|
const centerZ =
|
|
lo.center_z !== null && hi.center_z !== null
|
|
? lerp(lo.center_z, hi.center_z)
|
|
: (lo.center_z ?? hi.center_z);
|
|
return {
|
|
...lo,
|
|
center_x: lerp(lo.center_x, hi.center_x),
|
|
center_y: lerp(lo.center_y, hi.center_y),
|
|
center_z: centerZ,
|
|
frame: {
|
|
left_xy: [
|
|
lerp(lo.frame.left_xy[0], hi.frame.left_xy[0]),
|
|
lerp(lo.frame.left_xy[1], hi.frame.left_xy[1]),
|
|
],
|
|
},
|
|
};
|
|
};
|
|
return list
|
|
.filter((entry) => entry.chainage_m >= 0 && entry.chainage_m <= maxChainage + 1e-6)
|
|
.map((entry) => ({
|
|
...anchorAt(entry.chainage_m),
|
|
station_id: irregularStationId(entry.id),
|
|
chainage_m: entry.chainage_m,
|
|
label: irregularLabel(entry),
|
|
kind: "irregular" as const,
|
|
// 3D 측점 라벨이 `측점번호 구조물명`으로 표기할 수 있게 구조물 이름을 실어 보낸다.
|
|
structure: entry.structure,
|
|
}));
|
|
}
|
|
|
|
/** 시설 종류별 표시 이름 — 그래프·3D 라벨용(배관은 관종·관경까지 따로 붙인다). */
|
|
export const FACILITY_NAMES: Record<PipeFacility, string> = {
|
|
pipe: "배수관",
|
|
box_culvert: "BOX암거",
|
|
ford_pavement: "물넘이포장",
|
|
ford_bridge: "세월교",
|
|
};
|