Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -122,6 +122,23 @@ def extract_contours_from_grid(
|
||||
return contours_geojson_list
|
||||
|
||||
|
||||
def contour_cache_is_stale(contour_path: Path, model_path: Path) -> bool:
|
||||
"""등고선 캐시가 없거나 그 면을 낳은 파일보다 옛것이면 True(754-1).
|
||||
|
||||
낳은 파일 = 모델 npz · 같은 필터 `dtm_{필터}.npz`(외곽 자르기) · implicit 굳힌 격자.
|
||||
"""
|
||||
stem = Path(model_path).stem.removesuffix("_smooth")
|
||||
folder = Path(model_path).parent
|
||||
sources = [Path(model_path), folder / f"dtm_{stem.split('_', 1)[-1]}.npz"]
|
||||
if stem.startswith("implicit_"):
|
||||
sources.append(folder / f"{stem}_grid.npz")
|
||||
try:
|
||||
built = Path(contour_path).stat().st_mtime_ns
|
||||
except OSError:
|
||||
return True
|
||||
return any(path.is_file() and path.stat().st_mtime_ns > built for path in sources)
|
||||
|
||||
|
||||
def _load_footprint_mask(
|
||||
model_npz_path: Path, x_coords: np.ndarray, y_coords: np.ndarray
|
||||
) -> np.ndarray | None:
|
||||
@@ -137,10 +154,10 @@ def _load_footprint_mask(
|
||||
if not dtm_path.exists():
|
||||
return None
|
||||
try:
|
||||
d = np.load(dtm_path)
|
||||
dtm_x = np.asarray(d["x"]).ravel()
|
||||
dtm_y = np.asarray(d["y"]).ravel()
|
||||
dtm_mask = np.asarray(d["valid_mask"], dtype=bool)
|
||||
with np.load(dtm_path) as d:
|
||||
dtm_x = np.asarray(d["x"]).ravel()
|
||||
dtm_y = np.asarray(d["y"]).ravel()
|
||||
dtm_mask = np.asarray(d["valid_mask"], dtype=bool)
|
||||
except Exception:
|
||||
return None
|
||||
if len(dtm_x) < 2 or len(dtm_y) < 2:
|
||||
@@ -360,7 +377,21 @@ def extract_contours(
|
||||
model_npz_path = Path(model_npz_path)
|
||||
if not model_npz_path.exists():
|
||||
raise FileNotFoundError(f"모델 파일이 존재하지 않습니다: {model_npz_path}")
|
||||
data = np.load(model_npz_path)
|
||||
# 실패해도 모델 파일을 닫음 — 열린 채 남으면 Windows 에서 다시 짓기(교체)가 막힘(754-1)
|
||||
with np.load(model_npz_path) as data:
|
||||
return _extract_loaded(
|
||||
data, model_npz_path, representation, interval, target_grid_m, scene_center
|
||||
)
|
||||
|
||||
|
||||
def _extract_loaded(
|
||||
data: Any,
|
||||
model_npz_path: Path,
|
||||
representation: str,
|
||||
interval: float,
|
||||
target_grid_m: float,
|
||||
scene_center: tuple[float, float, float] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
|
||||
if representation == "regular_grid":
|
||||
x_coords, y_coords, z_grid, valid_mask = (
|
||||
|
||||
@@ -16,6 +16,7 @@ import numpy as np
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
|
||||
CONTOUR_EXTRACTOR_VERSION,
|
||||
contour_cache_is_stale,
|
||||
extract_contours,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Engine_ModelBuild import BUILDERS
|
||||
@@ -191,11 +192,9 @@ def _build_all_terrain_models(
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = output_dir / "manifest.json"
|
||||
filters = tuple(key for key in config["source_filters"] if key in ground_masks)
|
||||
methods_list = [key for key in config["precompute"] if key in BUILDERS]
|
||||
if "dtm" in methods_list:
|
||||
methods_list.remove("dtm")
|
||||
methods_list.insert(0, "dtm")
|
||||
methods = tuple(methods_list)
|
||||
# dtm 은 늘 맨 앞 — 같은 필터 외곽 · implicit 굳힌 격자 · 등고선 자르기의 바탕이라
|
||||
# 어느 표현을 지어도 낡았으면(manifest 항목 없음 · 지문 다름) 같이 다시 지음(754-1).
|
||||
methods = ("dtm", *(key for key in config["precompute"] if key in BUILDERS and key != "dtm"))
|
||||
# 무거운 implicit 은 기본 필터(첫 필터)에만 — 분류 LAS 의 csf 까지 두 번 짓지 않음(요구 558).
|
||||
later_methods = tuple(method for method in methods if method != "implicit")
|
||||
signature = config_signature(config)
|
||||
@@ -244,12 +243,14 @@ def _build_all_terrain_models(
|
||||
if not force and _cache_is_valid(output_dir, stem, method, entry, config):
|
||||
interval = float(config.get("contour_interval_meters", 1.0))
|
||||
contour_path = output_dir / f"contour_{filter_key}_{method}_{interval}m.json"
|
||||
needs_contours = not contour_path.exists()
|
||||
needs_contours = contour_cache_is_stale(contour_path, output_dir / f"{stem}.npz")
|
||||
if method in config.get("smoothing_methods", ("dtm", "tin")):
|
||||
smooth_contour_path = (
|
||||
output_dir / f"contour_{filter_key}_{method}_smooth_{interval}m.json"
|
||||
)
|
||||
needs_contours = needs_contours or not smooth_contour_path.exists()
|
||||
needs_contours = needs_contours or contour_cache_is_stale(
|
||||
smooth_contour_path, output_dir / f"{stem}_smooth.npz"
|
||||
)
|
||||
if needs_contours:
|
||||
_cache_contours_safely(
|
||||
output_dir,
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi.responses import JSONResponse, Response
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
|
||||
CONTOUR_EXTRACTOR_VERSION,
|
||||
contour_cache_is_stale,
|
||||
extract_contours,
|
||||
)
|
||||
from common_util.common_util_atomic import atomic_write_bytes
|
||||
@@ -37,10 +38,10 @@ MODEL_REPRESENTATIONS = {
|
||||
|
||||
|
||||
def _is_contour_cache_current(contour_path: Path, model_path: Path) -> bool:
|
||||
"""캐시가 현재 추출기 버전이고 기반 모델 npz보다 최신인지 검사한다."""
|
||||
"""캐시가 현재 추출기 버전이고 모델 · 같은 필터 dtm · 굳힌 격자보다 최신인지 검사한다."""
|
||||
if contour_cache_is_stale(contour_path, model_path):
|
||||
return False
|
||||
try:
|
||||
if contour_path.stat().st_mtime < model_path.stat().st_mtime:
|
||||
return False
|
||||
with contour_path.open("rb") as cache_file:
|
||||
head = cache_file.read(256).decode("utf-8", errors="ignore")
|
||||
except OSError:
|
||||
|
||||
@@ -38,6 +38,7 @@ import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { baseHeads } from "./M02_MasterTemplete_Sangeun_BaseHead";
|
||||
import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side";
|
||||
import { baseUnitOf, buildUnitSwap, type SwapHow } from "./M02_MasterTemplete_Sangeun_Unit";
|
||||
import {
|
||||
dropBase,
|
||||
@@ -63,6 +64,8 @@ export interface SangeunDetailOptions {
|
||||
owner: string | null;
|
||||
readOnly: boolean;
|
||||
onChanged: (dirty: boolean) => void;
|
||||
/** 752-1 오른쪽 원문 패널 — 단가산출 화면만 켬(일위대가 줄 모달 안 상세는 끔) */
|
||||
source?: boolean;
|
||||
}
|
||||
|
||||
const LIB = { read: "/m02/logic-lib", calc: "/m02/logic-lib" };
|
||||
@@ -90,6 +93,9 @@ export function mountSangeunDetail(
|
||||
let fail = "";
|
||||
let extra: LogicInput[] = []; // 더한줄 로직 입력 중 바탕에 없는 것 = 옵션 칸(725-6 · 727-3)
|
||||
const inputsOf = new Map<string, LogicInput[]>();
|
||||
const infoOf = new Map<string, SourceTab>(); // 더한줄 로직 → 원문 탭(752-1)
|
||||
let extraKeys: string[] = [];
|
||||
const side = opt.source ? createSangeunSide() : null;
|
||||
let seq = 0;
|
||||
let alive = true;
|
||||
|
||||
@@ -141,19 +147,17 @@ export function mountSangeunDetail(
|
||||
touch(false);
|
||||
void loadBase(true);
|
||||
}
|
||||
const baseHint = createText(L("M02_SangeunBaseViews"));
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-sk",
|
||||
attrs: { "data-sangeun-detail": doc.키 ?? "" },
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [views.root, addButton, baseHint] }),
|
||||
editor,
|
||||
],
|
||||
}),
|
||||
);
|
||||
const main = el("div", {
|
||||
className: "m02-sk",
|
||||
attrs: { "data-sangeun-detail": doc.키 ?? "" },
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [views.root, addButton] }),
|
||||
editor,
|
||||
],
|
||||
});
|
||||
host.replaceChildren(side ? side.wrap(main) : main);
|
||||
|
||||
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
|
||||
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3) · 둘째 기초 지문(727-2)
|
||||
@@ -185,11 +189,15 @@ export function mountSangeunDetail(
|
||||
.filter((k) => !inputsOf.has(k))
|
||||
.map((k) =>
|
||||
fetchLogic(k).then(
|
||||
(r) => void inputsOf.set(k, r.logic.입력 ?? []),
|
||||
(r) => {
|
||||
inputsOf.set(k, r.logic.입력 ?? []);
|
||||
infoOf.set(k, tabOf(r.logic));
|
||||
},
|
||||
() => void inputsOf.set(k, []),
|
||||
),
|
||||
),
|
||||
);
|
||||
extraKeys = keys;
|
||||
const seen = new Set<string>((base?.logic.입력 ?? []).map((i) => i.이름));
|
||||
extra = keys
|
||||
.flatMap((k) => inputsOf.get(k) ?? [])
|
||||
@@ -306,7 +314,6 @@ export function mountSangeunDetail(
|
||||
if (!alive) return;
|
||||
drawHead();
|
||||
drawNotes();
|
||||
baseHint.hidden = !base;
|
||||
const rows = ans ? rowsOf(ans, bare, doc) : [];
|
||||
const names = new Map((bare ? rowsOf(bare, null, {}) : []).map((r) => [r.cid, r.이름]));
|
||||
const removed = [
|
||||
@@ -382,6 +389,10 @@ export function mountSangeunDetail(
|
||||
cover: { ho, est, open: lineOpener(hoCtx) }, // 748-2 텍스트 수식 변수 · 값 누름 = 같은 줄 모달
|
||||
});
|
||||
drawCalc();
|
||||
side?.set([
|
||||
...(base ? [tabOf(base.logic)] : []),
|
||||
...extraKeys.flatMap((k) => (k === baseKey() ? [] : infoOf.has(k) ? [infoOf.get(k)!] : [])),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 재료 고름(줄 모달 · 시험 계산 재료 칸) = SK 문서 `고름`(cid → MT) — 둘째 기초 세부 줄은 보기만 */
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_Side.ts
|
||||
* 단가산출(SK) 상세 오른쪽 원문 패널(PLAN 752-1 · 요구 588-3) — 일위대가 상세(47-4 · 85-4)와 같은 부품:
|
||||
* 오른쪽 서랍(`createSidePanels`) + 원문 보기(`createSourceView` · M02 로그인 길 `/m02/logic-lib/source`)
|
||||
* 기초마다 탭 — 바탕 로직 · 둘째 기초(더한줄 로직) 각각의 출처 · 기초가 하나면 탭 없음 · 없으면 안내
|
||||
* 처음 = 넓은 화면만 열림(좁으면 세로 띠로 접힘) · 열림 · 폭 기억 = `m02-sangeun-side`
|
||||
* 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `source` 를 안 켬).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
export interface SourceTab {
|
||||
label: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
/** 기초 로직 → 원문 탭(「원문번호 이름」 · 출처 글) */
|
||||
export const tabOf = (logic: {
|
||||
키: string;
|
||||
원문번호?: string;
|
||||
이름: string;
|
||||
출처?: string;
|
||||
}): SourceTab => ({
|
||||
label: `${logic.원문번호 || logic.키} ${logic.이름}`,
|
||||
src: logic.출처 ?? "",
|
||||
});
|
||||
|
||||
/** 이 폭 이상일 때만 처음부터 열림 — 상세 표 여섯 칸이 좁아지지 않게 */
|
||||
const OPEN_FROM = 1500;
|
||||
|
||||
export function createSangeunSide(): {
|
||||
wrap: (main: HTMLElement) => HTMLElement;
|
||||
set: (tabs: SourceTab[]) => void;
|
||||
} {
|
||||
const view = createSourceView({
|
||||
path: "/m02/logic-lib/source",
|
||||
pdf: "/m02/logic-lib/source-pdf",
|
||||
});
|
||||
const host = el("div", { attrs: { "data-sangeun-source": "" } });
|
||||
let tabs: SourceTab[] = [];
|
||||
let at = 0;
|
||||
const side = createSidePanels({
|
||||
prefKey: "m02-sangeun-side",
|
||||
follow: false, // 일위대가와 같음 — 상세 고정 높이 칸을 채움 · 본문과 따로 스크롤
|
||||
defaultOpen: window.innerWidth >= OPEN_FROM ? "source" : null,
|
||||
panels: [{ id: "source", title: tc("Labor_Source"), body: host }],
|
||||
onChange: () => paint(),
|
||||
});
|
||||
function paint(): void {
|
||||
if (side.current() !== "source") return; // 접힘 = 찾지 않음
|
||||
if (!tabs.length)
|
||||
return void host.replaceChildren(createText(L("M02_SangeunSourceNone"), "muted"));
|
||||
at = Math.min(at, tabs.length - 1);
|
||||
view.show(tabs[at].src);
|
||||
const bar =
|
||||
tabs.length > 1
|
||||
? [
|
||||
createSegment({
|
||||
options: tabs.map((x, i) => ({ value: String(i), label: x.label })),
|
||||
value: String(at),
|
||||
size: 28,
|
||||
ariaLabel: tc("Labor_Source"),
|
||||
onChange: (v) => ((at = Number(v)), paint()),
|
||||
}).root,
|
||||
]
|
||||
: [];
|
||||
host.replaceChildren(...bar, view.root);
|
||||
}
|
||||
return {
|
||||
wrap: (main) => el("div", { className: "ui-side-panels-row", children: [main, side.root] }),
|
||||
set: (next) => {
|
||||
if (JSON.stringify(next) === JSON.stringify(tabs)) return;
|
||||
tabs = next;
|
||||
paint();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -18,5 +18,5 @@ export function mountSangeunView(
|
||||
doc: unknown,
|
||||
opt: SangeunViewOptions,
|
||||
): { getDoc: () => unknown; destroy: () => void } {
|
||||
return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null });
|
||||
return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ def _text(path: Path) -> str:
|
||||
|
||||
def test_721_1_단추_이름과_자리():
|
||||
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "children: [views.root, addButton, baseHint]" in detail
|
||||
assert "children: [views.root, addButton]" in detail # 750-2 「시험 계산 = 바탕」 안내 걷음
|
||||
assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert "M02_SangeunPickBase" not in locale
|
||||
|
||||
@@ -30,3 +30,14 @@ def test_calc_view_answer_hook_skips_logic_lib():
|
||||
ts = _ts("M01_MasterData/M01_MasterData_UI_Logic_Calc.ts")
|
||||
assert "ctx.onText && !ctx.answer" in ts # 미리보기 안 부름
|
||||
assert "if (ctx.answer)" in ts and "runCalc" in ts # M01 옛 길 그대로
|
||||
|
||||
|
||||
def test_752_1_source_panel_only_on_sangeun_screen():
|
||||
"""752-1 원문 패널 — 단가산출 화면만 켬 · 일위대가 줄 모달 안 상세는 끔 · 기초마다 탭 · 좁은 폭 접힘."""
|
||||
side = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts")
|
||||
assert "/m02/logic-lib/source" in side and "createSidePanels" in side
|
||||
assert "OPEN_FROM" in side and "createSegment" in side
|
||||
assert "source: true" in _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts")
|
||||
assert "source" not in _ts("M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts")
|
||||
detail = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "opt.source ? createSangeunSide()" in detail and "side?.set(" in detail
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""754-1 등고선 · 격자 외곽 = 지금 지표면 외곽 (요구 589-11).
|
||||
|
||||
implicit 하나만 지어도 같은 필터 dtm 이 낡았으면 같이 다시 지음 · 등고선 캐시는 dtm · 굳힌 격자가
|
||||
바뀌면 다시 셈 · ptd 처럼 dtm 없던 필터도 굳힌 격자가 생김. 서버 함수만 — 합성 점군 · 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import contour_cache_is_stale
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Pipeline import build_all_terrain_models
|
||||
from common_util.common_util_surface_sampler import implicit_grid_npz
|
||||
from config import config_system as cfg
|
||||
|
||||
|
||||
def _banded_plane() -> tuple[dict, np.ndarray]:
|
||||
axis = np.arange(0.0, 60.0, 0.5)
|
||||
x, y = np.meshgrid(axis, axis)
|
||||
points = np.column_stack([x.ravel(), y.ravel(), (100.0 + 0.2 * x).ravel()])
|
||||
points = points[~((points[:, 0] > 28) & (points[:, 0] < 33))] # 5 m 빈 띠(z 106 = x 30)
|
||||
data = {"xyz": points, "bounds": np.array([points.min(axis=0), points.max(axis=0)]).T}
|
||||
return data, np.ones(len(points), bool)
|
||||
|
||||
|
||||
def _config(filter_key: str, precompute: list[str], close_m: float) -> dict:
|
||||
config = cfg.build_surface_model_config()
|
||||
config.update(
|
||||
{
|
||||
"source_filters": [filter_key],
|
||||
"precompute": precompute,
|
||||
"footprint_gap_close_meters": close_m,
|
||||
"implicit_min_points": 500,
|
||||
"implicit_max_points": 2_000,
|
||||
}
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _valid_area(path: Path) -> float:
|
||||
with np.load(path) as grid:
|
||||
resolution = float(np.asarray(grid["resolution"]).ravel()[0])
|
||||
return float(np.count_nonzero(grid["valid_mask"])) * resolution**2
|
||||
|
||||
|
||||
def _band_contour(path: Path) -> bool:
|
||||
lines = json.loads(path.read_text(encoding="utf-8"))["contours"]
|
||||
return any(abs(line["level"] - 106.0) < 1e-6 for line in lines)
|
||||
|
||||
|
||||
def test_implicit_만_지어도_낡은_dtm_같이_다시(tmp_path: Path) -> None:
|
||||
data, mask = _banded_plane()
|
||||
masks = {"csf": mask}
|
||||
build_all_terrain_models(data, masks, tmp_path, _config("csf", ["dtm", "implicit"], 1.0))
|
||||
narrow = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
assert not _band_contour(contour) # 1 m 닫기 = 띠 빈칸 · 106 m 줄 잘림
|
||||
|
||||
# 화면 [서피스 생성] = 고른 표현 하나만 — 외곽 설정(지문)이 바뀌었으니 dtm 도 다시
|
||||
manifest = build_all_terrain_models(data, masks, tmp_path, _config("csf", ["implicit"], 3.0))
|
||||
methods = manifest["source_filters"]["csf"]["methods"]
|
||||
assert methods["dtm"]["status"] == "completed"
|
||||
wide = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
grid_area = _valid_area(implicit_grid_npz(tmp_path, "csf"))
|
||||
glb_area = methods["implicit"]["footprint_area_m2"]
|
||||
assert wide > 1.05 * narrow
|
||||
assert grid_area == wide # 격자 외곽 = dtm 외곽
|
||||
assert abs(glb_area - wide) / wide < 0.05 # 3D 외곽 = 격자 외곽
|
||||
assert _band_contour(contour) # 등고선이 넓힌 띠까지
|
||||
assert not contour_cache_is_stale(contour, tmp_path / "implicit_csf.npz")
|
||||
|
||||
|
||||
def test_dtm_이_바뀌면_등고선_캐시_낡음(tmp_path: Path) -> None:
|
||||
model = tmp_path / "implicit_csf.npz"
|
||||
dtm = tmp_path / "dtm_csf.npz"
|
||||
grid = tmp_path / "implicit_csf_grid.npz"
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
assert contour_cache_is_stale(contour, model) # 없음
|
||||
for index, path in enumerate((model, dtm, grid, contour)):
|
||||
path.write_bytes(b"x")
|
||||
os.utime(path, ns=(10**18 + index, 10**18 + index))
|
||||
assert not contour_cache_is_stale(contour, model)
|
||||
for newer in (dtm, grid):
|
||||
os.utime(newer, ns=(2 * 10**18, 2 * 10**18))
|
||||
assert contour_cache_is_stale(contour, model)
|
||||
os.utime(newer, ns=(10**18, 10**18))
|
||||
tin_smooth = tmp_path / "tin_csf_smooth.npz"
|
||||
tin_smooth.write_bytes(b"x")
|
||||
os.utime(tin_smooth, ns=(10**18, 10**18))
|
||||
os.utime(dtm, ns=(2 * 10**18, 2 * 10**18))
|
||||
assert contour_cache_is_stale(contour, tin_smooth) # 스무딩 면도 같은 필터 dtm 을 봄
|
||||
|
||||
|
||||
def test_ptd_implicit_만_지어도_굳힌_격자(tmp_path: Path) -> None:
|
||||
data, mask = _banded_plane()
|
||||
build_all_terrain_models(data, {"ptd": mask}, tmp_path, _config("ptd", ["implicit"], 3.0))
|
||||
assert (tmp_path / "dtm_ptd.npz").is_file()
|
||||
grid = implicit_grid_npz(tmp_path, "ptd") # B05 · B06 표본이 읽는 파일
|
||||
with np.load(grid) as loaded:
|
||||
assert np.isfinite(loaded["z"][loaded["valid_mask"]]).all()
|
||||
@@ -207,10 +207,7 @@ export const ui_locales_m2 = {
|
||||
"Lines not priced — total uses the rest · drop the line or fill its input",
|
||||
],
|
||||
M02_SangeunScaleOnly: ["열린 입력 — 품은 배율만", "Open inputs — quantity as multiplier only"],
|
||||
M02_SangeunBaseViews: [
|
||||
"시험 계산 · 텍스트 수식 = 바탕 단가산출 기초 + 고친 품 · 단가(뺀 줄 · 더한 줄 빼고)",
|
||||
"Test calc · formula = base M01 logic + edited qty · price (no dropped · added lines)",
|
||||
],
|
||||
M02_SangeunSourceNone: ["원문 연결 없음 — 바탕 로직 없음", "No source — no base logic"],
|
||||
M02_SangeunWait: ["셈 기다리는 중", "Calculating…"],
|
||||
M02_SangeunNoInputs: [
|
||||
"바탕 단가산출 기초 없음 — 시험 입력 없음",
|
||||
|
||||
Reference in New Issue
Block a user