perf(3D): 345-4 3D 길 모형 웹 워커 — 저장 안 한 초안 미리보기를 워커에서(buildCorridor 그대로) · 리본 좌표 Transferable · 주 스레드 멈춤 0 · 저장본 먼저(해시 맞으면 받기만)
- 새 _Corridor_Worker(워커 몸 · handleCorridorRequest) · _Corridor_Worker_Client(buildCorridorOffThread · 워커 없거나 깨지면 주 스레드 같은 함수) - ensureCorridor 의 짓기만 워커로 · vite 설정 그대로(new Worker(new URL(…)) 기본 지원) - 서버 Node 묶음 입구 · Build 안 고침(다시 짓기 불필요) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -6,8 +6,9 @@
|
|||||||
* 1) 짓기 = 서버 Node(`B05_Profile_Corridor_Prebuild`) — 자동 체인 · 노선 [확인] 체인 ·
|
* 1) 짓기 = 서버 Node(`B05_Profile_Corridor_Prebuild`) — 자동 체인 · 노선 [확인] 체인 ·
|
||||||
* B06 [저장] · [확정] 끝. 브라우저는 올리지 않는다(PUT 없음).
|
* B06 [저장] · [확정] 끝. 브라우저는 올리지 않는다(PUT 없음).
|
||||||
* 2) 받기: 저장본 GET → 버전 해시가 현 정본과 맞으면 그대로 표시.
|
* 2) 받기: 저장본 GET → 버전 해시가 현 정본과 맞으면 그대로 표시.
|
||||||
* 3) 어긋나면(저장 안 한 초안 — 벽 · 구조물) [3D 업데이트] 때만 브라우저가 미리보기로
|
* 3) 어긋나면(저장 안 한 초안 — 벽 · 구조물) [3D 업데이트] · 낡은 채 3D 모달을 열 때만 브라우저가
|
||||||
* 지음 — 메모리에만 · 올리지 않음.
|
* 미리보기로 지음 — **웹 워커**(`_Corridor_Worker_Client` · 345-4 · 주 스레드 멈춤 0) · 메모리에만 ·
|
||||||
|
* 올리지 않음.
|
||||||
* 버전 해시 = 종횡단 정본의 설계 입력 요약(FNV-1a) — 측점·계획고·설계선이
|
* 버전 해시 = 종횡단 정본의 설계 입력 요약(FNV-1a) — 측점·계획고·설계선이
|
||||||
* 바뀌면 달라진다. 구조물 서피스 훅(CorridorStructureHook)은 예약만(범위 제외).
|
* 바뀌면 달라진다. 구조물 서피스 훅(CorridorStructureHook)은 예약만(범위 제외).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
@@ -21,7 +22,8 @@ import {
|
|||||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||||
import { refreshStaleDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
|
import { refreshStaleDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
|
||||||
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||||
import { buildCorridor, type CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
|
import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
|
||||||
|
import { buildCorridorOffThread } from "./B05_Profile_UI_Corridor_Worker_Client";
|
||||||
import {
|
import {
|
||||||
corridorHash,
|
corridorHash,
|
||||||
ENVELOPE_VERSION,
|
ENVELOPE_VERSION,
|
||||||
@@ -169,7 +171,7 @@ export async function ensureCorridor(
|
|||||||
// 종단 계획선 샘플을 함께 넘겨 측점 사이가 종단곡선을 따라 부드럽게 이어지게 한다.
|
// 종단 계획선 샘플을 함께 넘겨 측점 사이가 종단곡선을 따라 부드럽게 이어지게 한다.
|
||||||
// 라이브 편집분(alignment.samples)이 있으면 그걸 쓴다 — 정본 design_profiles는
|
// 라이브 편집분(alignment.samples)이 있으면 그걸 쓴다 — 정본 design_profiles는
|
||||||
// 편집 전 값이라 전환점·종단 보정이 낡은 계획고를 따라간다(2026-08-23 지적 ③).
|
// 편집 전 값이라 전환점·종단 보정이 낡은 계획고를 따라간다(2026-08-23 지적 ③).
|
||||||
const build = buildCorridor(
|
const build = await buildCorridorOffThread(
|
||||||
detail.cross_sections,
|
detail.cross_sections,
|
||||||
routePoints,
|
routePoints,
|
||||||
designSamples ?? detail.longitudinal.design_profiles?.[0]?.samples,
|
designSamples ?? detail.longitudinal.design_profiles?.[0]?.samples,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B05_Profile_UI_Corridor_Worker.ts
|
||||||
|
* 3D 길 모형 **웹 워커**(345-4) — 저장 안 한 초안의 미리보기를 주 스레드 밖에서 짓는다.
|
||||||
|
*
|
||||||
|
* 짓는 셈은 `buildCorridor` 그대로(서버 Node 묶음 `B05_Profile_Corridor_Node` 와 같은 순수 계산) —
|
||||||
|
* 새로 짜지 않는다. 결과의 리본 좌표(Float64Array)는 복사 없이 넘긴다(Transferable).
|
||||||
|
* 부르는 쪽은 `_Corridor_Worker_Client`. 이 파일 맨 위에 DOM · window 를 두지 않는다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||||
|
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||||
|
import { buildCorridor, type CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
|
||||||
|
|
||||||
|
export interface CorridorWorkerRequest {
|
||||||
|
id: number;
|
||||||
|
crossSections: CrossSection[];
|
||||||
|
routePoints: RoutePoint[];
|
||||||
|
designSamples?: Array<{ chainage_m: number; elevation_m: number; ground_elevation_m?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CorridorWorkerResponse {
|
||||||
|
id: number;
|
||||||
|
build: CorridorBuildResult | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 넘길 버퍼 — 리본 좌표(가장 큼). 나머지는 구조화 복제. */
|
||||||
|
export function corridorTransfers(build: CorridorBuildResult | null): ArrayBuffer[] {
|
||||||
|
return build ? build.ribbons.map((ribbon) => ribbon.positions.buffer as ArrayBuffer) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 요청 하나 — 워커 안에서도 시험에서도 같은 함수. */
|
||||||
|
export function handleCorridorRequest(request: CorridorWorkerRequest): CorridorWorkerResponse {
|
||||||
|
try {
|
||||||
|
const build = buildCorridor(request.crossSections, request.routePoints, request.designSamples);
|
||||||
|
return { id: request.id, build };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
id: request.id,
|
||||||
|
build: null,
|
||||||
|
error: error instanceof Error ? error.message : "짓기 실패",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerScope {
|
||||||
|
onmessage: ((event: { data: CorridorWorkerRequest }) => void) | null;
|
||||||
|
postMessage: (message: CorridorWorkerResponse, transfer: ArrayBuffer[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 워커로 돌 때만 듣는다(모듈을 시험 · 주 스레드가 들여와도 아무 일 없음).
|
||||||
|
if (typeof (globalThis as { WorkerGlobalScope?: unknown }).WorkerGlobalScope !== "undefined") {
|
||||||
|
const scope = globalThis as unknown as WorkerScope;
|
||||||
|
scope.onmessage = (event) => {
|
||||||
|
const response = handleCorridorRequest(event.data);
|
||||||
|
scope.postMessage(response, corridorTransfers(response.build));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B05_Profile_UI_Corridor_Worker_Client.ts
|
||||||
|
* 3D 길 모형 워커 부르기(345-4) — `buildCorridorOffThread` 가 워커(`_Corridor_Worker`)에 맡겨 주 스레드
|
||||||
|
* 멈춤 없이 짓는다. 워커를 못 쓰는 곳(시험 · 옛 브라우저) · 워커가 깨지면 주 스레드에서 같은 함수로 짓는다.
|
||||||
|
* 워커는 처음 부를 때 하나 세워 페이지 수명 동안 다시 쓴다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||||
|
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||||
|
import { buildCorridor, type CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
|
||||||
|
import type {
|
||||||
|
CorridorWorkerRequest,
|
||||||
|
CorridorWorkerResponse,
|
||||||
|
} from "./B05_Profile_UI_Corridor_Worker";
|
||||||
|
|
||||||
|
type Samples = CorridorWorkerRequest["designSamples"];
|
||||||
|
|
||||||
|
let worker: Worker | null = null;
|
||||||
|
let seq = 0;
|
||||||
|
const waits = new Map<number, (response: CorridorWorkerResponse | null) => void>();
|
||||||
|
|
||||||
|
function settleAll(): void {
|
||||||
|
waits.forEach((resolve) => resolve(null));
|
||||||
|
waits.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorker(): Worker | null {
|
||||||
|
if (worker) return worker;
|
||||||
|
if (typeof Worker === "undefined") return null;
|
||||||
|
try {
|
||||||
|
worker = new Worker(new URL("./B05_Profile_UI_Corridor_Worker.ts", import.meta.url), {
|
||||||
|
type: "module",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
worker.onmessage = (event: MessageEvent<CorridorWorkerResponse>) => {
|
||||||
|
const resolve = waits.get(event.data.id);
|
||||||
|
waits.delete(event.data.id);
|
||||||
|
resolve?.(event.data);
|
||||||
|
};
|
||||||
|
// 워커가 깨지면 버리고 기다리던 것은 주 스레드로 — 다음 부름은 새 워커.
|
||||||
|
worker.onerror = () => {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
settleAll();
|
||||||
|
};
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 초안 미리보기 짓기 — 결과는 `buildCorridor` 와 같음(같은 입력 · 같은 함수). */
|
||||||
|
export async function buildCorridorOffThread(
|
||||||
|
crossSections: CrossSection[],
|
||||||
|
routePoints: RoutePoint[],
|
||||||
|
designSamples?: Samples,
|
||||||
|
): Promise<CorridorBuildResult | null> {
|
||||||
|
const onMain = (): CorridorBuildResult | null =>
|
||||||
|
buildCorridor(crossSections, routePoints, designSamples);
|
||||||
|
const target = getWorker();
|
||||||
|
if (!target) return onMain();
|
||||||
|
const id = ++seq;
|
||||||
|
const response = await new Promise<CorridorWorkerResponse | null>((resolve) => {
|
||||||
|
waits.set(id, resolve);
|
||||||
|
try {
|
||||||
|
target.postMessage({ id, crossSections, routePoints, designSamples });
|
||||||
|
} catch {
|
||||||
|
waits.delete(id); // 복제 못 하는 값 — 주 스레드로
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response || response.error) return onMain();
|
||||||
|
return response.build;
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/* 345-4 3D 길 모형 웹 워커 헬퍼 — 워커 몸(`handleCorridorRequest`) · 부르기(`buildCorridorOffThread`)를
|
||||||
|
* 트랜스파일해 같은 입력으로 주 스레드 `buildCorridor` 와 견줌. 워커 경계는 structuredClone(+transfer)로 흉내 —
|
||||||
|
* 브라우저 postMessage 와 같은 복제 규칙. 측점 꼴은 helper_115_8_corridor_loft.cjs 와 같다. 결과는 JSON 한 벌.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const ROOT = path.join(__dirname, "..", "..");
|
||||||
|
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||||
|
|
||||||
|
const cache = new Map();
|
||||||
|
function loadTs(file) {
|
||||||
|
const full = path.resolve(file);
|
||||||
|
if (cache.has(full)) return cache.get(full);
|
||||||
|
const js = ts
|
||||||
|
.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||||
|
})
|
||||||
|
.outputText.replace(/import\.meta\.url/g, '"file:///x.js"');
|
||||||
|
const box = { exports: {} };
|
||||||
|
cache.set(full, box.exports);
|
||||||
|
const localRequire = (request) => {
|
||||||
|
if (request.startsWith("@ui/") || request.startsWith("@config/"))
|
||||||
|
return new Proxy({}, { get: () => () => undefined });
|
||||||
|
const target = request.startsWith("@util/")
|
||||||
|
? path.join(ROOT, "common_util", request.slice(6))
|
||||||
|
: path.join(path.dirname(full), request);
|
||||||
|
if (request.endsWith(".json")) return JSON.parse(fs.readFileSync(target, "utf8"));
|
||||||
|
return loadTs(fs.existsSync(`${target}.ts`) ? `${target}.ts` : path.join(target, "index.ts"));
|
||||||
|
};
|
||||||
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||||
|
return box.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`);
|
||||||
|
const { buildCorridor } = loadTs(B05("B05_Profile_UI_Corridor_Build"));
|
||||||
|
const { handleCorridorRequest, corridorTransfers } = loadTs(B05("B05_Profile_UI_Corridor_Worker"));
|
||||||
|
const { buildCorridorOffThread } = loadTs(B05("B05_Profile_UI_Corridor_Worker_Client"));
|
||||||
|
|
||||||
|
const HALF = 3;
|
||||||
|
const ROAD_Z = 100;
|
||||||
|
function station(chainage, cx, cy, lx, ly, groundZ, ratio) {
|
||||||
|
const w = Math.abs(groundZ - ROAD_Z) * ratio;
|
||||||
|
const mode = groundZ < ROAD_Z ? "both_fill" : "both_cut";
|
||||||
|
const pts = [
|
||||||
|
[-40, groundZ],
|
||||||
|
[-(HALF + w), groundZ],
|
||||||
|
[-HALF, ROAD_Z],
|
||||||
|
[-2.5, ROAD_Z],
|
||||||
|
[2.5, ROAD_Z],
|
||||||
|
[HALF, ROAD_Z],
|
||||||
|
[HALF + w, groundZ],
|
||||||
|
[40, groundZ],
|
||||||
|
];
|
||||||
|
const samples = [];
|
||||||
|
for (let o = -40; o <= 40; o += 1) samples.push({ offset_m: o, elevation_m: groundZ });
|
||||||
|
return {
|
||||||
|
station_id: `S${chainage}`,
|
||||||
|
chainage_m: chainage,
|
||||||
|
label: "",
|
||||||
|
kind: "regular",
|
||||||
|
center_x: cx,
|
||||||
|
center_y: cy,
|
||||||
|
center_z: groundZ,
|
||||||
|
frame: { left_xy: [lx, ly] },
|
||||||
|
samples,
|
||||||
|
design: {
|
||||||
|
section_mode: mode,
|
||||||
|
road_edges: {
|
||||||
|
left: { offset_m: HALF, elevation_m: ROAD_Z },
|
||||||
|
right: { offset_m: -HALF, elevation_m: ROAD_Z },
|
||||||
|
},
|
||||||
|
carriageway_edges: {
|
||||||
|
left: { offset_m: 2.5, elevation_m: ROAD_Z },
|
||||||
|
right: { offset_m: -2.5, elevation_m: ROAD_Z },
|
||||||
|
},
|
||||||
|
design_line: pts.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })),
|
||||||
|
ditch: { type: "none" },
|
||||||
|
ditch_enabled: false,
|
||||||
|
ditch_side: "left",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
station(0, 0, 0, 0, 1, 96, 1.5),
|
||||||
|
station(20, 20, 0, 0, 1, 104, 1.0),
|
||||||
|
station(40, 40, 0, 0, 1, 96, 1.5),
|
||||||
|
];
|
||||||
|
const route = [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 40, y: 0 },
|
||||||
|
];
|
||||||
|
const designSamples = [
|
||||||
|
{ chainage_m: 0, elevation_m: 100, ground_elevation_m: 96 },
|
||||||
|
{ chainage_m: 20, elevation_m: 100, ground_elevation_m: 104 },
|
||||||
|
{ chainage_m: 40, elevation_m: 100, ground_elevation_m: 96 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 비교용 평탄화 — 타입 배열은 숫자 배열로. */
|
||||||
|
const plain = (value) =>
|
||||||
|
JSON.parse(JSON.stringify(value, (_k, v) => (ArrayBuffer.isView(v) ? Array.from(v) : v)));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const out = {};
|
||||||
|
const direct = buildCorridor(sections, route, designSamples);
|
||||||
|
// 워커 경계 흉내 — 요청 · 응답 둘 다 구조화 복제 · 응답은 리본 버퍼를 넘김(transfer)
|
||||||
|
const request = structuredClone({
|
||||||
|
id: 7,
|
||||||
|
crossSections: sections,
|
||||||
|
routePoints: route,
|
||||||
|
designSamples,
|
||||||
|
});
|
||||||
|
const response = handleCorridorRequest(request);
|
||||||
|
const transfer = corridorTransfers(response.build);
|
||||||
|
const received = structuredClone(response, { transfer });
|
||||||
|
out.ribbonCount = direct.ribbons.length;
|
||||||
|
out.idKept = received.id === 7;
|
||||||
|
out.sameAsMain = JSON.stringify(plain(received.build)) === JSON.stringify(plain(direct));
|
||||||
|
out.float64 = received.build.ribbons.every((r) => r.positions instanceof Float64Array);
|
||||||
|
out.transferCount = transfer.length;
|
||||||
|
out.detached = response.build.ribbons.every((r) => r.positions.byteLength === 0); // 복사 없이 넘김
|
||||||
|
// 측점 모자람 → null(워커도 같음)
|
||||||
|
out.tooFew = handleCorridorRequest({
|
||||||
|
id: 1,
|
||||||
|
crossSections: [sections[0]],
|
||||||
|
routePoints: route,
|
||||||
|
}).build;
|
||||||
|
// 워커 없는 곳(시험 · 옛 브라우저) — 부르기가 주 스레드 같은 함수로
|
||||||
|
const fallback = await buildCorridorOffThread(sections, route, designSamples);
|
||||||
|
out.fallbackSame = JSON.stringify(plain(fallback)) === JSON.stringify(plain(direct));
|
||||||
|
console.log(JSON.stringify(out));
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user