260717_4
This commit is contained in:
@@ -1,211 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B04 Surface GIS"])
|
||||
tiles_router = APIRouter(tags=["B04 MVT Tiles"])
|
||||
|
||||
|
||||
# VWorld 메타 API
|
||||
@router.get("/{project_id}/vworld-meta", response_model=None)
|
||||
async def get_vworld_meta(
|
||||
project_id: UUID, layer_name: str = "satellite"
|
||||
) -> dict[str, Any] | JSONResponse:
|
||||
"""VWorld 위성 맵 이미지 매핑 좌표 메타데이터를 반환합니다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
target_dir = project_root / "B04_wf1_Surface" / "processed"
|
||||
|
||||
target_layer = "white" if layer_name.lower() in ["gray", "white"] else layer_name.lower()
|
||||
meta_name = f"vworld_{target_layer}_meta.json"
|
||||
meta_path = target_dir / meta_name
|
||||
|
||||
if not meta_path.exists() and target_layer == "satellite":
|
||||
meta_path = target_dir / "vworld_meta.json"
|
||||
|
||||
if not meta_path.exists():
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": f"VWorld {layer_name} 메타데이터를 찾을 수 없습니다.",
|
||||
},
|
||||
)
|
||||
return json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "message": str(exc)})
|
||||
|
||||
|
||||
# VWorld 맵 API
|
||||
@router.get("/{project_id}/vworld-map", response_model=None)
|
||||
async def get_vworld_map(
|
||||
project_id: UUID, layer_name: str = "satellite"
|
||||
) -> FileResponse | JSONResponse:
|
||||
"""배경 지도 레이어 PNG 이미지를 반환합니다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
target_dir = project_root / "B04_wf1_Surface" / "processed"
|
||||
|
||||
target_layer = layer_name.lower()
|
||||
if target_layer in ["gray", "white"]:
|
||||
target_layer = "white"
|
||||
|
||||
map_name = f"vworld_{target_layer}.png"
|
||||
map_path = target_dir / map_name
|
||||
|
||||
if not map_path.exists() and target_layer == "satellite":
|
||||
map_path = target_dir / "vworld_map.png"
|
||||
|
||||
if not map_path.exists():
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": f"VWorld {layer_name} 지도가 존재하지 않습니다.",
|
||||
},
|
||||
)
|
||||
return FileResponse(map_path, media_type="image/png")
|
||||
except Exception as exc:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "message": str(exc)})
|
||||
|
||||
|
||||
# GeoJSON 조회 API
|
||||
@router.get("/{project_id}/geojson", response_model=None)
|
||||
async def get_project_geojson(project_id: UUID, layer: str) -> dict[str, Any] | JSONResponse:
|
||||
"""저장된 프로젝트의 특정 GeoJSON 레이어 데이터를 반환합니다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
target_dir = project_root / "B04_wf1_Surface" / "processed"
|
||||
|
||||
layer_mapping = {
|
||||
"지적도": "연속지적도_bounds.geojson",
|
||||
"용도지역": "용도지역도_bounds.geojson",
|
||||
"행정구역_시군구": "행정구역_시군구_bounds.geojson",
|
||||
"행정구역_읍면동": "행정구역_읍면동_bounds.geojson",
|
||||
"수계망": "수계망_물줄기_bounds.geojson",
|
||||
"등고선": "등고선_bounds.geojson",
|
||||
"산사태": "산사태위험등급_bounds.geojson",
|
||||
}
|
||||
|
||||
filename = layer_mapping.get(layer)
|
||||
if not filename:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"status": "error", "message": "유효하지 않은 레이어명입니다."},
|
||||
)
|
||||
|
||||
filepath = target_dir / filename
|
||||
if not filepath.exists():
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": f"요청한 레이어({layer}) 파일이 존재하지 않습니다.",
|
||||
},
|
||||
)
|
||||
|
||||
if layer == "등고선":
|
||||
simplified_filepath = target_dir / "등고선_bounds_simplified.geojson"
|
||||
if simplified_filepath.exists():
|
||||
try:
|
||||
return json.loads(simplified_filepath.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
import geopandas as gpd
|
||||
|
||||
gdf = gpd.read_file(filepath)
|
||||
gdf["geometry"] = gdf["geometry"].simplify(
|
||||
tolerance=0.00003, preserve_topology=True
|
||||
)
|
||||
simplified_filepath.write_text(gdf.to_json(), encoding="utf-8")
|
||||
return json.loads(simplified_filepath.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
logger.warning("등고선 단순화 처리 실패 (원본 전송): %s", e)
|
||||
|
||||
return json.loads(filepath.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "message": str(exc)})
|
||||
|
||||
|
||||
@tiles_router.get("/tiles/{project_id}/{layer}/{z}/{x}/{y}.pbf", response_model=None)
|
||||
async def get_vector_tile(project_id: UUID, layer: str, z: int, x: int, y: int) -> Response:
|
||||
"""프로젝트의 특정 레이어에 대한 정밀 벡터 타일(MVT) 조각을 동적으로 렌더링하여 반환합니다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
target_dir = project_root / "B04_wf1_Surface" / "processed"
|
||||
|
||||
layer_mapping = {
|
||||
"지적도": "연속지적도_bounds.geojson",
|
||||
"용도지역": "용도지역도_bounds.geojson",
|
||||
"행정구역_시군구": "행정구역_시군구_bounds.geojson",
|
||||
"행정구역_읍면동": "행정구역_읍면동_bounds.geojson",
|
||||
"수계망": "수계망_물줄기_bounds.geojson",
|
||||
"등고선": "등고선_bounds.geojson",
|
||||
"산사태": "산사태위험등급_bounds.geojson",
|
||||
"임도노선": "임도노선.geojson",
|
||||
}
|
||||
|
||||
filename = layer_mapping.get(layer)
|
||||
if not filename:
|
||||
raise HTTPException(status_code=400, detail="유효하지 않은 레이어명입니다.")
|
||||
|
||||
filepath = target_dir / filename
|
||||
|
||||
if layer == "임도노선" and not filepath.exists():
|
||||
from config.config_system import PROJECT_ROOT
|
||||
|
||||
road_shp_candidates = list(PROJECT_ROOT.glob("samples/**/*_Polyline.shp"))
|
||||
if road_shp_candidates:
|
||||
try:
|
||||
import geopandas as gpd
|
||||
|
||||
gdf = gpd.read_file(road_shp_candidates[0])
|
||||
gdf = gdf.to_crs(epsg=4326)
|
||||
filepath.write_text(gdf.to_json(), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not filepath.exists():
|
||||
import mapbox_vector_tile
|
||||
|
||||
empty_tile = mapbox_vector_tile.encode([]) if mapbox_vector_tile else b""
|
||||
return Response(content=empty_tile, media_type="application/x-protobuf")
|
||||
|
||||
cache_key = f"{project_id}_{layer}"
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Engine_MvtHelper import generate_mvt_tile
|
||||
|
||||
mvt_bytes = generate_mvt_tile(filepath, cache_key, z, x, y, layer_name=layer)
|
||||
return Response(
|
||||
content=mvt_bytes,
|
||||
media_type="application/x-protobuf",
|
||||
headers={"Content-Encoding": "identity"},
|
||||
)
|
||||
except Exception:
|
||||
import mapbox_vector_tile
|
||||
|
||||
empty_tile = mapbox_vector_tile.encode([]) if mapbox_vector_tile else b""
|
||||
return Response(content=empty_tile, media_type="application/x-protobuf")
|
||||
@@ -114,6 +114,10 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
viewer.applyCameraState(state);
|
||||
syncingCamera = false;
|
||||
});
|
||||
terrainViewer.onAxesVisibilityChange((visible) => {
|
||||
viewer.setAxesVisible(visible);
|
||||
});
|
||||
viewer.setAxesVisible(false);
|
||||
|
||||
const confirmButton = createButton({
|
||||
label: "모델 확정",
|
||||
|
||||
@@ -286,15 +286,17 @@
|
||||
}
|
||||
|
||||
.viewer-controls {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewer-controls button {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
padding: 0 var(--spacing-12);
|
||||
padding: 0 var(--spacing-4);
|
||||
font-size: var(--text-caption);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
@@ -342,6 +344,86 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.model-display-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-display-options .toggle-button {
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 0 var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.model-display-options .toggle-button input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-display-options .toggle-button:has(input:checked) {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.model-display-options .toggle-button:has(input:focus-visible) {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.model-display-options .toggle-button.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.contour-interval-form {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: auto minmax(64px, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contour-interval-input,
|
||||
.contour-interval-submit {
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.contour-interval-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
.contour-interval-submit {
|
||||
padding: 0 var(--spacing-12);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contour-interval-submit:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.viewer-option-val {
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 38px;
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface SurfaceTerrainViewer {
|
||||
setSelection: (sourceFilter: string, method: string) => void;
|
||||
applyCameraState: (state: SurfaceCameraState) => void;
|
||||
onCameraChange: (listener: (state: SurfaceCameraState) => void) => void;
|
||||
onAxesVisibilityChange: (listener: (visible: boolean) => void) => void;
|
||||
isSmoothingEnabled: () => boolean;
|
||||
resetOptions: () => void;
|
||||
dispose: () => void;
|
||||
@@ -39,14 +40,14 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
const axesCheck = document.createElement("input");
|
||||
axesCheck.type = "checkbox";
|
||||
axesCheck.checked = true;
|
||||
axesCheck.checked = false;
|
||||
|
||||
const rightControls = document.createElement("div");
|
||||
rightControls.className = "viewer-options";
|
||||
rightControls.className = "viewer-options model-display-options";
|
||||
|
||||
// Surface Toggle
|
||||
const surfLabel = document.createElement("label");
|
||||
surfLabel.className = "toggle-label";
|
||||
surfLabel.className = "toggle-label toggle-button";
|
||||
const surfCheck = document.createElement("input");
|
||||
surfCheck.type = "checkbox";
|
||||
surfCheck.checked = true;
|
||||
@@ -54,7 +55,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
// Smooth Toggle (for tin/dtm)
|
||||
const smoothLabel = document.createElement("label");
|
||||
smoothLabel.className = "toggle-label";
|
||||
smoothLabel.className = "toggle-label toggle-button";
|
||||
const smoothCheck = document.createElement("input");
|
||||
smoothCheck.type = "checkbox";
|
||||
smoothCheck.checked = true;
|
||||
@@ -62,7 +63,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
// Contour Toggle
|
||||
const contourLabel = document.createElement("label");
|
||||
contourLabel.className = "toggle-label";
|
||||
contourLabel.className = "toggle-label toggle-button";
|
||||
const contourCheck = document.createElement("input");
|
||||
contourCheck.type = "checkbox";
|
||||
contourCheck.checked = true;
|
||||
@@ -70,24 +71,19 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
// Contour Interval input form
|
||||
const intervalForm = document.createElement("form");
|
||||
intervalForm.style.display = "flex";
|
||||
intervalForm.style.alignItems = "center";
|
||||
intervalForm.style.gap = "var(--spacing-4)";
|
||||
intervalForm.className = "contour-interval-form";
|
||||
|
||||
const intervalInput = document.createElement("input");
|
||||
intervalInput.type = "number";
|
||||
intervalInput.value = "5.0";
|
||||
intervalInput.value = "1.0";
|
||||
intervalInput.step = "0.5";
|
||||
intervalInput.min = "0.5";
|
||||
intervalInput.style.width = "60px";
|
||||
intervalInput.style.padding = "var(--spacing-4) var(--spacing-8)";
|
||||
intervalInput.style.border = "1px solid var(--color-border)";
|
||||
intervalInput.style.borderRadius = "var(--radius-normal)";
|
||||
intervalInput.className = "contour-interval-input";
|
||||
|
||||
const intervalSubmit = document.createElement("button");
|
||||
intervalSubmit.type = "submit";
|
||||
intervalSubmit.textContent = "적용";
|
||||
intervalSubmit.style.padding = "var(--spacing-4) var(--spacing-12)";
|
||||
intervalSubmit.className = "contour-interval-submit";
|
||||
|
||||
intervalForm.append(
|
||||
document.createTextNode("간격 "),
|
||||
@@ -97,7 +93,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
);
|
||||
|
||||
const axesLabel = document.createElement("label");
|
||||
axesLabel.className = "toggle-label";
|
||||
axesLabel.className = "toggle-label toggle-button";
|
||||
axesLabel.append(axesCheck, document.createTextNode(" 축"));
|
||||
rightControls.append(axesLabel, surfLabel, smoothLabel, contourLabel, intervalForm);
|
||||
|
||||
@@ -174,8 +170,11 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
let currentModelsList: readonly SurfaceModelSummary[] = [];
|
||||
let referenceBounds: SurfaceBounds | null = null;
|
||||
let cameraListener: ((state: SurfaceCameraState) => void) | null = null;
|
||||
let axesVisibilityListener: ((visible: boolean) => void) | null = null;
|
||||
let suppressCameraEvent = false;
|
||||
let smoothPreferred = true;
|
||||
let currentModelId: number | null = null;
|
||||
let currentModelSmooth = false;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0xf5f7f9);
|
||||
@@ -295,6 +294,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
clearMesh();
|
||||
clearContours();
|
||||
currentModelId = null;
|
||||
scaleBar.hidden = true;
|
||||
statusSpan.textContent = "모델 조회 중...";
|
||||
|
||||
@@ -318,6 +318,8 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
const modelId = match.id;
|
||||
const isSmooth = (activeMethod === "tin" || activeMethod === "dtm") && smoothCheck.checked;
|
||||
currentModelId = modelId;
|
||||
currentModelSmooth = isSmooth;
|
||||
|
||||
statusSpan.textContent = "3D 메쉬 파일 다운로드 중...";
|
||||
const previewUrl = `${API_BASE_URL}/projects/${currentProjectId}/surface/models/${modelId}/preview?smooth=${isSmooth}`;
|
||||
@@ -339,7 +341,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
scene.add(points);
|
||||
fitCamera(points);
|
||||
statusSpan.textContent = `${activeFilter.toUpperCase()} · ${activeMethod.toUpperCase()} 표시 중`;
|
||||
loadContourLines(modelId, isSmooth);
|
||||
void loadContourLines(modelId, isSmooth);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
@@ -361,7 +363,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
scene.add(gltf.scene);
|
||||
fitCamera(gltf.scene);
|
||||
statusSpan.textContent = `${activeFilter.toUpperCase()} · ${activeMethod.toUpperCase()} 표시 중`;
|
||||
loadContourLines(modelId, isSmooth);
|
||||
void loadContourLines(modelId, isSmooth);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
@@ -374,8 +376,8 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContourLines(modelId: number, isSmooth: boolean) {
|
||||
const interval = parseFloat(intervalInput.value) || 5.0;
|
||||
async function loadContourLines(modelId: number, isSmooth: boolean): Promise<boolean> {
|
||||
const interval = parseFloat(intervalInput.value) || 1.0;
|
||||
const contourUrl = `${API_BASE_URL}/projects/${currentProjectId}/surface/models/${modelId}/contour?interval=${interval}&smooth=${isSmooth}`;
|
||||
|
||||
try {
|
||||
@@ -386,7 +388,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
clearContours();
|
||||
|
||||
const bounds = data.bounds;
|
||||
if (!bounds) return;
|
||||
if (!bounds) return false;
|
||||
|
||||
const cx = (bounds.x[0] + bounds.x[1]) / 2;
|
||||
const cy = (bounds.y[0] + bounds.y[1]) / 2;
|
||||
@@ -477,8 +479,10 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
} else {
|
||||
legendBar.style.display = "none";
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
legendBar.style.display = "none";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,6 +533,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
// Event Listeners
|
||||
axesCheck.addEventListener("change", () => {
|
||||
axes.visible = axesCheck.checked;
|
||||
axesVisibilityListener?.(axesCheck.checked);
|
||||
});
|
||||
|
||||
surfCheck.addEventListener("change", () => {
|
||||
@@ -549,9 +554,17 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
});
|
||||
});
|
||||
|
||||
intervalForm.addEventListener("submit", (e) => {
|
||||
intervalForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
updateSelectedModel();
|
||||
const interval = Number(intervalInput.value);
|
||||
if (!Number.isFinite(interval) || interval < 0.5 || currentModelId === null) return;
|
||||
intervalSubmit.disabled = true;
|
||||
statusSpan.textContent = `등고선 ${interval}m 계산 중...`;
|
||||
const loaded = await loadContourLines(currentModelId, currentModelSmooth);
|
||||
statusSpan.textContent = loaded
|
||||
? `${activeFilter.toUpperCase()} · ${activeMethod.toUpperCase()} · 등고선 ${interval}m`
|
||||
: "등고선 계산 또는 조회에 실패했습니다.";
|
||||
intervalSubmit.disabled = false;
|
||||
});
|
||||
|
||||
controls.addEventListener("change", emitCameraState);
|
||||
@@ -590,18 +603,22 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
onCameraChange(listener) {
|
||||
cameraListener = listener;
|
||||
},
|
||||
onAxesVisibilityChange(listener) {
|
||||
axesVisibilityListener = listener;
|
||||
},
|
||||
isSmoothingEnabled() {
|
||||
return !smoothCheck.disabled && smoothCheck.checked;
|
||||
},
|
||||
resetOptions() {
|
||||
axesCheck.checked = true;
|
||||
axes.visible = true;
|
||||
axesCheck.checked = false;
|
||||
axes.visible = false;
|
||||
axesVisibilityListener?.(false);
|
||||
surfCheck.checked = true;
|
||||
smoothPreferred = true;
|
||||
syncSmoothingSupport();
|
||||
contourCheck.checked = true;
|
||||
contourGroup.visible = true;
|
||||
intervalInput.value = "5.0";
|
||||
intervalInput.value = "1.0";
|
||||
if (terrainMesh) terrainMesh.visible = true;
|
||||
void updateSelectedModel();
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface SurfacePointCloudViewer {
|
||||
optionsGroup: HTMLElement;
|
||||
statusSpan: HTMLElement;
|
||||
render: (data: SurfacePointCloudSampleResponse | null) => void;
|
||||
setAxesVisible: (visible: boolean) => void;
|
||||
applyCameraState: (state: SurfaceCameraState) => void;
|
||||
onCameraChange: (listener: (state: SurfaceCameraState) => void) => void;
|
||||
resetOptions: () => void;
|
||||
@@ -50,14 +51,7 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
["front", makeButton("정면")],
|
||||
["side", makeButton("측면")],
|
||||
];
|
||||
const resetViewButton = makeButton("리셋");
|
||||
const axesLabel = document.createElement("label");
|
||||
axesLabel.className = "toggle-label";
|
||||
const axesCheck = document.createElement("input");
|
||||
axesCheck.type = "checkbox";
|
||||
axesCheck.checked = true;
|
||||
axesLabel.append(axesCheck, document.createTextNode(" 축"));
|
||||
controls.append(...viewButtons.map(([, button]) => button), resetViewButton, axesLabel);
|
||||
controls.append(...viewButtons.map(([, button]) => button));
|
||||
|
||||
const controlsGroup = document.createElement("section");
|
||||
controlsGroup.className = "b04-surface__group";
|
||||
@@ -119,6 +113,7 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
orbit.dampingFactor = 0.08;
|
||||
orbit.screenSpacePanning = true;
|
||||
const axes = new THREE.AxesHelper(45);
|
||||
axes.visible = false;
|
||||
scene.add(axes);
|
||||
|
||||
let pointsObject: THREE.Points | null = null;
|
||||
@@ -281,10 +276,6 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
viewButtons.forEach(([view, button]) => {
|
||||
button.addEventListener("click", () => setCameraView(view));
|
||||
});
|
||||
resetViewButton.addEventListener("click", () => setCameraView("iso"));
|
||||
axesCheck.addEventListener("change", () => {
|
||||
axes.visible = axesCheck.checked;
|
||||
});
|
||||
sizeInput.addEventListener("input", () => {
|
||||
if (pointsObject)
|
||||
(pointsObject.material as THREE.PointsMaterial).size = Number(sizeInput.value);
|
||||
@@ -316,9 +307,11 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
onCameraChange(listener) {
|
||||
cameraListener = listener;
|
||||
},
|
||||
setAxesVisible(visible) {
|
||||
axes.visible = visible;
|
||||
},
|
||||
resetOptions() {
|
||||
axesCheck.checked = true;
|
||||
axes.visible = true;
|
||||
axes.visible = false;
|
||||
sizeInput.value = "0.42";
|
||||
densityInput.value = "10";
|
||||
const label = densityLabel.querySelector(".viewer-option-val");
|
||||
|
||||
Reference in New Issue
Block a user