/* 215-1 종단 [자유지정] 단위검증 헬퍼 — `design_profile_free.ts` 를 그 자리에서 트랜스파일해 * 범위 · 시작 이음 · 세로선 붙음 · 높이 변환 · 미리 보기 · 기울기 판정 · 종단곡선 · 끝 이음 · * Esc · Backspace · 적용 편집을 Node 로 돌린다. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); const { loadTs } = makeLoader({ root: ROOT, }); 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));