Files
Aislo/resources/tester/helper_b05_routeedit_view.cjs
T
eomsangdonandClaude Opus 5.5 a108c3ae11 feat(B05 평면 도구줄): 구분선 · 스냅 간격 고르기 · Ctrl+휠 5° 회전 · [기울기] 기본 꺼짐 (390-1 · 2 · 3)
- [측점 스냅] · 간격 | 분석 지형 구분선(분석 지형 숨으면 같이 숨김) · [기울기] 기본 끔
- 스냅 간격 0.1 · 1 · 10 m · 측점 No.(기본) — 프로젝트별 화면 기억 · 스냅 끔이면 흐림 · MC 를 간격 배수에 · 허용 오차 간격 따라(0.1 m = 0.005 m)
- Ctrl+휠 한 칸 5° 회전(단추 15° 그대로 · 손가락 · 트랙패드 벌림 = 확대 · 추천 지도 안 돎) · 도움말 한 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-17 17:17:51 +09:00

228 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 계획노선 편집 창 **보던 화면 기억**(계획서 18 ③)과 노드 끼우기·빼기 단위검증 헬퍼 —
* `B05_Profile_UI_RouteEdit_View.ts` · `_Edits.ts` 를 그 자리에서 트랜스파일해 Node 로 돌린다.
*
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_cut_merged_loop.cjs` 머리에 적힌 것과 같다.
* 브라우저 전용인 저장소·팔레트는 가짜로 채운다 — 셈만 본다.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
// 브라우저 저장소 흉내 — `b_page_state` 가 읽고 쓰는 자리.
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
_map: map,
};
}
const local = memoryStorage();
globalThis.window = { localStorage: local, sessionStorage: memoryStorage() };
globalThis.fetch = () => Promise.resolve({ ok: false });
const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } };
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;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const viewMod = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_View.ts"));
const render = loadTs(path.join(ROOT, "B04_PreProcess", "B04_PreProcess_UI_MapRender.ts"));
const edits = loadTs(path.join(ROOT, "B05_Profile", "logic", "B05_Profile_UI_RouteEdit_Edits.ts"));
// 도엽 한 장 — 1200m × 900m 사업지 좌표.
const meta = {
x_min: 200000,
y_min: 500000,
width_meters: 1200,
height_meters: 900,
lon_min: 127,
lon_max: 127.01,
lat_min: 37,
lat_max: 37.008,
};
function viewOf(width, height, scale, offsetX, offsetY) {
return {
width,
height,
scale,
offsetX,
offsetY,
mapRect: render.computeMapRect(meta, width, height),
};
}
// ① 한 창 크기에서 잡은 화면을 **다른 창 크기**로 되살린다 — 가운데 자리와 1m 당 픽셀이 같아야 한다.
const before = viewOf(1000, 700, 3.2, -140, 85);
const saved = viewMod.captureView(meta, before, (30 * Math.PI) / 180);
const resized = viewOf(1400, 820, 1, 0, 0);
const restored = { ...resized, ...viewMod.restoreSavedView(meta, resized, saved) };
const again = viewMod.captureView(meta, restored, (30 * Math.PI) / 180);
// ② 저장소 왕복 — 적은 값을 같은 프로젝트로 다시 읽고, 다른 프로젝트에는 없다.
const memory = viewMod.createViewMemory("p-1", () => saved);
memory.flush();
const readBack = viewMod.readSavedView("p-1");
const otherProject = viewMod.readSavedView("p-2");
const storedKeys = [...local._map.keys()];
// ③ 보던 화면이 없으면 노선 전체에 맞춘다(회전 0).
const fresh = viewMod.openingView("p-none", meta, viewOf(1000, 700, 1, 0, 0), [
[200300, 500300],
[200600, 500500],
]);
const withSaved = viewMod.openingView("p-1", meta, viewOf(1000, 700, 1, 0, 0), [
[saved.cx, saved.cy],
]);
// ③-2 기억한 화면에 노선이 한 점도 안 보이면(밀려 저장된 화면) 버리고 노선에 맞춘다.
const strayMemory = viewMod.createViewMemory("p-stray", () => ({
cx: 201150,
cy: 500850,
pxPerMeter: 8,
rotationDeg: 45,
}));
strayMemory.flush();
const strayNodes = [
[200300, 500300],
[200400, 500350],
];
const stray = viewMod.openingView("p-stray", meta, viewOf(1000, 700, 1, 0, 0), strayNodes);
const strayOnScreen = strayNodes.some(([x, y]) => {
const [px, py] = render.metricToScreen(meta, stray.view, x, y);
return px >= 0 && px <= 1000 && py >= 0 && py <= 700;
});
// ③-3 창 크기가 바뀌어도(편집 중 resize) 가운데 · 배율이 그대로 — 편집 창 `resize()` 와 같은 셈.
const zoomed = viewOf(1500, 1000, 6, -900, 400);
const keptBefore = viewMod.captureView(meta, zoomed, 0);
const shrunk = viewOf(1280, 760, zoomed.scale, zoomed.offsetX, zoomed.offsetY);
const refit = { ...shrunk, ...viewMod.restoreSavedView(meta, shrunk, keptBefore) };
const keptAfter = viewMod.captureView(meta, refit, 0);
// ③-4 숨김(크기 0) 뒤 다시 보임(349-0h) — 크기 0 으로 셈한 화면은 엉뚱함 · 그 셈을 건너뛰고
// 떠나기 전 화면을 다시 보일 때 크기로 되살리면 그대로.
const shown = viewOf(1500, 1000, 4, -600, 250);
const keptShown = viewMod.captureView(meta, shown, 0);
const zero = viewOf(0, 0, shown.scale, shown.offsetX, shown.offsetY);
const zeroView = { ...zero, ...viewMod.restoreSavedView(meta, zero, keptShown) };
const zeroBack = viewOf(1500, 1000, 1, 0, 0);
const fromZero = viewMod.captureView(
meta,
{
...zeroBack,
...viewMod.restoreSavedView(meta, zeroBack, viewMod.captureView(meta, zeroView, 0)),
},
0,
);
const back = viewOf(1500, 1000, shown.scale, shown.offsetX, shown.offsetY);
const keptBack = viewMod.captureView(
meta,
{
...back,
...viewMod.restoreSavedView(meta, back, keptShown),
},
0,
);
// ④ 노드 끼우기·빼기 — 편집값 여섯 줄이 같은 자리에서 함께 움직인다.
const columns = {
planned: [
[0, 0],
[10, 0],
[20, 0],
],
curveOn: [true, false, true],
curveRadius: [null, 30, null],
curveLock: [null, "radius", null],
curveArc: [null, 12, null],
apexLock: [false, true, false],
};
edits.spliceNode(columns, 1, [5, 5]);
const afterInsert = JSON.parse(JSON.stringify(columns));
edits.spliceNode(columns, 1);
const afterRemove = JSON.parse(JSON.stringify(columns));
// ⑤ Ctrl+휠 = 5° 돌리기(390-3) · 손가락 · 트랙패드 벌림 · 맨 휠 = 확대 · rotateWheel 없으면(추천 지도) 확대
const input = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Input.ts"));
const rotateMod = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Rotate.ts"));
function wheelCase(withRotate, event) {
const handlers = {};
const canvas = {
addEventListener: (name, fn) => (handlers[name] = fn),
getBoundingClientRect: () => ({ left: 0, top: 0, width: 400, height: 300 }),
};
let wheelView = {
width: 400,
height: 300,
scale: 1,
offsetX: 0,
offsetY: 0,
mapRect: { width: 400 },
};
let changes = 0;
const rotation = rotateMod.createMapRotation({
overlay: { querySelector: () => null },
size: () => wheelView,
onChange: () => (changes += 1),
});
input.bindRouteEditNavigation({
canvas,
getView: () => wheelView,
setView: (next) => (wheelView = next),
getMeta: () => null,
rotateWheel: withRotate ? rotation.wheel : undefined,
draw: () => {},
});
handlers.wheel({ clientX: 200, clientY: 150, preventDefault: () => {}, ...event });
return { deg: (rotation.radians() * 180) / Math.PI, scale: wheelView.scale, changes };
}
const wheel = {
ctrlDown: wheelCase(true, { ctrlKey: true, deltaY: 100 }),
ctrlUp: wheelCase(true, { ctrlKey: true, deltaY: -100 }),
trackpad: wheelCase(true, { ctrlKey: true, deltaY: -4 }),
plain: wheelCase(true, { ctrlKey: false, deltaY: -100 }),
recommend: wheelCase(false, { ctrlKey: true, deltaY: 100 }),
};
process.stdout.write(
JSON.stringify({
wheel,
saved,
again,
readBack,
otherProject,
storedKeys,
fresh: { rotationRad: fresh.rotationRad, scale: fresh.view.scale },
withSaved: { rotationRad: withSaved.rotationRad },
stray: { rotationRad: stray.rotationRad, onScreen: strayOnScreen },
keptBefore,
keptAfter,
hidden: { keptShown, fromZero, keptBack },
afterInsert,
afterRemove,
}),
);