Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1

This commit is contained in:
2026-10-27 20:54:34 +09:00
3 changed files with 86 additions and 30 deletions
+25 -15
View File
@@ -65,8 +65,9 @@ def fixed_slope_point(
height_at: HeightAt,
max_m: float = 1000,
) -> Vertex | None:
"""기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 첫 자리(1 m 걸음 · 이분) ·
`max_m` 안에 없거나 지형 밖에 닿으면 None."""
"""기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 자리 중 마우스까지 거리에 가장
가까운 자리(1 m 걸음 · 이분 · 같으면 앞 자리) · `max_m` 안에 없으면 None · 지형 밖에 닿으면
거기까지의 자리만."""
d = segment_length(start, cursor)
z0 = height_at(start[0], start[1])
if d < EPS or not _finite(z0):
@@ -82,33 +83,42 @@ def fixed_slope_point(
p = at(t)
return abs(height_at(p[0], p[1]) - z0) - grade * t
roots: list[float] = []
lo = 0.0
flo = math.nan
i = 1
while i * SLOPE_STEP_M <= max_m:
t = i * SLOPE_STEP_M
i += 1
ft = f(t)
if not _finite(ft):
return None
break
if ft == 0:
return at(t)
if i > 1 and (flo < 0) != (ft < 0):
hi = t
roots.append(t)
lo = t
flo = math.nan
continue
if not math.isnan(flo) and (flo < 0) != (ft < 0):
a, b, fa = lo, t, flo
ok = True
for _ in range(BISECT):
mid = (lo + hi) / 2
mid = (a + b) / 2
fm = f(mid)
if not _finite(fm):
return None
if (fm < 0) == (flo < 0):
lo = mid
flo = fm
ok = False
break
if (fm < 0) == (fa < 0):
a = mid
fa = fm
else:
hi = mid
return at((lo + hi) / 2)
b = mid
if ok:
roots.append((a + b) / 2)
lo = t
flo = ft
i += 1
return None
if not roots:
return None
return at(min(roots, key=lambda r: abs(r - d)))
def compass_candidates(
+29 -14
View File
@@ -44,8 +44,9 @@ export function fixedLengthPoint(from: Vertex, cursor: Vertex, lengthM: number):
];
}
/** 기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 첫 자리(1 m 걸음 · 이분) ·
* `maxM` 안에 없거나 지형 밖에 닿으면 null. */
/** 기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 자리 중 마우스까지 거리에 가장
* 가까운 자리(1 m 걸음 · 이분 · 같으면 앞 자리) · `maxM` 안에 없으면 null · 지형 밖에 닿으면 거기까지의
* 자리만. */
export function fixedSlopePoint(
from: Vertex,
cursor: Vertex,
@@ -64,30 +65,44 @@ export function fixedSlopePoint(
const p = at(t);
return Math.abs(heightAt(p[0], p[1]) - z0) - grade * t;
};
const roots: number[] = [];
let lo = 0;
let flo = NaN;
for (let i = 1; i * SLOPE_STEP_M <= maxM; i += 1) {
const t = i * SLOPE_STEP_M;
const ft = f(t);
if (!Number.isFinite(ft)) return null;
if (ft === 0) return at(t);
if (i > 1 && flo < 0 !== ft < 0) {
let hi = t;
if (!Number.isFinite(ft)) break;
if (ft === 0) {
roots.push(t);
lo = t;
flo = NaN;
continue;
}
if (!Number.isNaN(flo) && flo < 0 !== ft < 0) {
let a = lo;
let b = t;
let fa = flo;
let ok = true;
for (let k = 0; k < BISECT; k += 1) {
const mid = (lo + hi) / 2;
const mid = (a + b) / 2;
const fm = f(mid);
if (!Number.isFinite(fm)) return null;
if (fm < 0 === flo < 0) {
lo = mid;
flo = fm;
} else hi = mid;
if (!Number.isFinite(fm)) {
ok = false;
break;
}
if (fm < 0 === fa < 0) {
a = mid;
fa = fm;
} else b = mid;
}
return at((lo + hi) / 2);
if (ok) roots.push((a + b) / 2);
}
lo = t;
flo = ft;
}
return null;
let best: number | null = null;
for (const t of roots) if (best === null || Math.abs(t - d) < Math.abs(best - d)) best = t;
return best === null ? null : at(best);
}
/** 둘 다(양각기) — 반지름 `lengthM` 원 위에서 |기울기| = 목표인 후보들(방위 표본 · 이분 · 동쪽부터
+32 -1
View File
@@ -37,6 +37,10 @@ def plane(x: float, y: float) -> float:
return 0.1 * x
def bumps(x: float, y: float) -> float:
return 5 * math.sin(x / 10)
LINE = [[0.0, 0.0], [30.0, 40.0], [30.0, 100.0], [90.0, 180.0]] # 50 + 60 + 100 = 210 m
# (함수 이름, 인자) — 높이 자리는 "hill" · "plane" 글자로 넘김
@@ -53,6 +57,9 @@ CASES = [
("fixedSlopePoint", [[100, 30], [130, 70], 12, "hill"]),
("fixedSlopePoint", [[0, 0], [50, 0], 9, "hill", 100]),
("fixedSlopePoint", [[600, 0], [700, 0], 1, "hill"]),
("fixedSlopePoint", [[0, 0], [30, 0], 9, "bumps"]),
("fixedSlopePoint", [[0, 0], [60, 0], 9, "bumps"]),
("fixedSlopePoint", [[0, 0], [200, 0], 9, "bumps"]),
("compassCandidates", [[0, 0], 50, 5, "plane"]),
("compassCandidates", [[200, 10], 60, 15, "hill"]),
("compassCandidates", [[0, 0], 50, 50, "plane"]),
@@ -77,6 +84,7 @@ import * as draw from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw.js";
const heights = {
hill: (x, y) => (x > 800 ? NaN : 0.0005 * x * x + 3 * Math.sin(y / 40)),
plane: (x, y) => 0.1 * x,
bumps: (x, y) => 5 * Math.sin(x / 10),
};
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
const out = cases.map(([name, args]) =>
@@ -85,7 +93,7 @@ const out = cases.map(([name, args]) =>
writeFileSync(process.argv[3], JSON.stringify(out));
"""
_HEIGHTS = {"hill": hill, "plane": plane}
_HEIGHTS = {"hill": hill, "plane": plane, "bumps": bumps}
_PY = {
"segmentLength": draw.segment_length,
"groundSlopePct": draw.ground_slope_pct,
@@ -187,6 +195,29 @@ def test_hand_values() -> None:
assert draw.compass_point((0, 0), (10, 40), 50, 50, plane) is None
def test_fixed_slope_nearest_to_cursor() -> None:
# 5·sin(x/10) 에서 9 % 자리가 여럿 — 마우스 가까운 자리(짧은 첫 자리 아님)
def roots() -> list[float]:
out, prev = [], None
for i in range(1, 100001):
t = i * 0.01
f = abs(bumps(t, 0)) - 0.09 * t
if prev is not None and (prev < 0) != (f < 0):
out.append(t)
prev = f
return out
rs = roots()
assert len(rs) >= 3
for cx in (30, 60, 200):
want = min(rs, key=lambda r: abs(r - cx))
got = draw.fixed_slope_point((0, 0), (cx, 0), 9, bumps)
assert got is not None and got[0] == pytest.approx(want, abs=0.02)
first = draw.fixed_slope_point((0, 0), (30, 0), 9, bumps)
far = draw.fixed_slope_point((0, 0), (200, 0), 9, bumps)
assert first is not None and far is not None and far[0] > first[0] + 20
def test_line_values() -> None:
assert draw.line_length(LINE) == 210
assert draw.point_at_distance(LINE, -5) == (0, 0)