feat(562-1): 기울기 고정 = 마우스에 가장 가까운 자리 · 짧은 첫 자리 대신
- 반직선 위 |기울기| 목표 자리 전부 모아 마우스 거리 가까운 자리 선택(없으면 없음) - 셈 짝 TS ↔ Py 같이 · 거울 시험에 여러 자리 경우 추가 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,8 +65,9 @@ def fixed_slope_point(
|
|||||||
height_at: HeightAt,
|
height_at: HeightAt,
|
||||||
max_m: float = 1000,
|
max_m: float = 1000,
|
||||||
) -> Vertex | None:
|
) -> Vertex | None:
|
||||||
"""기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 첫 자리(1 m 걸음 · 이분) ·
|
"""기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 자리 중 마우스까지 거리에 가장
|
||||||
`max_m` 안에 없거나 지형 밖에 닿으면 None."""
|
가까운 자리(1 m 걸음 · 이분 · 같으면 앞 자리) · `max_m` 안에 없으면 None · 지형 밖에 닿으면
|
||||||
|
거기까지의 자리만."""
|
||||||
d = segment_length(start, cursor)
|
d = segment_length(start, cursor)
|
||||||
z0 = height_at(start[0], start[1])
|
z0 = height_at(start[0], start[1])
|
||||||
if d < EPS or not _finite(z0):
|
if d < EPS or not _finite(z0):
|
||||||
@@ -82,33 +83,42 @@ def fixed_slope_point(
|
|||||||
p = at(t)
|
p = at(t)
|
||||||
return abs(height_at(p[0], p[1]) - z0) - grade * t
|
return abs(height_at(p[0], p[1]) - z0) - grade * t
|
||||||
|
|
||||||
|
roots: list[float] = []
|
||||||
lo = 0.0
|
lo = 0.0
|
||||||
flo = math.nan
|
flo = math.nan
|
||||||
i = 1
|
i = 1
|
||||||
while i * SLOPE_STEP_M <= max_m:
|
while i * SLOPE_STEP_M <= max_m:
|
||||||
t = i * SLOPE_STEP_M
|
t = i * SLOPE_STEP_M
|
||||||
|
i += 1
|
||||||
ft = f(t)
|
ft = f(t)
|
||||||
if not _finite(ft):
|
if not _finite(ft):
|
||||||
return None
|
break
|
||||||
if ft == 0:
|
if ft == 0:
|
||||||
return at(t)
|
roots.append(t)
|
||||||
if i > 1 and (flo < 0) != (ft < 0):
|
lo = t
|
||||||
hi = 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):
|
for _ in range(BISECT):
|
||||||
mid = (lo + hi) / 2
|
mid = (a + b) / 2
|
||||||
fm = f(mid)
|
fm = f(mid)
|
||||||
if not _finite(fm):
|
if not _finite(fm):
|
||||||
return None
|
ok = False
|
||||||
if (fm < 0) == (flo < 0):
|
break
|
||||||
lo = mid
|
if (fm < 0) == (fa < 0):
|
||||||
flo = fm
|
a = mid
|
||||||
|
fa = fm
|
||||||
else:
|
else:
|
||||||
hi = mid
|
b = mid
|
||||||
return at((lo + hi) / 2)
|
if ok:
|
||||||
|
roots.append((a + b) / 2)
|
||||||
lo = t
|
lo = t
|
||||||
flo = ft
|
flo = ft
|
||||||
i += 1
|
if not roots:
|
||||||
return None
|
return None
|
||||||
|
return at(min(roots, key=lambda r: abs(r - d)))
|
||||||
|
|
||||||
|
|
||||||
def compass_candidates(
|
def compass_candidates(
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ export function fixedLengthPoint(from: Vertex, cursor: Vertex, lengthM: number):
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 첫 자리(1 m 걸음 · 이분) ·
|
/** 기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 자리 중 마우스까지 거리에 가장
|
||||||
* `maxM` 안에 없거나 지형 밖에 닿으면 null. */
|
* 가까운 자리(1 m 걸음 · 이분 · 같으면 앞 자리) · `maxM` 안에 없으면 null · 지형 밖에 닿으면 거기까지의
|
||||||
|
* 자리만. */
|
||||||
export function fixedSlopePoint(
|
export function fixedSlopePoint(
|
||||||
from: Vertex,
|
from: Vertex,
|
||||||
cursor: Vertex,
|
cursor: Vertex,
|
||||||
@@ -64,30 +65,44 @@ export function fixedSlopePoint(
|
|||||||
const p = at(t);
|
const p = at(t);
|
||||||
return Math.abs(heightAt(p[0], p[1]) - z0) - grade * t;
|
return Math.abs(heightAt(p[0], p[1]) - z0) - grade * t;
|
||||||
};
|
};
|
||||||
|
const roots: number[] = [];
|
||||||
let lo = 0;
|
let lo = 0;
|
||||||
let flo = NaN;
|
let flo = NaN;
|
||||||
for (let i = 1; i * SLOPE_STEP_M <= maxM; i += 1) {
|
for (let i = 1; i * SLOPE_STEP_M <= maxM; i += 1) {
|
||||||
const t = i * SLOPE_STEP_M;
|
const t = i * SLOPE_STEP_M;
|
||||||
const ft = f(t);
|
const ft = f(t);
|
||||||
if (!Number.isFinite(ft)) return null;
|
if (!Number.isFinite(ft)) break;
|
||||||
if (ft === 0) return at(t);
|
if (ft === 0) {
|
||||||
if (i > 1 && flo < 0 !== ft < 0) {
|
roots.push(t);
|
||||||
let hi = 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) {
|
for (let k = 0; k < BISECT; k += 1) {
|
||||||
const mid = (lo + hi) / 2;
|
const mid = (a + b) / 2;
|
||||||
const fm = f(mid);
|
const fm = f(mid);
|
||||||
if (!Number.isFinite(fm)) return null;
|
if (!Number.isFinite(fm)) {
|
||||||
if (fm < 0 === flo < 0) {
|
ok = false;
|
||||||
lo = mid;
|
break;
|
||||||
flo = fm;
|
}
|
||||||
} else hi = mid;
|
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;
|
lo = t;
|
||||||
flo = ft;
|
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` 원 위에서 |기울기| = 목표인 후보들(방위 표본 · 이분 · 동쪽부터
|
/** 둘 다(양각기) — 반지름 `lengthM` 원 위에서 |기울기| = 목표인 후보들(방위 표본 · 이분 · 동쪽부터
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ def plane(x: float, y: float) -> float:
|
|||||||
return 0.1 * x
|
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
|
LINE = [[0.0, 0.0], [30.0, 40.0], [30.0, 100.0], [90.0, 180.0]] # 50 + 60 + 100 = 210 m
|
||||||
|
|
||||||
# (함수 이름, 인자) — 높이 자리는 "hill" · "plane" 글자로 넘김
|
# (함수 이름, 인자) — 높이 자리는 "hill" · "plane" 글자로 넘김
|
||||||
@@ -53,6 +57,9 @@ CASES = [
|
|||||||
("fixedSlopePoint", [[100, 30], [130, 70], 12, "hill"]),
|
("fixedSlopePoint", [[100, 30], [130, 70], 12, "hill"]),
|
||||||
("fixedSlopePoint", [[0, 0], [50, 0], 9, "hill", 100]),
|
("fixedSlopePoint", [[0, 0], [50, 0], 9, "hill", 100]),
|
||||||
("fixedSlopePoint", [[600, 0], [700, 0], 1, "hill"]),
|
("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", [[0, 0], 50, 5, "plane"]),
|
||||||
("compassCandidates", [[200, 10], 60, 15, "hill"]),
|
("compassCandidates", [[200, 10], 60, 15, "hill"]),
|
||||||
("compassCandidates", [[0, 0], 50, 50, "plane"]),
|
("compassCandidates", [[0, 0], 50, 50, "plane"]),
|
||||||
@@ -77,6 +84,7 @@ import * as draw from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw.js";
|
|||||||
const heights = {
|
const heights = {
|
||||||
hill: (x, y) => (x > 800 ? NaN : 0.0005 * x * x + 3 * Math.sin(y / 40)),
|
hill: (x, y) => (x > 800 ? NaN : 0.0005 * x * x + 3 * Math.sin(y / 40)),
|
||||||
plane: (x, y) => 0.1 * x,
|
plane: (x, y) => 0.1 * x,
|
||||||
|
bumps: (x, y) => 5 * Math.sin(x / 10),
|
||||||
};
|
};
|
||||||
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||||
const out = cases.map(([name, args]) =>
|
const out = cases.map(([name, args]) =>
|
||||||
@@ -85,7 +93,7 @@ const out = cases.map(([name, args]) =>
|
|||||||
writeFileSync(process.argv[3], JSON.stringify(out));
|
writeFileSync(process.argv[3], JSON.stringify(out));
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_HEIGHTS = {"hill": hill, "plane": plane}
|
_HEIGHTS = {"hill": hill, "plane": plane, "bumps": bumps}
|
||||||
_PY = {
|
_PY = {
|
||||||
"segmentLength": draw.segment_length,
|
"segmentLength": draw.segment_length,
|
||||||
"groundSlopePct": draw.ground_slope_pct,
|
"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
|
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:
|
def test_line_values() -> None:
|
||||||
assert draw.line_length(LINE) == 210
|
assert draw.line_length(LINE) == 210
|
||||||
assert draw.point_at_distance(LINE, -5) == (0, 0)
|
assert draw.point_at_distance(LINE, -5) == (0, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user