- [측점 스냅] · 간격 | 분석 지형 구분선(분석 지형 숨으면 같이 숨김) · [기울기] 기본 끔 - 스냅 간격 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
228 lines
8.2 KiB
JavaScript
228 lines
8.2 KiB
JavaScript
/* 계획노선 편집 창 **보던 화면 기억**(계획서 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,
|
||
}),
|
||
);
|