Files
Aislo/resources/tester/helper_453_plan_pick_draft.cjs
T
eomsangdonandClaude Opus 5.5 92e5157890 fix(453 B05 평면 편집): 노드 가운데만 노드 우선 · 손잡이 끌기 측점 맞춤 · 막힌 걸음 놓음 되돌림 · 스냅만 바꾼 초안
- 433-14 ③ · 433-21 ③ 누름 · 맴돔 · 커서 한 판정(pressWinner) — 노드 가운데 6 px = 노드 · 밖 = 마커 · 노드 · 손잡이 중 가까운 것
- 433-21 ① 손잡이 끌기 = 노드 갈래와 같게 — 맞추기 전 자리에서 셈 · 미리보기 맞춤 · 측점 밖이 늘면 그 걸음 되돌림
- 433-14 ① 막힌 걸음만 하고 놓으면 미리보기 맞춤을 누르기 전 자리로
- 433-14 ② 사진첩 다리도 스냅 · 분석 지형 바뀜을 봄 — 노선이 처음으로 돌아가도 초안
- 433-1 test_365 ⑧ 이름 = 노선 한 걸음 + 계획고 맞춤
- 시험 test_453_plan_pick_draft 새로 · 390_5 손잡이 훑기 · 381_3 · 399 · 409 · 430 배선 글 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:33:24 +09:00

159 lines
6.5 KiB
JavaScript

