feat(B04): 등고선 보간 6종을 만들어 화면에서 비교하게 한다
문헌(ANUDEM Hutchinson 1988/89, DEM 보간 비교연구)은 지형에 따라 우열이
갈려 단일 최적해가 없다고 본다. 방식을 하나로 고르지 않고 전부 만들어 두고
사용자가 눈으로 비교해 정하도록 했다(2026-08-30 사용자 지시).
신규 B04_PreProcess_Engine_SheetMethods.py — 거리비례 / TPS(박판, 감쇠
최소곡률 LSMR) / 라플라스 / TIN 선형 / TIN 곡면(Clough-Tocher) / IDW.
방식마다 dtm_sheet_{key}.npz + 프리뷰를 만들어 surface_models에
source_filter=sheet_{key}로 등록하므로 기존 뷰어·등고선·프리뷰 경로가 그대로
돈다. 폐합 링 안쪽 처리는 방식과 무관하게 똑같이 적용한다.
화면: 도엽등고 3D 서피스 컨테이너에 방식 전환 버튼과 '라이다 겹쳐 보기'
토글(반투명 파랑)을 달았다. 라이다는 같은 좌표계라 같은 자리에 겹친다.
실측(c1bb453f, 6종 65s): 평탄 셀 TPS 0.003% / 거리비례 0.014% /
TIN 곡면 0.026% / 라플라스 0.235% / TIN 선형 10.4% / IDW 69.1%.
LAS 대비 노선 |dz| 평균은 2.59~3.00m로 방식 간 차이가 작다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,12 @@ export interface SurfaceTerrainViewer {
|
||||
render: (projectId: string, models: readonly SurfaceModelSummary[]) => void;
|
||||
setReferenceBounds: (bounds: SurfaceBounds) => void;
|
||||
setSelection: (sourceFilter: string, method: string) => void;
|
||||
/** 다른 모델(예: 라이다 지표면)을 반투명으로 겹쳐 본다. 빈 문자열이면 걷어낸다. */
|
||||
showOverlay: (
|
||||
sourceFilter: string,
|
||||
method: string,
|
||||
smooth: boolean,
|
||||
) => Promise<boolean>;
|
||||
applyCameraState: (state: SurfaceCameraState) => void;
|
||||
onCameraChange: (listener: (state: SurfaceCameraState) => void) => void;
|
||||
onAxesVisibilityChange: (listener: (visible: boolean) => void) => void;
|
||||
@@ -291,6 +297,76 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 겹쳐 보기 메시 ─────────────────────────────────────────────────────────
|
||||
// 도엽등고 서피스 위에 라이다 지표면을 겹쳐 두 지형을 눈으로 대조한다
|
||||
// (2026-08-30 사용자 지시). 본 메시와 카메라·좌표계를 공유하므로 같은 자리에 겹친다.
|
||||
let overlayMesh: THREE.Object3D | null = null;
|
||||
let overlayGeneration = 0;
|
||||
|
||||
function clearOverlay() {
|
||||
if (overlayMesh) {
|
||||
scene.remove(overlayMesh);
|
||||
disposeObject(overlayMesh);
|
||||
overlayMesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOverlay(
|
||||
projectId: string,
|
||||
models: readonly SurfaceModelSummary[],
|
||||
sourceFilter: string,
|
||||
method: string,
|
||||
smooth: boolean,
|
||||
): Promise<boolean> {
|
||||
const generation = ++overlayGeneration;
|
||||
clearOverlay();
|
||||
const match = models.find((model) => {
|
||||
const configured = model.generation_params?.source_filter;
|
||||
return (
|
||||
model.model_type.toLowerCase() === method.toLowerCase() &&
|
||||
typeof configured === "string" &&
|
||||
configured.toLowerCase() === sourceFilter.toLowerCase()
|
||||
);
|
||||
});
|
||||
if (!match) return false;
|
||||
const url = `${API_BASE_URL}/projects/${projectId}/surface/models/${match.id}/preview?smooth=${smooth}`;
|
||||
try {
|
||||
const buffer = await fetchCachedBytes(projectId, url);
|
||||
if (generation !== overlayGeneration) return false;
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
new GLTFLoader().parse(
|
||||
buffer,
|
||||
"",
|
||||
(gltf) => {
|
||||
if (generation !== overlayGeneration) {
|
||||
disposeObject(gltf.scene);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
// 겹친 두 면을 구분하려고 반투명 단색으로 덮어씌운다.
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x60a5fa,
|
||||
transparent: true,
|
||||
opacity: 0.45,
|
||||
side: THREE.DoubleSide,
|
||||
flatShading: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
overlayMesh = gltf.scene;
|
||||
scene.add(gltf.scene);
|
||||
resolve(true);
|
||||
},
|
||||
() => resolve(false),
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearContours() {
|
||||
while (contourGroup.children.length > 0) {
|
||||
const child = contourGroup.children[0];
|
||||
@@ -776,6 +852,19 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
activeMethod = method;
|
||||
syncSmoothingSupport();
|
||||
},
|
||||
showOverlay(sourceFilter, method, smooth) {
|
||||
if (!sourceFilter) {
|
||||
clearOverlay();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return loadOverlay(
|
||||
currentProjectId,
|
||||
currentModelsList,
|
||||
sourceFilter,
|
||||
method,
|
||||
smooth,
|
||||
);
|
||||
},
|
||||
applyCameraState,
|
||||
onCameraChange(listener) {
|
||||
cameraListener = listener;
|
||||
|
||||
Reference in New Issue
Block a user