Files
Aislo/resources/tester/helper_428_pipe_move.mjs
T
eomsangdonandClaude Opus 5.5 35758ac6ea fix(428 관 옮김 횡단 한 값): 옮긴 시설 측점 기본 설계 · 다시 셈 미교차 넓힘 · 관 PUT 끝 측점 세움 · 따라가기만 한 관 서버 누가거리
- 측점 갈아끼움 — 상단측 새 지반으로 · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자가 만진 칸
- 노선 초안 동안 관 길이 = 계산 줄 새 노선 상세
- 저장 노선 다시 셈 창구에 미교차 넓힘(서버 extend_unclosed 짝 · 격자 유무 지문)
- 관 PUT 끝 서버가 측점 다시 세움 · GET 관 지점에 x · y
- rebasePipeDraft — 땅 위 자리 = 정본인 관은 서버 누가거리
- 「측점이 없는 구조물」 알림 — 관 · 구조물 초안이 비면 저장 끝난 뒤 다시 봄
- 시험 test_428_pipe_move_mirror(옮긴 관 관 길이 브라우저 대 서버) · 416 출처 글 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
2026-10-18 15:53:03 +09:00

181 lines
6.8 KiB
JavaScript

/* PLAN 428 헬퍼 — 옮긴 관(0.5 m 넘게)의 횡단: 측점 갈아끼움(기본 설계 + 만진 칸) → 저장 노선 다시 셈(미교차
* 넓힘) → 관 길이. 서버 길(기본 설계 · 넓힘)과 같은 값인지 시험이 견줌 · 관 초안 얹기(rebasePipeDraft).
* node helper_428_pipe_move.mjs <묶음.mjs> <job.json> <grid.bin>
* 짝 = `test_428_pipe_move_mirror.py` · 표준출력 JSON 한 벌.
*/
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
const store = () => {
const m = new Map();
return {
getItem: (k) => (m.has(k) ? m.get(k) : null),
setItem: (k, v) => m.set(k, String(v)),
removeItem: (k) => m.delete(k),
key: (i) => [...m.keys()][i] ?? null,
get length() {
return m.size;
},
clear: () => m.clear(),
};
};
globalThis.window = globalThis;
globalThis.sessionStorage = store();
globalThis.localStorage = store();
const any = new Proxy(function () {}, {
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
apply: () => any,
construct: () => any,
});
globalThis.document = any;
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
const job = JSON.parse(readFileSync(process.argv[3], "utf8"));
const gridBytes = readFileSync(process.argv[4]);
// 설계 지형 격자 창구만 가짜 — 그 밖 요청은 404(관 초안 · 저장 관 없음)
globalThis.fetch = async (url) => {
const href = String(url);
if (href.includes("/route/terrain-sources"))
return Response.json({ design: "las", grid: { model_id: 1, smooth: false, signature: "s" } });
if (href.includes("/grid?")) return new Response(gridBytes);
return new Response("{}", { status: 404 });
};
const m = await import(pathToFileURL(process.argv[2]).href);
const P = "p428";
const R = 7;
m.writeState("std-cross", job.standard, P);
m.rememberRockBoundaryDefault(P, job.rockDefault);
/** 서버 횡단(표본 · 틀) → 묶음 측점 꼴(지반 = 중심 → 바깥 · 좌 +). */
const stationOf = (key, section) => {
const samples = section.samples;
const side = (sign) =>
samples
.filter((s) => s.offset_m * sign > 1e-9)
.map((s) => ({ offsetM: Math.abs(s.offset_m), z: s.elevation_m }))
.sort((a, b) => a.offsetM - b.offsetM);
const center = samples.find((s) => Math.abs(s.offset_m) < 1e-9);
return {
key,
kind: "pipe",
chainageM: section.chainage_m,
x: section.center_x,
y: section.center_y,
left: section.frame.left_xy,
azimuthDeg: section.azimuth_deg ?? 0,
planRadiusM: null,
curveOuterSide: null,
curveWideningM: null,
ground: { left: side(1), right: side(-1), centerZ: center ? center.elevation_m : null },
};
};
const culvertAt = (chainage) => ({ ...job.culvert, chainage_m: chainage });
const alignmentAt = (chainage, z) => ({
stations: [{ chainage_m: chainage, plan_elevation_m: z }],
samples: [
{ chainage_m: 0, elevation_m: z },
{ chainage_m: 100000, elevation_m: z },
],
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
});
const pipeLength = (section) =>
m.computeStoredLayouts(section, [section])?.culvert?.pipe?.lengthM ?? null;
const pick = (design) => ({
section_mode: design.section_mode,
ground_type: design.ground_type,
ditch_side: design.ditch_side ?? null,
two_stage_slope: design.two_stage_slope ?? null,
rock_boundary_offset_m: design.rock_boundary_offset_m ?? null,
inlet_structure: design.inlet_structure ?? null,
cut: design.cut_area_m2 ?? null,
fill: design.fill_area_m2 ?? null,
ends: design.design_line
? [design.design_line[0].offset_m, design.design_line.at(-1).offset_m]
: null,
});
const out = {};
const oldDetail = () => ({
longitudinal: { length_m: 1000, stations: [], profile_alignment: { policy: {} } },
cross_sections: [
{ ...structuredClone(job.old), kind: "irregular", culvert: culvertAt(job.old.chainage_m) },
],
});
/* 옮긴 관 — 0.5 m 넘게: 갈아끼움 → 다시 셈(넓힘) → 관 길이 */
{
const swap = m.swapCrossSections(
oldDetail(),
[stationOf("pipe:p-a", job.moved)],
["pipe:p-a"],
20,
);
const section = swap.detail.cross_sections[0];
out.swapped = { uphill: section.uphill_side, ...pick(section.design) };
section.culvert = culvertAt(job.moved.chainage_m); // 그림 자료가 새 자리에 다시 붙음(withDraftShapes 몫)
// 격자는 받기가 끝나야 씀 — 첫 다시 셈이 받기를 띄움 · 받은 뒤 다시 셈이 넓힘
await m.refreshCrossDesigns({
projectId: P,
routeId: R,
detail: swap.detail,
edits: { station_offsets: {}, curve_radii: {} },
alignment: alignmentAt(job.moved.chainage_m, job.elevation),
});
await new Promise((next) => setTimeout(next, 50));
out.firstSamples = section.samples.length; // 격자 받기 전 = 안 넓힘(지문에 격자 유무 · 받은 뒤 다시 셈)
await m.refreshCrossDesigns({
projectId: P,
routeId: R,
detail: swap.detail,
edits: { station_offsets: {}, curve_radii: {} },
alignment: alignmentAt(job.moved.chainage_m, job.elevation),
});
out.browser = {
...pick(section.design),
samples: section.samples.length,
sampleEnds: [section.samples[0].offset_m, section.samples.at(-1).offset_m],
pipeLength: pipeLength(section),
};
// 서버 = 같은 표본 · 기본 설계 → 서버 Node 가 같은 함수로 구조물 폐회로 면적 · 관 길이(`_recompute`)
const server = {
...structuredClone(job.moved),
kind: "irregular",
samples: job.serverSamples,
design: { ...job.serverDesign, inlet_structure: job.old.design.inlet_structure },
culvert: culvertAt(job.moved.chainage_m),
};
m.applyStructureAreaRows([server], m.structureAreaRows([server]));
out.server = { ...pick(server.design), pipeLength: pipeLength(server) };
}
/* 0.5 m 안 옮김 — 앞 설계 그대로(서버가 행을 옮겨 사용자 설계 값 그대로) */
{
const near = { ...stationOf("pipe:p-a", job.old), chainageM: job.old.chainage_m + 0.3 };
const swap = m.swapCrossSections(oldDetail(), [near], ["pipe:p-a"], 20);
out.near = pick(swap.detail.cross_sections[0].design);
}
/* 관 초안 얹기 — 따라가기만 한 관(땅 위 자리 = 정본) = 서버 누가거리 · 사용자가 옮긴 관 = 그대로 */
out.rebase = m
.rebasePipeDraft(
[
{ id: "f", chainage_m: 105, x: 10, y: 20, source: "user" },
{ id: "u", chainage_m: 210, x: 55, y: 66, source: "user" },
{ id: "s", chainage_m: 300, source: "user" },
],
[
{ id: "f", chainage_m: 100, x: 10, y: 20 },
{ id: "u", chainage_m: 200, x: 50, y: 60 },
{ id: "s", chainage_m: 300 },
],
[
{ id: "f", chainage_m: 104, x: 10.5, y: 20.5, source: "user" },
{ id: "u", chainage_m: 203, x: 51, y: 61, source: "user" },
{ id: "s", chainage_m: 301, x: 1, y: 1, source: "user" },
],
)
.map((pipe) => [pipe.id, pipe.chainage_m]);
process.stdout.write(JSON.stringify(out));