fix(B05): 계획노선 편집 등고선 고르기 — 끊어진 같은 높이 줄을 함께 강조

- 고른 가닥과 높이가 같은 가닥을 모두 덧그림 · 높이를 모르면 고른 가닥만
- 검증 시나리오 ui_checks/48-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
2026-10-05 14:04:23 +09:00
co-authored by Claude Opus 5.5
parent 481001ca65
commit 863988f73d
2 changed files with 74 additions and 1 deletions
@@ -184,10 +184,17 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
context.lineWidth = 0.8;
drawPreparedLayer(context, scene.contours.layer, view, "dot", everyM);
// 고른 가닥은 굵고 다른 색으로 덧그린다 — 지우고 다시 그리지 않고 위에 얹는다.
// 끊어져 여러 가닥으로 나뉜 **같은 높이 줄은 함께** 덧그린다(높이를 모르면 고른 가닥만).
if (scene.pickedContour >= 0) {
const { layer } = scene.contours;
const level = layer.features[scene.pickedContour]?.labelValue ?? null;
context.strokeStyle = style.getPropertyValue("--map-flow-arrow") || "#7c3aed";
context.lineWidth = 2.6;
drawPreparedFeature(context, scene.contours.layer, scene.pickedContour, view);
layer.features.forEach((feature, index) => {
if (index === scene.pickedContour || (level !== null && feature.labelValue === level)) {
drawPreparedFeature(context, layer, index, view);
}
});
}
// 높이값 라벨은 여기서 안 낸다 — 노선·눈금 **뒤에** 그려야 안 묻힌다(맨 아래 참고).
}
@@ -0,0 +1,66 @@
/* 48-3 · B05 계획노선 편집 등고선 고르기 — 끊어진 같은 높이 줄을 함께 강조
* 시작 화면: B05 종단설계 · [계획노선 편집] 창이 떠 있음(저장 안 함 · 재기 모드 꺼짐)
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
* 하는 일: 격자(30px)로 눌러 「고른 등고선 Nm」 가 뜨는 자리를 높이마다 하나씩 모으고, 그 높이를 골랐을 때
* 강조색(#7c3aed) 픽셀 수를 셈 → 같은 높이의 **다른 가닥 자리**를 눌러도 같은 수가 나오는지 봄
* 기대 수치: 같은 높이를 다른 가닥에서 골라도 강조 픽셀 수 차 ≤ 12% — 남는 차는 높이 글자 상자(같은 색)가
* 고른 가닥 자리에 붙어 선과 겹치는 정도가 달라서 · 고치기 전 코드는 최대 57%(2026-10-05 데스크탑_보조)
* · 끝나면 빈 자리를 눌러 고름을 풂
* 고치기 전 코드와 견줄 때: 같은 높이 두 자리의 강조 픽셀 수가 크게 다름(가닥별로만 칠함)
*/
(async () => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const o = document.querySelector(".b05-routeedit");
const c = o.querySelector("canvas");
const status = o.querySelector(".b05-routeedit__status");
const r = c.getBoundingClientRect();
const fire = (type, x, y) =>
c.dispatchEvent(
new PointerEvent(type, {
button: 0,
clientX: x,
clientY: y,
pointerId: 1,
pointerType: "mouse",
bubbles: true,
}),
);
const click = (x, y) => {
fire("pointerdown", x, y);
fire("pointerup", x, y);
};
const purple = () => {
const d = c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
let n = 0;
for (let i = 0; i < d.length; i += 4)
if (d[i] === 124 && d[i + 1] === 58 && d[i + 2] === 237) n++;
return n;
};
const byLevel = {};
for (let y = r.top + 30; y < r.bottom - 30; y += 30)
for (let x = r.left + 30; x < r.right - 30; x += 30) {
click(x, y);
const m = status.textContent.match(/고른 등고선 (-?[\d.]+)m/);
if (!m) continue;
await wait(30);
const list = (byLevel[m[1]] ??= []);
if (list.length < 4) list.push({ x, y, purple: purple() });
}
// 빈 자리(왼쪽 위 모서리)를 눌러 고름을 푼다.
click(r.left + 2, r.top + 2);
const levels = Object.entries(byLevel).filter(([, list]) => list.length >= 2);
const spread = levels.map(([level, list]) => {
const counts = list.map((entry) => entry.purple);
const max = Math.max(...counts);
const min = Math.min(...counts);
return { level, counts, ratio: max ? (max - min) / max : 0 };
});
const worst = spread.reduce((a, b) => (b.ratio > a ? b.ratio : a), 0);
return JSON.stringify({
levelsFound: Object.keys(byLevel).length,
compared: spread.length,
worst: Math.round(worst * 1000) / 1000,
sample: spread.slice(0, 6),
pass: spread.length > 0 && worst <= 0.12,
});
})();