204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
/* 509-3 공용 선 그리기 화면 밖 구간 건너뛰기 헬퍼 — `B04_PreProcess_UI_MapRender.ts` 의
|
||
* `drawPreparedLayer` 를 가짜 캔버스로 돌려 그린 구간 · 호출 수를 JSON 으로 낸다.
|
||
* 판정 = 화면(+ 여백)에 걸치는 원래 구간이 모두 그려졌나 · 화면 밖 구간은 줄었나.
|
||
*/
|
||
const path = require("path");
|
||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||
|
||
const ROOT = path.join(__dirname, "..", "..");
|
||
const { load } = makeLoader({ root: ROOT, ui: () => ({ themeColor: () => "" }) });
|
||
const render = load("B04_PreProcess", "B04_PreProcess_UI_MapRender.ts");
|
||
|
||
const W = 800;
|
||
const H = 600;
|
||
const VIEW = {
|
||
width: W,
|
||
height: H,
|
||
scale: 1,
|
||
offsetX: 0,
|
||
offsetY: 0,
|
||
mapRect: { x: 0, y: 0, width: W, height: H },
|
||
};
|
||
|
||
/** 그린 선분을 모으는 가짜 캔버스. */
|
||
function fakeContext() {
|
||
const calls = { moveTo: 0, lineTo: 0, stroke: 0, closePath: 0 };
|
||
const segments = [];
|
||
let start = null;
|
||
let pen = null;
|
||
return {
|
||
calls,
|
||
segments,
|
||
beginPath() {
|
||
start = pen = null;
|
||
},
|
||
moveTo(x, y) {
|
||
calls.moveTo += 1;
|
||
start = pen = [x, y];
|
||
},
|
||
lineTo(x, y) {
|
||
calls.lineTo += 1;
|
||
segments.push([pen, [x, y]]);
|
||
pen = [x, y];
|
||
},
|
||
closePath() {
|
||
calls.closePath += 1;
|
||
segments.push([pen, start]);
|
||
pen = start;
|
||
},
|
||
stroke() {
|
||
calls.stroke += 1;
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 화면 px 점렬 → 한 피처 레이어(정규 좌표 = px / 화면 크기 · weights 없음 = 다 그림). */
|
||
function layerOf(points, closed = false) {
|
||
const coords = [];
|
||
points.forEach(([x, y]) => coords.push(x / W, y / H));
|
||
const xs = points.map((p) => p[0] / W);
|
||
const ys = points.map((p) => p[1] / H);
|
||
return {
|
||
features: [
|
||
{
|
||
kind: "line",
|
||
parts: [{ coords: Float64Array.from(coords), closed, weights: null }],
|
||
minX: Math.min(...xs),
|
||
maxX: Math.max(...xs),
|
||
minY: Math.min(...ys),
|
||
maxY: Math.max(...ys),
|
||
labelAnchorX: 0,
|
||
labelAnchorY: 0,
|
||
labelText: null,
|
||
labelValue: null,
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
/** 선분이 사각형(여백 32)에 걸치나 — Liang–Barsky. */
|
||
function touches([a, b], m = 32) {
|
||
let t0 = 0;
|
||
let t1 = 1;
|
||
const dx = b[0] - a[0];
|
||
const dy = b[1] - a[1];
|
||
const edges = [
|
||
[-dx, a[0] + m],
|
||
[dx, W + m - a[0]],
|
||
[-dy, a[1] + m],
|
||
[dy, H + m - a[1]],
|
||
];
|
||
for (const [p, q] of edges) {
|
||
if (p === 0) {
|
||
if (q < 0) return false;
|
||
continue;
|
||
}
|
||
const t = q / p;
|
||
if (p < 0) t0 = Math.max(t0, t);
|
||
else t1 = Math.min(t1, t);
|
||
if (t0 > t1) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const key = ([a, b]) => [a, b].map((p) => p.map((v) => v.toFixed(6)).join(",")).join(">");
|
||
|
||
function run(points, closed = false, pad = 0) {
|
||
const context = fakeContext();
|
||
render.drawPreparedLayer(context, layerOf(points, closed), VIEW, "dot", undefined, pad);
|
||
const drawn = new Set(context.segments.map(key));
|
||
const rev = new Set(context.segments.map(([a, b]) => key([b, a])));
|
||
const original = [];
|
||
for (let i = 1; i < points.length; i += 1) original.push([points[i - 1], points[i]]);
|
||
if (closed) original.push([points[points.length - 1], points[0]]);
|
||
const visible = original.filter((segment) => touches(segment));
|
||
const missing = visible.filter((s) => !drawn.has(key(s)) && !rev.has(key(s))).length;
|
||
return { calls: context.calls, drawn: context.segments.length, visible: visible.length, missing };
|
||
}
|
||
|
||
// 1) 다 화면 안 — 예전과 같음(moveTo 1 · lineTo n-1 · stroke 1).
|
||
const inside = run([
|
||
[100, 100],
|
||
[200, 150],
|
||
[300, 120],
|
||
[400, 300],
|
||
]);
|
||
|
||
// 2) 화면 밖 왼쪽으로 길게 돌다 돌아옴 — 밖 구간은 안 그림 · 보이는 구간 다 그림.
|
||
const wander = [[400, 300]];
|
||
for (let i = 0; i < 200; i += 1) wander.push([-100 - (i % 7) * 10, 300 + i]);
|
||
wander.push([400, 500]);
|
||
const leftLoop = run(wander);
|
||
|
||
// 3) 왼쪽 밖 → 아래 밖(모서리를 질러 감) — 쪽이 다르면 그대로 그림.
|
||
const corner = run([
|
||
[-50, 300],
|
||
[300, 700],
|
||
]);
|
||
|
||
// 4) 닫힌 고리 다 안 = closePath · 일부 밖 = 닫는 구간 따로.
|
||
const ringIn = run(
|
||
[
|
||
[100, 100],
|
||
[300, 100],
|
||
[300, 300],
|
||
[100, 300],
|
||
],
|
||
true,
|
||
);
|
||
const ringOut = run(
|
||
[
|
||
[100, 100],
|
||
[-200, 100],
|
||
[-200, 300],
|
||
[100, 300],
|
||
],
|
||
true,
|
||
);
|
||
|
||
// 5) 다 밖(한쪽) = 안 그림(stroke 0).
|
||
const allOut = run([
|
||
[-200, 10],
|
||
[-300, 200],
|
||
[-250, 500],
|
||
]);
|
||
|
||
// 6) 넓힘(pad) = 무한이면 하나도 안 거름(고른 등고선 덧그리기).
|
||
const padded = run(wander, false, Infinity);
|
||
|
||
// 7) 무작위 선 500 가닥 — 보이는 구간 빠짐 0.
|
||
let seed = 7;
|
||
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647) * 2 - 0.5;
|
||
let randomMissing = 0;
|
||
let randomDrawn = 0;
|
||
let randomTotal = 0;
|
||
for (let n = 0; n < 500; n += 1) {
|
||
const points = [];
|
||
let x = rand() * W * 2;
|
||
let y = rand() * H * 2;
|
||
for (let i = 0; i < 40; i += 1) {
|
||
x += (rand() - 0.25) * 200;
|
||
y += (rand() - 0.25) * 200;
|
||
points.push([x, y]);
|
||
}
|
||
const result = run(points, n % 5 === 0);
|
||
randomMissing += result.missing;
|
||
randomDrawn += result.drawn;
|
||
randomTotal += points.length - (n % 5 === 0 ? 0 : 1);
|
||
}
|
||
|
||
console.log(
|
||
JSON.stringify({
|
||
inside,
|
||
leftLoop,
|
||
corner,
|
||
ringIn,
|
||
ringOut,
|
||
allOut,
|
||
padded,
|
||
randomMissing,
|
||
randomDrawn,
|
||
randomTotal,
|
||
}),
|
||
);
|