Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -184,7 +184,6 @@ def _overview(project_root: Path, root: Path) -> dict[str, Any]:
|
||||
for item in saved:
|
||||
current = run.fingerprint(finger_source, ground, item.pop("settings"))
|
||||
item["stale"] = item.pop("fingerprint") != current
|
||||
points = source.db_snapshot(root).get("route_points") or []
|
||||
return {
|
||||
"sections": run.auto_sections(stations),
|
||||
"stations": [
|
||||
@@ -196,9 +195,10 @@ def _overview(project_root: Path, root: Path) -> dict[str, Any]:
|
||||
}
|
||||
for s in stations
|
||||
],
|
||||
# 중심선 = 측점 중심(B06 누가 그대로 · route_points 는 접선 꺾은선이라 누가가 다름)
|
||||
"line": [
|
||||
[float(p["model_x"]), float(p["model_y"]), float(p["chainage_m"])]
|
||||
for p in sorted(points, key=lambda p: p["sequence_num"])
|
||||
[float(s["center_x"]), float(s["center_y"]), float(s["chainage_m"])]
|
||||
for s in sorted(stations, key=lambda s: float(s["chainage_m"]))
|
||||
],
|
||||
"saved": saved,
|
||||
"defaults": SearchSettings().model_dump(),
|
||||
|
||||
@@ -33,7 +33,7 @@ from E03_StabSection.E03_StabSection_Types import DEFAULT_ROAD_SURCHARGE_KPA
|
||||
from E04_Stab3D import E04_Stab3D_Engine_Grid as grid_engine
|
||||
from E04_Stab3D import E04_Stab3D_Engine_Search as search_engine
|
||||
|
||||
ENGINE_VERSION = "e04-1" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈
|
||||
ENGINE_VERSION = "e04-3" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈
|
||||
STAGE_DIR = "E04_Stab3D"
|
||||
GROUND_FILE = Path("E02_StabGround") / "ground.json"
|
||||
SECTION_LENGTH_M = 100.0 # 자동 구간 길이
|
||||
@@ -127,13 +127,19 @@ def _engine_station(root: Path, station: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _node_mask(
|
||||
root: Path, grid: dict[str, Any], start_m: float, end_m: float, half_width_m: float
|
||||
stations: list[dict[str, Any]],
|
||||
grid: dict[str, Any],
|
||||
start_m: float,
|
||||
end_m: float,
|
||||
half_width_m: float,
|
||||
) -> np.ndarray:
|
||||
"""탐색 노드 자리 = 중심선에서 half_width 안 · 가장 가까운 중심선 점의 누가가 구간 안."""
|
||||
points = sorted(
|
||||
source.db_snapshot(root).get("route_points") or [], key=lambda p: p["sequence_num"]
|
||||
"""탐색 노드 자리 = 중심선에서 half_width 안 · 가장 가까운 중심선 점의 누가가 구간 안
|
||||
· 가장 가까운 점이 중심선 양 끝점인 셀(시점 앞 · 종점 뒤)은 뺌.
|
||||
|
||||
중심선 = 측점 중심을 이은 선(B06 누가 그대로 · route_points 는 접선 꺾은선이라 누가가 다름)."""
|
||||
line = np.array(
|
||||
[[float(s["center_x"]), float(s["center_y"]), float(s["chainage_m"])] for s in stations]
|
||||
)
|
||||
line = np.array([[p["model_x"], p["model_y"], p["chainage_m"]] for p in points], float)
|
||||
# 0.5 m 로 촘촘히 — 꺾인 점 사이 셀도 바른 누가
|
||||
dense = [line[:1]]
|
||||
for a, b in zip(line[:-1], line[1:]):
|
||||
@@ -145,6 +151,7 @@ def _node_mask(
|
||||
dist, at = cKDTree(line[:, :2]).query(np.column_stack([xx.ravel(), yy.ravel()]))
|
||||
chain = line[at, 2]
|
||||
keep = (dist <= half_width_m) & (chain >= start_m - 1e-6) & (chain <= end_m + 1e-6)
|
||||
keep &= (at > 0) & (at < len(line) - 1)
|
||||
return keep.reshape(xx.shape)
|
||||
|
||||
|
||||
@@ -197,7 +204,7 @@ def run_engine(
|
||||
strata = grid_engine.strata_from_grid(
|
||||
grid, (ground or {}).get("materials"), water_depth_m=0.0 if wet else None
|
||||
)
|
||||
mask = _node_mask(root, grid, settings["start_m"], settings["end_m"], half)
|
||||
mask = _node_mask(stations, grid, settings["start_m"], settings["end_m"], half)
|
||||
ratios = (1.0,) if settings["shape"] == "sphere" else (1.0, float(settings["ellipsoid_ratio"]))
|
||||
params = search_engine.SearchParams(
|
||||
node_mask=mask,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { cutCells } from "./logic/E04_Stab3D_Mesh";
|
||||
import { cutCells, footprintCenter } from "./logic/E04_Stab3D_Mesh";
|
||||
import {
|
||||
FS_STEPS,
|
||||
VOLUME_STEPS,
|
||||
@@ -40,6 +40,8 @@ interface View {
|
||||
scale: number; // px / m
|
||||
}
|
||||
|
||||
const RANGE_COLOR = "#2563eb"; // 고른 구간 — 3D 노선 관과 같은 색 · 안전율 색 단계와 겹치지 않음
|
||||
|
||||
const shortLabel = (label: string): string => label.replace(/^STA\./, "").replace(/\.0+$/, "");
|
||||
|
||||
const css = (name: string, fallback: string): string =>
|
||||
@@ -61,6 +63,7 @@ export function createPlanMap(): PlanMap {
|
||||
let layer: MapLayer = "fs";
|
||||
let image: HTMLCanvasElement | null = null;
|
||||
let outline: Set<number> = new Set();
|
||||
let marker: { x: number; y: number } | null = null;
|
||||
let view: View = { cx: 0, cy: 0, scale: 1 };
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
@@ -118,7 +121,7 @@ export function createPlanMap(): PlanMap {
|
||||
if (line.length < 2) return;
|
||||
ctx.lineJoin = ctx.lineCap = "round";
|
||||
ctx.strokeStyle = text;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
line.forEach((p, i) =>
|
||||
i ? ctx.lineTo(...toScreen(p[0], p[1])) : ctx.moveTo(...toScreen(p[0], p[1])),
|
||||
@@ -184,9 +187,9 @@ export function createPlanMap(): PlanMap {
|
||||
}
|
||||
|
||||
function drawMarker(text: string): void {
|
||||
if (!result) return;
|
||||
if (!result || !marker) return;
|
||||
const { best } = result;
|
||||
const [px, py] = toScreen(best.x, best.y);
|
||||
const [px, py] = toScreen(marker.x, marker.y);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
@@ -245,7 +248,7 @@ export function createPlanMap(): PlanMap {
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
const text = css("--color-text", "#1f2937");
|
||||
const accent = css("--color-primary", "#6d28d9");
|
||||
const accent = RANGE_COLOR;
|
||||
if (result && image) {
|
||||
const g = result.grid;
|
||||
const [left, top] = toScreen(g.x0 - g.dx / 2, g.y0 + (g.ny - 0.5) * g.dx);
|
||||
@@ -335,6 +338,8 @@ export function createPlanMap(): PlanMap {
|
||||
return { x: rect.left + px, y: rect.top + py };
|
||||
},
|
||||
result: () => result,
|
||||
marker: () => marker,
|
||||
outline: () => [...outline],
|
||||
layer: () => layer,
|
||||
};
|
||||
(window as unknown as { __e04Map?: typeof probe }).__e04Map = probe;
|
||||
@@ -354,7 +359,9 @@ export function createPlanMap(): PlanMap {
|
||||
},
|
||||
setResult(next) {
|
||||
result = next;
|
||||
outline = new Set(next ? cutCells(next.grid, next.z, next.best).keys() : []);
|
||||
const cut = next ? cutCells(next.grid, next.z, next.best) : new Map<number, number>();
|
||||
outline = new Set(cut.keys());
|
||||
marker = next ? footprintCenter(next.grid, cut, next.best) : null;
|
||||
buildImage();
|
||||
fit();
|
||||
draw();
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
.e04-map__empty {
|
||||
position: absolute;
|
||||
inset: auto 0 45% 0;
|
||||
inset: var(--spacing-16) 0 auto 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@@ -90,6 +90,22 @@ export function cutCells(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 임계 활동체 평면 중심 = 잘린 셀 중심의 평균(없으면 구 중심) — 지도 최소 표시 자리 */
|
||||
export function footprintCenter(
|
||||
grid: Grid,
|
||||
cut: ReadonlyMap<number, number>,
|
||||
best: SlipBest,
|
||||
): { x: number; y: number } {
|
||||
if (!cut.size) return { x: best.x, y: best.y };
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
for (const index of cut.keys()) {
|
||||
sx += grid.x0 + (index % grid.nx) * grid.dx;
|
||||
sy += grid.y0 + Math.floor(index / grid.nx) * grid.dx;
|
||||
}
|
||||
return { x: sx / cut.size, y: sy / cut.size };
|
||||
}
|
||||
|
||||
/** 잘린 셀의 8 이웃 중 잘리지 않은 셀(값 있음) — 활동면 테두리 */
|
||||
export function rimCells(
|
||||
grid: Grid,
|
||||
|
||||
@@ -83,16 +83,6 @@ def root(tmp_path, monkeypatch):
|
||||
z = _ground(yy)
|
||||
np.savez(models / "dtm_classification.npz", x=xs, y=ys, z=z, valid_mask=np.ones_like(z, bool))
|
||||
monkeypatch.setattr(run.source, "cross_section", lambda r, ch: _cross(float(ch)))
|
||||
monkeypatch.setattr(
|
||||
run.source,
|
||||
"db_snapshot",
|
||||
lambda r: {
|
||||
"route_points": [
|
||||
{"model_x": ch, "model_y": 0.0, "chainage_m": ch, "sequence_num": i}
|
||||
for i, ch in enumerate(CHAINAGES)
|
||||
]
|
||||
},
|
||||
)
|
||||
return tmp_path
|
||||
|
||||
|
||||
@@ -178,3 +168,17 @@ def test_hooks_do_not_change_search():
|
||||
with pytest.raises(InterruptedError):
|
||||
search(strata, SearchParams(**base, should_stop=lambda: True))
|
||||
assert math.isfinite(plain.best["governing"])
|
||||
|
||||
|
||||
def test_node_mask_section_and_route_ends(root):
|
||||
"""노드 자리 = 구간 누가 안 · 탐색 폭 안 · 노선 시점 앞(x < 0) · 종점 뒤(x > 100)는 뺌."""
|
||||
x = np.arange(-30.0, 131.0, 1.0)
|
||||
y = np.arange(-20.0, 21.0, 1.0)
|
||||
grid = {"x": x, "y": y}
|
||||
mask = run._node_mask(STATIONS, grid, 0.0, 100.0, 12.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
assert not mask[xx < -0.5].any() and not mask[xx > 100.5].any()
|
||||
assert not mask[np.abs(yy) > 12.0].any()
|
||||
assert mask[(np.abs(yy) <= 12.0) & (xx >= 1) & (xx <= 99)].all()
|
||||
part = run._node_mask(STATIONS, grid, 20.0, 60.0, 12.0)
|
||||
assert not part[(xx < 19.5) | (xx > 60.5)].any() and part[(yy == 0) & (xx == 40)].all()
|
||||
|
||||
@@ -60,6 +60,8 @@ writeFileSync(process.argv[3], JSON.stringify({
|
||||
sphere: mesh(M.slipMesh(c.grid, c.z, c.sphere)),
|
||||
ellipsoid: mesh(M.slipMesh(c.grid, c.z, c.ellipsoid)),
|
||||
cut: Array.from(M.cutCells(c.grid, c.z, c.sphere).keys()),
|
||||
center: M.footprintCenter(c.grid, M.cutCells(c.grid, c.z, c.sphere), c.sphere),
|
||||
centerEmpty: M.footprintCenter(c.grid, new Map(), c.sphere),
|
||||
floored: Object.fromEntries(M.cutCells(c.grid, c.z,
|
||||
{ ...c.sphere, floor: [[7 * 21 + 10, 53.0], [7 * 21 + 11, 60.0]] }).entries()),
|
||||
holed: mesh(M.terrainWithout(c.grid, c.z, M.cutCells(c.grid, c.z, c.sphere))),
|
||||
@@ -180,6 +182,15 @@ def test_slip_mesh_vertices(ts, name):
|
||||
assert all(z < Z[i] for i, z in cut.items())
|
||||
|
||||
|
||||
def test_footprint_center(ts):
|
||||
"""지도 최소 표시 = 잘린 셀 중심 평균 · 잘린 셀 없으면 구 중심."""
|
||||
cut = [i for i, _ in _py_cut(SPHERE)]
|
||||
x = sum(GRID["x0"] + i % GRID["nx"] for i in cut) / len(cut)
|
||||
y = sum(GRID["y0"] + i // GRID["nx"] for i in cut) / len(cut)
|
||||
assert ts["center"] == pytest.approx({"x": x, "y": y})
|
||||
assert ts["centerEmpty"] == {"x": SPHERE["x"], "y": SPHERE["y"]}
|
||||
|
||||
|
||||
def test_floor_truncates_slip(ts):
|
||||
"""못 뚫는 층 — 활동면이 층 윗면 아래로 안 내려감 · 층이 지표보다 높으면 잘린 셀 아님."""
|
||||
center = 7 * 21 + 10
|
||||
|
||||
@@ -57,21 +57,6 @@ def project(tmp_path, monkeypatch):
|
||||
z = 500.0 + 0.4 * (yy - 200.0) + 0.002 * (xx - 200.0) ** 2 # 노선 옆 비탈
|
||||
np.savez(models / "dtm_classification.npz", x=xs, y=ys, z=z, valid_mask=np.ones_like(z, bool))
|
||||
monkeypatch.setattr(run.source, "stations", lambda root: STATIONS)
|
||||
monkeypatch.setattr(
|
||||
run.source,
|
||||
"db_snapshot",
|
||||
lambda root: {
|
||||
"route_points": [
|
||||
{
|
||||
"model_x": s["center_x"],
|
||||
"model_y": 200.0,
|
||||
"chainage_m": s["chainage_m"],
|
||||
"sequence_num": i,
|
||||
}
|
||||
for i, s in enumerate(STATIONS)
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
async def fake_root(project_id):
|
||||
return tmp_path
|
||||
|
||||
Reference in New Issue
Block a user