Files
Aislo/resources/tester/helper_398_2_curve_input.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

198 lines
6.5 KiB
JavaScript

/* 398-2 꺾임점 창 반지름 · 곡선 길이 넣기(요구 249) — `_CurveBar` 배선을 Node 로 돌려 JSON 한 줄
* (`test_398_2_curve_input.py` 가 판정). 창(`_Label`)은 손잡이만 잡는 가짜 · 편집 창 `applyEdit` 는
* 같은 길(스냅 켬 = `fitCurveStations` → 길이 고정 R · 하한 → `buildEditedPolyline`)로 흉내.
* node helper_398_2_curve_input.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 B05 = path.join(ROOT, "B05_Profile");
const cache = new Map();
let handlers = null;
const shows = [];
const STUBS = {
"./B05_Profile_UI_RouteEdit_Label": {
createCurveLabel: (given) => {
handlers = given;
return { show: (state) => shows.push(state), hide: () => {}, destroy: () => {} };
},
centerDirectionOf: () => null,
},
"@ui/ui_template_elements": { showToast: () => {} }, // 곡선 삭제 경고(430)
};
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) =>
STUBS[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 curve = loadTs(path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Curve.ts"));
const edits = loadTs(path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Edits.ts"));
const fit = loadTs(path.join(B05, "logic", "B05_Profile_RouteEdit_StationFit.ts"));
const bar = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_CurveBar.ts"));
// IP1 = 앞뒤 직선 길어 R 60 들어감 · IP2 = 뒤 직선 16 m 라 접선 8 m 까지(큰 R 눌림)
const ROUTE = [
[0, 0],
[120, 0],
[220, 70],
[236, 70],
[340, 0],
];
const LIMITS = { minRadiusM: 12, limitRadiusM: 12, limitArcM: 5, intervalM: 20 };
function editor(snap) {
const n = ROUTE.length;
const state = {
picked: -1,
planned: ROUTE.map((v) => [v[0], v[1]]),
nodeInfo: [],
curveInfo: [],
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),
limitRadiusM: LIMITS.limitRadiusM,
limitArcM: LIMITS.limitArcM,
line: [],
snap: snap ? { stepM: LIMITS.intervalM, stationM: LIMITS.intervalM } : null,
};
const status = { textContent: "" };
const markEdited = () => {
edits.applyArcLocks(state.planned, state.curveLock, state.curveArc, state.curveRadius);
edits.applyCurveLimits(
state.planned,
state.curveOn,
state.curveRadius,
LIMITS.limitRadiusM,
LIMITS.limitArcM,
);
const built = curve.buildEditedPolyline(
state.planned,
state.curveOn,
state.curveRadius,
LIMITS.minRadiusM,
);
state.line = built.vertices;
state.curveInfo = built.curves;
state.nodeInfo = built.nodes;
};
let made = null;
const applyEdit = (message) => {
if (snap) state.planned = fit.fitCurveStations(state, LIMITS, 1);
markEdited();
made.sync();
status.textContent = `예상노선 — ${message} 꼬리`;
};
made = bar.createCurveBar({
canvas: { getBoundingClientRect: () => ({ left: 0, top: 0, right: 800, bottom: 600 }) },
container: { querySelector: (sel) => (sel === ".b05-routeedit__status" ? status : null) },
state: () => state,
toScreen: (v) => [v[0], v[1]],
applyEdit,
onUnselect: () => {},
});
markEdited();
const drawn = (k) => state.curveInfo.find((c) => c.node_first === k) ?? null;
const arcOf = (k) => {
const c = drawn(k);
return c ? c.radius_m * edits.deflectionRad(c.inner_angle_deg) : null;
};
const mc = (k) => {
const c = drawn(k);
if (!c) return null;
const [[a, b]] = loadTs(
path.join(B05, "logic", "B05_Profile_UI_RouteEdit_Frame.ts"),
).curveSpans(state.line, [c]);
return (a + b) / 2;
};
const pick = (k) => {
state.picked = k;
made.sync();
};
const lastShow = () => shows[shows.length - 1];
return { state, status, handlers: () => handlers, drawn, arcOf, mc, pick, lastShow, markEdited };
}
const round = (v) => (v === null ? null : Math.round(v * 1000) / 1000);
const out = {};
for (const snap of [false, true]) {
const key = snap ? "snapOn" : "snapOff";
const ed = editor(snap);
const h = ed.handlers();
const row = {};
// IP1 — 반지름 40 → 60 · 곡선 길이 18 → 25
ed.pick(1);
h.onRadius(40);
row.r40 = round(ed.drawn(1).radius_m);
const later = () => [2, 3].map((k) => round(ed.mc(k))); // 뒤쪽 곡선 MC(408-1)
row.r40Later = later();
h.onRadius(60);
row.r60 = round(ed.drawn(1).radius_m);
row.r60Shown = round(ed.lastShow().radiusShown);
row.r60Status = ed.status.textContent;
row.r60Mc = round(ed.mc(1));
row.r60Later = later();
h.onArcLength(18);
row.l18 = round(ed.arcOf(1));
h.onArcLength(25);
row.l25 = round(ed.arcOf(1));
row.l25Shown = round(ed.lastShow().arcLengthShown);
row.l25Status = ed.status.textContent;
// IP2 — 접선 자리 모자람(넣은 값 · 그린 값 · 까닭)
ed.pick(2);
h.onRadius(60);
row.capR = round(ed.drawn(2).radius_m);
row.capShown = round(ed.lastShow().radiusShown);
row.capStatus = ed.status.textContent;
h.onArcLength(40);
row.capArcStatus = ed.status.textContent;
h.onRadius(null);
row.autoStatus = ed.status.textContent;
out[key] = row;
}
// 「고정」 — 길이 고정 켬 = 교각이 바뀌어도 그 길이 · 끔 = 반지름이 남고 길이가 따라 바뀜
for (const lock of [true, false]) {
const ed = editor(false);
const h = ed.handlers();
ed.pick(1);
h.onArcLength(25);
if (lock) h.onLock("arc");
ed.state.planned[2] = [220, 50]; // IP1 교각이 바뀜
ed.markEdited();
out[lock ? "arcLocked" : "arcFree"] = {
arc: round(ed.arcOf(1)),
radius: round(ed.drawn(1).radius_m),
};
}
{
const ed = editor(false);
const h = ed.handlers();
ed.pick(1);
h.onRadius(60);
h.onLock("radius");
ed.state.planned[2] = [220, 50];
ed.markEdited();
out.radiusLocked = { radius: round(ed.drawn(1).radius_m), lock: ed.state.curveLock[1] };
}
process.stdout.write(JSON.stringify(out));