/* PLAN 428 헬퍼 — 옮긴 관(0.5 m 넘게)의 횡단: 측점 갈아끼움(기본 설계 + 만진 칸) → 저장 노선 다시 셈(미교차 * 넓힘) → 관 길이. 서버 길(기본 설계 · 넓힘)과 같은 값인지 시험이 견줌 · 관 초안 얹기(rebasePipeDraft). * node helper_428_pipe_move.mjs <묶음.mjs> * 짝 = `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));