/* 453 평면 집기 · 스냅만 바꾼 초안 헬퍼 — 진짜 `_RouteEdit_Input`(pressWinner · handleAtScreen) ·
* `_RouteEdit_History` · `_RouteEdit_Draft`(다리 감싸기)를 트랜스파일해 돌림. 결과 JSON 한 벌(표준출력).
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const data = new Map();
globalThis.sessionStorage = {
get length() {
return data.size;
},
key: (i) => [...data.keys()][i] ?? null,
getItem: (k) => (data.has(k) ? data.get(k) : null),
setItem: (k, v) => data.set(k, String(v)),
removeItem: (k) => data.delete(k),
};
const win = new EventTarget();
win.setTimeout = setTimeout;
win.clearTimeout = clearTimeout;
win.sessionStorage = globalThis.sessionStorage;
win.localStorage = { getItem: () => null };
globalThis.window = win;
const STUBS = {
"@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback },
"@util/common_util_svg": { stationLabel: (c, i) => `No.${Math.round(c / i)}` },
};
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;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const page = path.join(ROOT, "B05_Profile");
const input = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Input.ts"));
const edit = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_History.ts"));
const draftLink = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Draft.ts"));
// ① 누름 판정 — `_Pointer.chooseAt` 와 같은 셈(노드 · 손잡이는 화면 8 px 떨어짐 · 433-14 ③ 낮은 확대 R 12)
const toScreen = ([x, y]) => [x, y];
const node = [100, 100];
const curves = [{ node_first: 1, start: [108, 100], end: [100, 108] }];
const choose = (px, py, markDistance = Infinity) => {
const pick = input.pickAtScreen([[0, 0], node, [200, 200]], toScreen, px, py, 12, 6, null);
const at = pick && "node" in pick ? toScreen(node) : null;
const handle = input.handleAtScreen(curves, toScreen, px, py, 14);
return input.pressWinner(
at ? Math.hypot(at[0] - px, at[1] - py) : Infinity,
6,
markDistance,
handle?.distance ?? Infinity,
);
};
const pick = {
handleSide: choose(107, 100), // 노드 7 px(가운데 밖) · 손잡이 1 px — 손잡이
handleCenter: choose(108, 100), // 손잡이 위 · 노드 8 px — 손잡이
coreEdge: choose(106, 100), // 노드 6 px(가운데 끝) · 손잡이 2 px — 노드
nodeCore: choose(102, 100), // 노드 2 px(가운데 안) — 노드
nodeCloser: choose(93, 100), // 노드 7 px · 손잡이 15 px(문턱 밖) — 노드
handleOnly: choose(116, 100), // 노드 16 px(문턱 밖) · 손잡이 8 px — 손잡이
nothing: choose(150, 150), // 빈 곳 — pick(null 그대로)
markOnCore: choose(103, 100, 1), // 마커 1 px 이어도 노드 가운데 3 px — 노드(433-21 ③)
markCloser: choose(93, 100, 3), // 노드 7 px · 마커 3 px — 마커
nodeBeatsMark: choose(93, 100, 9), // 노드 7 px · 마커 9 px — 노드
tie: input.pressWinner(8, 6, 8, 8), // 같으면 마커 → 노드 → 손잡이
handleDistance: input.handleAtScreen(curves, toScreen, 111, 100, 14)?.distance ?? null,
};
// ② 스냅만 바꾼 뒤 노선 처음으로 — 다리 초안(433-14 ②)
const snap = (x) => ({
planned: [
[0, 0],
[x, 0],
[100, 0],
],
curveOn: [true, true, true],
curveRadius: [null, null, null],
curveLock: [null, null, null],
curveArc: [null, null, null],
apexLock: [false, false, false],
picked: -1,
});
const draft = (snapChanged) => {
const told = [];
const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null };
const history = draftLink.watch(
edit.createRouteEditHistory(snap(50)),
bridge,
() => ({ stationSnap: true, snapStepM: 1 }),
() => snapChanged,
);
history.commit(snap(60));
history.undo();
history.redo();
history.undo();
return told;
};
const old = (() => {
const told = [];
const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null };
const history = draftLink.watch(edit.createRouteEditHistory(snap(50)), bridge, () => ({}));
history.commit(snap(60));
history.undo();
return told;
})();
// ③ 맞추지 않은 기준에서 막힌 걸음만 하고 놓음(433-14 ① · req269 idem 재현) — 막힌 걸음 미리보기
// (`fitPreview` = `fitDrag`)가 노선을 바꿈 → `endDrag` 가 맞추기 전 자리(`dragRaw`)로 되돌림.
const fit = loadTs(path.join(page, "logic", "B05_Profile_RouteEdit_StationFit.ts"));
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: 20 };
const ROUTE = [[0, 0]];
{
let seed = 1;
const rnd = () => ((seed = (seed * 16807) % 2147483647), seed / 2147483647);
let a = 0;
for (let i = 0; i < 14; i += 1) {
a += (rnd() - 0.5) * 1.6;
const L = 35 + rnd() * 60;
const q = ROUTE[ROUTE.length - 1];
ROUTE.push([q[0] + Math.cos(a) * L, q[1] + Math.sin(a) * L]);
}
}
const cols = (planned) => ({
planned,
curveOn: planned.map(() => true),
curveRadius: planned.map(() => null),
curveLock: planned.map(() => null),
curveArc: planned.map(() => null),
apexLock: planned.map((_, i) => i === 0 || i === planned.length - 1),
});
const gap = (a, b) => Math.max(...a.map((v, i) => Math.hypot(v[0] - b[i][0], v[1] - b[i][1])));
const blocked = { drags: 0, previewMoved: 0, leftAfterRelease: 0 };
for (let K = 1; K < ROUTE.length - 1; K += 1) {
const base = ROUTE.map(([x, y]) => [x, y]);
const dragRaw = base.map(([x, y]) => [x, y]); // 막힌 걸음 = 누르기 전 자리 그대로 맞춤
let planned = fit.fitDrag(cols(base), base, K, LIMITS);
blocked.drags += 1;
if (gap(planned, base) > 1e-6) blocked.previewMoved += 1;
dragRaw.forEach((vertex, index) => (planned[index] = vertex)); // endDrag(!dragMoved && dragRaw)
if (gap(planned, base) > 1e-6) blocked.leftAfterRelease += 1;
}
process.stdout.write(
JSON.stringify({ pick, blocked, draft: { snapChanged: draft(true), same: draft(false), old } }),
);