Files
Aislo/resources/tester/helper_430_route_edit_rules.cjs
T
eomsangdonandClaude Opus 5.5 4fefd219c2 feat(B05 평면 형상 제어): 하한 검사 applyEdit 한 곳 · 곡선 삭제 155° 경고 · 놓을 때 맞춤 보고 · 스냅 즉시 계산 줄 · 넣은 R · 고정 곡선표 저장 (430)
- 하한 검사 = 모든 평면 조작이 applyEdit 에서 지난 걸음과 견줌 · 새로 깨면 그 걸음으로 되돌림 · 까닭 상태줄(끼우기 · 지우기 · 놓기 · 방향키 · 곡선 칸) · 손잡이 끌기도 걸음마다 검사
- 곡선 삭제 = 내각 155° 미만이면 막지 않고 경고(상태줄 · 알림)
- 끌기 · 손잡이 놓을 때 스냅 켬이면 「함께 밀린 교점 · 못 맞춘 곡선」 보고
- 스냅 끔 · 간격 바꿈 = 계산 줄에 초안 바로(tellDraft)
- R · 길이 고정 · 넣은 R = 곡선표 edits 칸(짝 B05_Profile_Route_NodeEdits.py ↔ flattenServerPlan) · 다시 열면 넣은 값으로
- 상태줄 = 겹친 노드(0.5 m 안) · 못 그린 곡선 표시
- 시험 test_430_route_edit_rules · 배선 바뀐 시험 둘(352-3 · 390-4) · 398-2 도우미 알림 가짜

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-18 16:07:48 +09:00

138 lines
5.0 KiB
JavaScript

/* 430 평면 형상 제어 규칙 한 곳 — `logic/B05_Profile_UI_RouteEdit_Edits.ts`(하한 검사 · 겹친 노드 ·
* 못 그린 곡선 · 넣은 값 되살림) · `_KeyNudge.fitNote` 를 Node 로 돌려 JSON 한 줄
* (`test_430_route_edit_rules.py` 가 판정).
* node helper_430_route_edit_rules.cjs <서버 곡선표 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 LOGIC = path.join(ROOT, "B05_Profile", "logic");
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) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
const nudge = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_KeyNudge.ts"));
const LIMITS = { minRadiusM: 30, limitRadiusM: 20, limitArcM: 0 };
// IP1 · IP2 · IP3 — 직선 넉넉(곡선 R 30 다 들어감)
const ROUTE = [
[0, 0],
[100, 0],
[200, 60],
[300, 0],
[400, 0],
];
function cols(planned) {
const n = planned.length;
return {
planned: planned.map((v) => [v[0], v[1]]),
curveOn: Array(n).fill(true),
curveRadius: Array(n).fill(null),
curveLock: Array(n).fill(null),
curveArc: Array(n).fill(null),
apexLock: Array(n).fill(false),
};
}
/** 지난 걸음 `before` 에서 `change` 로 고친 뒤 하한 검사 글. */
function stop(before, change) {
const now = cols(before.planned);
change(now);
const built = edits.rebuildLine(now, LIMITS);
return edits.floorStop(before, now.planned, built.nodes, LIMITS);
}
const base = cols(ROUTE);
const out = {
// 같은 노드 수 — IP2 를 IP1 쪽 8 m 앞으로(IP1 · IP2 접선 자리 모자람 → R 하한 아래)
moveBreak: stop(base, (c) => (c.planned[2] = [108, 4])),
moveOk: stop(base, (c) => (c.planned[2] = [200, 70])),
// 끼우기 — IP1 바로 뒤 4 m 에 새 노드(IP1 접선 자리 2 m)
insertBreak: stop(base, (c) => {
c.planned.splice(2, 0, [104, 1]);
for (const k of ["curveOn", "curveRadius", "curveLock", "curveArc", "apexLock"])
c[k].splice(2, 0, k === "curveOn" ? true : k === "apexLock" ? false : null);
}),
// 지우기 — IP2 를 빼도 IP1 · IP3 넉넉
deleteOk: stop(base, (c) => {
for (const k of ["planned", "curveOn", "curveRadius", "curveLock", "curveArc", "apexLock"])
c[k].splice(2, 1);
}),
// 둘 넘게 다르면 안 견줌
manyGap: stop(base, (c) => {
c.planned.splice(1, 0, [50, 0], [52, 0]);
for (const k of ["curveOn", "curveRadius", "curveLock", "curveArc", "apexLock"])
c[k].splice(
1,
0,
...(k === "curveOn" ? [true, true] : k === "apexLock" ? [false, false] : [null, null]),
);
}),
};
// 이미 하한을 밑돌던 자리는 「새로」가 아님 — 밑돈 노선에서 다시 조금 옮겨도 통과
const broken = cols(ROUTE);
broken.planned[2] = [108, 4];
out.alreadyBroken = stop(broken, (c) => (c.planned[2] = [108.5, 4.2]));
// 겹친 노드 · 못 그린 곡선
const overlap = [
[0, 0],
[100, 0],
[100.3, 0.2],
[200, 60],
[300, 0],
];
out.flags = edits.routeFlags(overlap, Array(5).fill(true), []);
out.flagsOff = edits.routeFlags(ROUTE, [true, false, false, false, true], []);
out.summary = edits.curveSummary({
nodeCount: 5,
curveOn: Array(5).fill(true),
curveRadius: Array(5).fill(null),
curveLock: Array(5).fill(null),
apexLock: [],
curveCount: 2,
violationCount: 0,
minRadiusM: 30,
fresh: false,
flags: out.flags,
});
// fitNote — 손으로 옮긴 고른 IP 는 「밀림」에 안 셈(놓을 때 · 430) · 입력 뒤는 셈(408-1)
const after = ROUTE.map((v, i) => (i === 2 ? [v[0] + 3, v[1]] : i === 3 ? [v[0] + 1, v[1]] : v));
out.noteHand = nudge.fitNote(ROUTE, after, 2, [], 20, 20, true);
out.noteInput = nudge.fitNote(ROUTE, after, 2, [], 20, 20);
// 짝 — 서버 곡선표(파이썬이 씀)를 그대로 펴서 넣은 값이 돌아오는가
const server = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const flat = edits.flattenServerPlan(server.nodes, server.curves, server.edits);
out.flat = {
planned: flat.planned,
curveOn: flat.curveOn,
curveRadius: flat.curveRadius,
curveLock: flat.curveLock,
curveArc: flat.curveArc,
};
const old = edits.flattenServerPlan(server.nodes, server.curves, null);
out.flatOld = { curveRadius: old.curveRadius, curveLock: old.curveLock };
process.stdout.write(JSON.stringify(out));