Files
Aislo/resources/tester/helper_215_1_free_profile.cjs
T
eomsangdonandClaude Opus 5.5 84876b747b feat(B05): 367-3 자유지정 되돌리기 · 다시하기 — 자유지정 안 한 걸음씩 · 큰 이력과 따로 · 적용 막히면 열린 채
- design_profile_free past/future(상태 바뀐 클릭만 · 새 클릭이 future 비움) · undo/redo/canUndo/canRedo · reset 비움 · Backspace = undo
- 자유지정 동안 ↶ ↷ 단추 · Ctrl+Z · Ctrl+Shift+Z = 자유지정 쪽만(되돌릴 것 없어도 큰 이력 안 감)
- applyEdits 성공 여부 돌려줌 · 막히면(기울기 · 흙덮기) 자유지정 열린 채
- 시험 helper_215_1 ⑥ ⑦ · test_215_1 · 345_3 창 넓힘

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

245 lines
9.1 KiB
JavaScript

/* 215-1 종단 [자유지정] 단위검증 헬퍼 — `design_profile_free.ts` 를 그 자리에서 트랜스파일해
* 범위 · 시작 이음 · 세로선 붙음 · 높이 변환 · 미리 보기 · 기울기 판정 · 종단곡선 · 끝 이음 ·
* Esc · Backspace · 적용 편집을 Node 로 돌린다.
*/
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();
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 (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 free = loadTs(path.join(ROOT, "A00_Common", "design", "logic", "design_profile_free.ts"));
const align = loadTs(
path.join(ROOT, "B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"),
);
// 전체 측점 폴리라인(20m 간격 · 0~200m) — 계획고 = 지반고, 마디마다 기울기가 다르다.
const stations = Array.from({ length: 11 }, (_, i) => i * 20);
const ground = stations.map((s, i) => 100 + 0.3 * i + (i % 2 ? 0.4 : 0));
const base = {
policy: {
station_interval_m: 20,
curve_radius_ratio: 1,
curve_tangent_max_ratio: 0.5,
curve_skip_legal_exception: false,
default_curve_length_m: 20,
default_curve_radius_m: 300,
balance_tolerance_percent: 10,
edit_step_m: 0.1,
grade_violation_policy: "warn",
max_grade_pct: 14,
curve_skip_delta_pct: 0,
paved: true,
},
onlyExplicitCurves: true,
basePvi: stations.map((s, i) => ({ chainage_m: s, elevation_m: ground[i] })),
chainage: stations,
ground,
stations: stations.map((s) => ({ station_id: `S${s}`, chainage_m: s })),
};
function harness() {
let edits = align.emptyEdits();
const log = { applied: [], closed: 0, changed: 0 };
const draw = free.createFreeDraw({
base: () => base,
alignment: () => align.buildAlignment(base, edits),
edits: () => edits,
apply: (next) => {
log.applied.push(next);
edits = next;
},
close: () => (log.closed += 1),
changed: () => (log.changed += 1),
});
return { draw, log, edits: () => edits };
}
const out = {};
const z0 = (s) => ground[stations.indexOf(s)];
// ① 높이 변환 — 세로 창(기준 + 지금 변환) 왕복 · 변환 글 읽기.
const view = { center: 100, pxPerM: 10, zero: 150, scale: 1.2, offset: -20 };
out.yAt103_2 = free.chartYAtElevation(view, 103.2);
out.zBack = free.elevationAtChartY(view, out.yAt103_2);
out.parsed = free.parseWindowTransform("translate(0 -20) scale(1 1.2)");
out.parsedNone = free.parseWindowTransform(null);
// ② 세로선 붙음 — 가장 가까운 측점 · 범위 안만.
out.snap79 = free.snapStation(stations, 79);
out.snapOutside = free.snapStation(stations, 190, [40, 160]);
// ③ 범위 → 시작 이음 → 점 → 미리 보기 → 끝 이음.
{
const { draw, log, edits } = harness();
draw.click(41, 0, stations); // 범위 첫 세로선(40)
out.pendingEdges = draw.marks().edges;
draw.click(158, 0, stations); // 범위 끝(160)
out.rangePhase = draw.phase();
out.band = draw.marks().band;
draw.click(100, 105, stations); // 범위 가운데 — 시작 아님
out.stillArmed = draw.phase();
draw.click(43, 999, stations); // 시작 세로선 근처 — 그 측점 계획고에 이어 시작
out.startPhase = draw.phase();
out.startDot = draw.marks().dots[0];
draw.click(79, 103.456, stations); // 80 에 붙고 높이는 클릭 높이(0.01 단위)
out.point1 = draw.marks().dots[1];
draw.click(61, 90, stations); // 뒤로 가는 클릭 — 무시
out.dotsAfterBack = draw.marks().dots.length;
draw.hover({ chainageM: 121, elevationM: 101.2, stations });
out.preview = draw.marks().segments.map((s) => ({
to: s.to,
grade: Number(s.grade_pct.toFixed(4)),
over: s.over,
preview: s.preview,
}));
draw.hover({ chainageM: 121, elevationM: 80, stations }); // 기울기 넘음
const steep = draw.marks().segments.at(-1);
out.steep = { grade: Number(steep.grade_pct.toFixed(2)), over: steep.over };
draw.click(121, 101.2, stations);
draw.hover({ chainageM: 157, elevationM: 50, stations }); // 끝 세로선 → 그 계획고로 이음
out.finishPreview = draw.marks().segments.at(-1).to;
out.hint = draw.hint();
draw.click(157, 50, stations); // 끝 세로선 근처 = 끝
out.appliedCount = log.applied.length;
out.closed = log.closed;
out.afterPhase = draw.phase();
const next = edits();
out.offsetKeys = Object.keys(next.station_offsets).sort();
out.radiiKeys = Object.keys(next.curve_radii).sort();
const built = align.buildAlignment(base, next);
const pvi = (s) => built.pvi.find((n) => Math.abs(n.chainage_m - s) < 1e-6).elevation_m;
out.pvi = {
40: pvi(40),
60: pvi(60),
80: pvi(80),
100: pvi(100),
120: pvi(120),
140: pvi(140),
160: pvi(160),
};
out.ground = { 40: z0(40), 160: z0(160) };
const g1 = (103.46 - z0(40)) / 40;
const g2 = (101.2 - 103.46) / 40;
out.expectR80 = Number((20 / Math.abs(g2 - g1)).toFixed(6));
out.r80 = next.curve_radii["80.000"];
out.curve80L = built.curves.find((c) => Math.abs(c.chainage_m - 80) < 1e-6)?.l_m ?? null;
out.outsideUntouched =
!("20.000" in next.station_offsets) && !("180.000" in next.station_offsets);
}
// ④ Enter = 끝(마지막 점 → 끝 세로선) · 끝 세로선에서 거꾸로 시작.
{
const { draw, log, edits } = harness();
draw.click(40, 0, stations);
draw.click(160, 0, stations);
draw.click(162, 0, stations); // 끝 쪽 세로선에서 시작
out.reverseAnchor = draw.marks().dots[0].chainage_m;
draw.click(100, 104, stations);
out.enter = draw.key("Enter");
out.enterApplied = log.applied.length;
const built = align.buildAlignment(base, edits());
const pvi = (s) => built.pvi.find((n) => Math.abs(n.chainage_m - s) < 1e-6).elevation_m;
out.enterPvi = { 40: pvi(40), 60: pvi(60), 100: pvi(100), 140: pvi(140), 160: pvi(160) };
}
// ⑤ Esc = 취소(편집 없음) · Backspace = 마지막 점 빼기.
{
const { draw, log } = harness();
draw.click(40, 0, stations);
draw.click(160, 0, stations);
draw.click(40, 0, stations);
draw.click(80, 102, stations);
draw.click(120, 103, stations);
draw.key("Backspace");
out.afterBackspace = draw.marks().dots.map((d) => d.chainage_m);
draw.key("Backspace");
draw.key("Backspace");
out.backspaceToArmed = draw.phase();
draw.click(40, 0, stations);
draw.click(80, 102, stations);
out.escape = draw.key("Escape");
out.escApplied = log.applied.length;
out.escClosed = log.closed;
out.escPhase = draw.phase();
out.escDots = draw.marks().dots.length;
out.enterOutsideDrawing = draw.key("Enter");
}
// ⑥ 되돌리기 · 다시하기(367-3) — 점 둘 → ↶ → ↷ → 새 클릭이 ↷ 지움 · apply 안 불림 · 범위까지 거꾸로.
{
const { draw, log } = harness();
out.undoEmpty = { can: draw.canUndo(), backspace: draw.key("Backspace") };
draw.click(40, 0, stations);
draw.click(40, 0, stations); // 같은 세로선 다시 — 상태 그대로 · 안 쌓임
draw.click(160, 0, stations);
draw.click(40, 0, stations);
draw.click(80, 102, stations);
draw.click(120, 103, stations);
const dots = () => draw.marks().dots.map((d) => d.chainage_m);
draw.undo();
out.undo1 = { dots: dots(), canRedo: draw.canRedo() };
draw.redo();
out.redo1 = { dots: dots(), canRedo: draw.canRedo() };
draw.undo();
draw.click(100, 104, stations); // 새 클릭 = future 비움
out.newClick = { dots: dots(), canRedo: draw.canRedo() };
const phases = [];
while (draw.canUndo()) {
draw.undo();
phases.push(`${draw.phase()}:${draw.marks().edges.length}`);
}
out.undoAll = phases;
out.undoApplied = log.applied.length;
out.undoClosed = log.closed;
draw.redo();
draw.redo();
out.redoTwice = { phase: draw.phase(), band: draw.marks().band };
draw.reset();
out.resetClears = { undo: draw.canUndo(), redo: draw.canRedo() };
}
// ⑦ 적용이 막히면(apply = false) 자유지정 열린 채 · 점 그대로.
{
let tries = 0;
const draw = free.createFreeDraw({
base: () => base,
alignment: () => align.buildAlignment(base, align.emptyEdits()),
edits: () => align.emptyEdits(),
apply: () => ((tries += 1), false),
close: () => (tries += 100),
changed: () => undefined,
});
draw.click(40, 0, stations);
draw.click(160, 0, stations);
draw.click(40, 0, stations);
draw.click(80, 102, stations);
draw.key("Enter");
draw.click(160, 0, stations);
out.blocked = {
tries,
phase: draw.phase(),
dots: draw.marks().dots.length,
canUndo: draw.canUndo(),
};
}
process.stdout.write(JSON.stringify(out));