This commit is contained in:
2026-07-17 17:26:11 +09:00
parent f63e44fefd
commit 33815e0855
9 changed files with 189 additions and 140 deletions
+10 -1
View File
@@ -65,12 +65,21 @@ export interface SurfaceInputFileListResponse {
files: SurfaceInputFileSummary[];
}
export interface SurfaceBounds {
x_min: number;
x_max: number;
y_min: number;
y_max: number;
z_min: number;
z_max: number;
}
export interface SurfacePointCloudSampleResponse {
status: string;
project_id: string;
point_count: number;
sampled_count: number;
bounds: Record<string, number>;
bounds: SurfaceBounds;
points: [number, number, number][];
rgb?: [number, number, number][];
}
+16 -7
View File
@@ -19,6 +19,7 @@ from config.config_system import build_surface_model_config
# 진행 콜백 시그니처: (진행률 0~100, 현재 단계 키, 메시지)
ProgressCallback = Callable[[int, str, str], None]
GROUND_POINT_SAMPLE_LIMIT = 500_000
GROUND_POINT_CACHE_VERSION = 2
def _relative_to_project(project_root: Path, path: Path) -> str:
@@ -37,19 +38,27 @@ def cache_ground_points(
if mask is None:
mask = build_ground_masks(structured, [filter_key])[filter_key]
ground_indexes = np.flatnonzero(mask)
ground_point_count = int(len(ground_indexes))
ground_points = xyz[ground_indexes]
ground_point_count = int(len(ground_points))
if ground_point_count:
data_bounds = np.column_stack((ground_points.min(axis=0), ground_points.max(axis=0)))
else:
data_bounds = np.zeros((3, 2), dtype=np.float64)
if ground_point_count > GROUND_POINT_SAMPLE_LIMIT:
rng = np.random.default_rng(20260717)
ground_indexes = rng.choice(ground_indexes, GROUND_POINT_SAMPLE_LIMIT, replace=False)
points = xyz[ground_indexes]
if len(points):
bounds = np.column_stack((points.min(axis=0), points.max(axis=0)))
sample_indexes = rng.choice(
ground_point_count, GROUND_POINT_SAMPLE_LIMIT, replace=False
)
ground_indexes = ground_indexes[sample_indexes]
points = ground_points[sample_indexes]
else:
bounds = np.zeros((3, 2), dtype=np.float64)
points = ground_points
arrays = {
"xyz": points,
"bounds": np.asarray(bounds, dtype=np.float64),
"bounds": np.asarray(structured["bounds"], dtype=np.float64),
"data_bounds": np.asarray(data_bounds, dtype=np.float64),
"cache_version": np.asarray(GROUND_POINT_CACHE_VERSION, dtype=np.int16),
"point_count": np.asarray(ground_point_count, dtype=np.int64),
"sampled_count": np.asarray(len(points), dtype=np.int64),
}
+15 -4
View File
@@ -10,11 +10,15 @@ from uuid import UUID
import aiomysql
import numpy as np
from fastapi import APIRouter, HTTPException, Response
from fastapi import APIRouter
from fastapi.responses import FileResponse, JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B04_wf1_Surface.B04_wf1_Surface_Engine import cache_ground_points, run_surface_analysis
from B04_wf1_Surface.B04_wf1_Surface_Engine import (
GROUND_POINT_CACHE_VERSION,
cache_ground_points,
run_surface_analysis,
)
from B04_wf1_Surface.B04_wf1_Surface_Engine_Contour import (
CONTOUR_EXTRACTOR_VERSION,
extract_contours,
@@ -22,10 +26,10 @@ from B04_wf1_Surface.B04_wf1_Surface_Engine_Contour import (
from B04_wf1_Surface.B04_wf1_Surface_Repository import (
clear_confirmed_surface_models,
confirm_surface_model,
get_input_file,
list_project_point_cloud_inputs,
list_surface_models,
save_surface_analysis_to_db,
update_project_status,
)
from B04_wf1_Surface.B04_wf1_Surface_Schema import (
SurfaceAnalyzeRequest,
@@ -285,7 +289,14 @@ async def get_surface_point_cloud(
content={"status": "error", "message": "지원하지 않는 지면 필터입니다."},
)
source_path = structured_path.parent / f"ground_points_{filter}.npz"
if not source_path.is_file():
cache_is_current = False
if source_path.is_file():
with np.load(source_path) as cached:
cache_is_current = (
"cache_version" in cached
and int(cached["cache_version"]) == GROUND_POINT_CACHE_VERSION
)
if not cache_is_current:
source_path = await asyncio.to_thread(cache_ground_points, structured_path, filter)
with np.load(source_path) as structured:
@@ -0,0 +1,42 @@
import type { SurfaceBounds } from "./B04_wf1_Surface_Api_Fetch";
export const SURFACE_CAMERA_FOV = 50;
export interface SurfaceCameraState {
direction: [number, number, number];
distanceMeters: number;
targetMeters: [number, number, number];
}
export function getReferenceCenter(bounds: SurfaceBounds): [number, number, number] {
return [
(bounds.x_min + bounds.x_max) / 2,
(bounds.y_min + bounds.y_max) / 2,
(bounds.z_min + bounds.z_max) / 2,
];
}
export function getTopFitDistance(bounds: SurfaceBounds, aspect: number): number {
const width = Math.max(bounds.x_max - bounds.x_min, 1);
const depth = Math.max(bounds.y_max - bounds.y_min, 1);
const verticalFov = (SURFACE_CAMERA_FOV * Math.PI) / 180;
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * Math.max(aspect, 0.1));
const verticalDistance = depth / (2 * Math.tan(verticalFov / 2));
const horizontalDistance = width / (2 * Math.tan(horizontalFov / 2));
return Math.max(verticalDistance, horizontalDistance, 1) * 1.12;
}
export function targetPlaneMetersPerPixel(distanceMeters: number, viewportHeight: number): number {
const verticalFov = (SURFACE_CAMERA_FOV * Math.PI) / 180;
return (
(2 * Math.tan(verticalFov / 2) * Math.max(distanceMeters, 0.001)) / Math.max(viewportHeight, 1)
);
}
export function niceScaleDistance(roughMeters: number): number {
const exponent = Math.floor(Math.log10(Math.max(roughMeters, 0.001)));
const base = 10 ** exponent;
const normalized = roughMeters / base;
const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
return step * base;
}
@@ -3,12 +3,14 @@ import {
fetchGisGeoJson,
fetchVWorldMeta,
getVWorldMapUrl,
type SurfaceBounds,
type VWorldMeta,
} from "./B04_wf1_Surface_Api_Fetch";
import { niceScaleDistance } from "./B04_wf1_Surface_UI_Camera";
export interface SurfaceMapViewer {
root: HTMLElement;
render: (projectId: string) => void;
render: (projectId: string, referenceBounds?: SurfaceBounds) => void;
dispose: () => void;
}
@@ -36,11 +38,6 @@ function makeOption(value: string, label: string): HTMLOptionElement {
return option;
}
function prettyScaleDistance(roughMeters: number): number {
const candidates = [2, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
return candidates.find((value) => value >= roughMeters) ?? candidates[candidates.length - 1];
}
export function createSurfaceMapViewer(): SurfaceMapViewer {
const root = document.createElement("section");
root.className = "b04-map";
@@ -105,6 +102,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
root.append(header, viewport);
let currentProjectId: string | null = null;
let referenceBounds: SurfaceBounds | null = null;
let meta: VWorldMeta | null = null;
let geoJson: GeoJsonCollection | null = null;
let scale = 1;
@@ -117,10 +115,29 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
}
function fitReferenceBounds(): void {
if (!meta || !referenceBounds) return;
const rect = viewport.getBoundingClientRect();
const width = Math.max(rect.width, 1);
const height = Math.max(rect.height, 1);
const mapRect = getMapRect(width, height);
const referenceWidth = Math.max(referenceBounds.x_max - referenceBounds.x_min, 1);
const referenceHeight = Math.max(referenceBounds.y_max - referenceBounds.y_min, 1);
scale =
Math.min(meta.width_meters / referenceWidth, meta.height_meters / referenceHeight) * 0.9;
const centerX = (referenceBounds.x_min + referenceBounds.x_max) / 2;
const centerY = (referenceBounds.y_min + referenceBounds.y_max) / 2;
const baseX = mapRect.x + ((centerX - meta.x_min) / meta.width_meters) * mapRect.width;
const baseY = mapRect.y + (1 - (centerY - meta.y_min) / meta.height_meters) * mapRect.height;
offsetX = -(baseX - width / 2) * scale;
offsetY = -(baseY - height / 2) * scale;
}
function resetView(): void {
scale = 1;
offsetX = 0;
offsetY = 0;
fitReferenceBounds();
updateImageTransform();
drawVectorLayer();
}
@@ -212,7 +229,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
return;
}
const metersPerPixel = meta.width_meters / getMapRect(width, height).width / scale;
const meters = prettyScaleDistance(100 * metersPerPixel);
const meters = niceScaleDistance(100 * metersPerPixel);
const pixels = meters / metersPerPixel;
scaleBar.hidden = false;
scaleBar.style.width = `${pixels}px`;
@@ -274,7 +291,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
status.textContent = geoJson?.features
? L("B04_Surface_Map_Features").replace("{count}", geoJson.features.length.toLocaleString())
: "";
drawVectorLayer();
resetView();
} catch (error) {
if (sequence !== loadSequence) return;
status.textContent = error instanceof Error ? error.message : L("B04_Surface_Map_LoadFailed");
@@ -314,8 +331,9 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
return {
root,
render(projectId) {
render(projectId, nextReferenceBounds) {
currentProjectId = projectId;
referenceBounds = nextReferenceBounds ?? null;
void loadLayers();
},
dispose() {
+4 -1
View File
@@ -283,6 +283,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
showLoadingOverlay();
try {
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
terrainViewer.setReferenceBounds(pointCloud.bounds);
viewer.render(pointCloud);
renderInputInfo();
} catch (error) {
@@ -304,13 +305,15 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
models = modelResponse.models;
renderInputFiles(inputs.files);
renderStatus(status);
mapViewer.render(projectId);
try {
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
terrainViewer.setReferenceBounds(pointCloud.bounds);
viewer.render(pointCloud);
mapViewer.render(projectId, pointCloud.bounds);
} catch {
pointCloud = null;
viewer.render(null);
mapViewer.render(projectId);
}
renderInputInfo();
updateSelectedModel();
@@ -3,13 +3,20 @@ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js";
import { API_BASE_URL } from "@config/config_frontend";
import type { SurfaceModelSummary } from "./B04_wf1_Surface_Api_Fetch";
import type { SurfaceCameraState } from "./B04_wf1_Surface_UI_Viewer";
import type { SurfaceBounds, SurfaceModelSummary } from "./B04_wf1_Surface_Api_Fetch";
import {
getTopFitDistance,
niceScaleDistance,
SURFACE_CAMERA_FOV,
targetPlaneMetersPerPixel,
type SurfaceCameraState,
} from "./B04_wf1_Surface_UI_Camera";
export interface SurfaceTerrainViewer {
root: HTMLElement;
optionsGroup: HTMLElement;
render: (projectId: string, models: readonly SurfaceModelSummary[]) => void;
setReferenceBounds: (bounds: SurfaceBounds) => void;
setSelection: (sourceFilter: string, method: string) => void;
applyCameraState: (state: SurfaceCameraState) => void;
onCameraChange: (listener: (state: SurfaceCameraState) => void) => void;
@@ -115,27 +122,10 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
// Scale bar overlay
const scaleBar = document.createElement("div");
scaleBar.style.position = "absolute";
scaleBar.style.bottom = "16px";
scaleBar.style.left = "16px";
scaleBar.style.background = "rgba(255, 255, 255, 0.9)";
scaleBar.style.border = "1.5px solid #1e293b";
scaleBar.style.borderTop = "none";
scaleBar.style.height = "8px";
scaleBar.style.width = "100px";
scaleBar.style.zIndex = "10";
scaleBar.style.display = "none";
scaleBar.style.flexDirection = "column";
scaleBar.style.alignItems = "center";
scaleBar.style.justifyContent = "flex-end";
scaleBar.className = "b04-surface__scale";
scaleBar.hidden = true;
const scaleLabel = document.createElement("span");
scaleLabel.style.fontSize = "10px";
scaleLabel.style.fontWeight = "bold";
scaleLabel.style.color = "#1e293b";
scaleLabel.style.position = "absolute";
scaleLabel.style.bottom = "10px";
scaleLabel.style.whiteSpace = "nowrap";
scaleBar.append(scaleLabel);
viewerArea.append(scaleBar);
@@ -182,7 +172,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
// Three.js context variables
let currentProjectId = "";
let currentModelsList: readonly SurfaceModelSummary[] = [];
const sceneCenter = new THREE.Vector3();
let referenceBounds: SurfaceBounds | null = null;
let cameraListener: ((state: SurfaceCameraState) => void) | null = null;
let suppressCameraEvent = false;
let smoothPreferred = true;
@@ -190,7 +180,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f7f9);
const camera = new THREE.PerspectiveCamera(50, 1, 0.01, 100000);
const camera = new THREE.PerspectiveCamera(SURFACE_CAMERA_FOV, 1, 0.01, 100000);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
@@ -256,12 +246,13 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
};
const fitCamera = (object: THREE.Object3D) => {
const { center, span } = getFitParams(object);
sceneCenter.copy(center);
controls.target.copy(center);
camera.position.set(center.x, center.y + span * 1.2, center.z + 0.001);
camera.near = Math.max(span / 10000, 0.01);
camera.far = span * 100;
const { span } = getFitParams(object);
const aspect = viewerArea.clientWidth / Math.max(viewerArea.clientHeight, 1);
const distance = referenceBounds ? getTopFitDistance(referenceBounds, aspect) : span * 1.2;
controls.target.set(0, 0, 0);
camera.position.set(0, distance, 0.001);
camera.near = Math.max(distance / 10000, 0.01);
camera.far = distance * 100;
camera.updateProjectionMatrix();
controls.update();
};
@@ -270,18 +261,17 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
if (suppressCameraEvent || !cameraListener) return;
const offset = camera.position.clone().sub(controls.target);
const distance = Math.max(offset.length(), 0.001);
const targetOffset = controls.target.clone().sub(sceneCenter);
offset.normalize();
cameraListener({
direction: [offset.x, offset.y, offset.z],
distanceMeters: distance,
targetMeters: [targetOffset.x, targetOffset.y, targetOffset.z],
targetMeters: [controls.target.x, controls.target.y, controls.target.z],
});
}
function applyCameraState(state: SurfaceCameraState): void {
suppressCameraEvent = true;
controls.target.set(...state.targetMeters).add(sceneCenter);
controls.target.set(...state.targetMeters);
camera.position
.set(...state.direction)
.multiplyScalar(Math.max(state.distanceMeters, 0.001))
@@ -305,7 +295,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
clearMesh();
clearContours();
scaleBar.style.display = "none";
scaleBar.hidden = true;
statusSpan.textContent = "모델 조회 중...";
// 1. Find matching model in list
@@ -513,38 +503,16 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
// Render scale bar dynamically
if (terrainMesh && terrainMesh.visible) {
scaleBar.style.display = "flex";
scaleBar.hidden = false;
const dist = camera.position.distanceTo(controls.target);
const metersPerPixel =
(2 * Math.tan((camera.fov * Math.PI) / 360) * dist) / viewerArea.clientWidth;
const metersPerPixel = targetPlaneMetersPerPixel(dist, viewerArea.clientHeight);
const roughMeters = 100 * metersPerPixel;
const prettyMeters =
roughMeters < 5
? 2
: roughMeters < 15
? 10
: roughMeters < 35
? 20
: roughMeters < 75
? 50
: roughMeters < 150
? 100
: roughMeters < 350
? 200
: roughMeters < 750
? 500
: roughMeters < 1500
? 1000
: roughMeters < 3500
? 2000
: roughMeters < 7500
? 5000
: 10000;
const prettyMeters = niceScaleDistance(roughMeters);
scaleBar.style.width = `${prettyMeters / metersPerPixel}px`;
scaleLabel.textContent =
prettyMeters >= 1000 ? `${(prettyMeters / 1000).toFixed(0)} km` : `${prettyMeters} m`;
} else {
scaleBar.style.display = "none";
scaleBar.hidden = true;
}
// Update labels position
@@ -610,6 +578,9 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
currentModelsList = models;
updateSelectedModel();
},
setReferenceBounds(bounds) {
referenceBounds = bounds;
},
setSelection(sourceFilter, method) {
activeFilter = sourceFilter;
activeMethod = method;
+35 -49
View File
@@ -1,13 +1,17 @@
import { RENDER_OPTIONS } from "@config/config_frontend";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import type { SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
import type { SurfaceBounds, SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
import {
getReferenceCenter,
getTopFitDistance,
niceScaleDistance,
SURFACE_CAMERA_FOV,
targetPlaneMetersPerPixel,
type SurfaceCameraState,
} from "./B04_wf1_Surface_UI_Camera";
export interface SurfaceCameraState {
direction: [number, number, number];
distanceMeters: number;
targetMeters: [number, number, number];
}
export type { SurfaceCameraState } from "./B04_wf1_Surface_UI_Camera";
export interface SurfacePointCloudViewer {
root: HTMLElement;
@@ -30,11 +34,6 @@ function makeButton(label: string): HTMLButtonElement {
return button;
}
function prettyScaleDistance(roughMeters: number): number {
const values = [2, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
return values.find((value) => value >= roughMeters) ?? values[values.length - 1];
}
export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
const root = document.createElement("div");
root.className = "point-viewer";
@@ -114,7 +113,7 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
renderer.setPixelRatio(Math.min(window.devicePixelRatio, RENDER_OPTIONS.maxPixelRatio));
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f7f9);
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 22000);
const camera = new THREE.PerspectiveCamera(SURFACE_CAMERA_FOV, 1, 0.1, 22000);
const orbit = new OrbitControls(camera, canvas);
orbit.enableDamping = true;
orbit.dampingFactor = 0.08;
@@ -129,7 +128,7 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
let disposed = false;
let cameraListener: ((state: SurfaceCameraState) => void) | null = null;
let suppressCameraEvent = false;
let sceneScale = 1;
let referenceBounds: SurfaceBounds | null = null;
function resize(): void {
const rect = viewerArea.getBoundingClientRect();
@@ -151,14 +150,22 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
}
function setCameraView(view: CameraView): void {
const positions: Record<CameraView, [number, number, number]> = {
const directions: Record<CameraView, [number, number, number]> = {
iso: [120, 95, 135],
top: [0, 190, 0.001],
top: [0, 1, 0.00001],
front: [0, 60, 240],
side: [240, 60, 0],
};
const aspect = viewerArea.clientWidth / Math.max(viewerArea.clientHeight, 1);
const distance = referenceBounds ? getTopFitDistance(referenceBounds, aspect) : 190;
orbit.target.set(0, 0, 0);
camera.position.set(...positions[view]);
camera.position
.set(...directions[view])
.normalize()
.multiplyScalar(distance);
camera.near = Math.max(distance / 10000, 0.01);
camera.far = distance * 100;
camera.updateProjectionMatrix();
camera.lookAt(orbit.target);
orbit.update();
}
@@ -170,21 +177,17 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
offset.normalize();
cameraListener({
direction: [offset.x, offset.y, offset.z],
distanceMeters: distance / sceneScale,
targetMeters: [
orbit.target.x / sceneScale,
orbit.target.y / sceneScale,
orbit.target.z / sceneScale,
],
distanceMeters: distance,
targetMeters: [orbit.target.x, orbit.target.y, orbit.target.z],
});
}
function applyCameraState(state: SurfaceCameraState): void {
suppressCameraEvent = true;
orbit.target.set(...state.targetMeters).multiplyScalar(sceneScale);
orbit.target.set(...state.targetMeters);
camera.position
.set(...state.direction)
.multiplyScalar(Math.max(state.distanceMeters * sceneScale, 0.001))
.multiplyScalar(Math.max(state.distanceMeters, 0.001))
.add(orbit.target);
camera.lookAt(orbit.target);
orbit.update();
@@ -194,18 +197,11 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
function renderPointCloud(data: SurfacePointCloudSampleResponse): void {
clearPoints();
const bounds = data.bounds;
const xMin = bounds.x_min ?? 0;
const xMax = bounds.x_max ?? 0;
const yMin = bounds.y_min ?? 0;
const yMax = bounds.y_max ?? 0;
const zMin = bounds.z_min ?? 0;
const zMax = bounds.z_max ?? 0;
const xMid = (xMin + xMax) / 2;
const yMid = (yMin + yMax) / 2;
const zMid = (zMin + zMax) / 2;
const [xMid, yMid, zMid] = getReferenceCenter(bounds);
const zSpan = Math.max(zMax - zMin, 1e-9);
const scale = 180 / Math.max(xMax - xMin, yMax - yMin, zSpan, 1e-9);
sceneScale = scale;
referenceBounds = bounds;
const density = Number(densityInput.value);
const step = Math.max(1, Math.ceil(10 / density));
const count = Math.ceil(data.points.length / step);
@@ -215,9 +211,9 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
let outputIndex = 0;
for (let index = 0; index < data.points.length; index += step) {
const [x, y, z] = data.points[index];
positions[outputIndex * 3] = (x - xMid) * scale;
positions[outputIndex * 3 + 1] = (z - zMid) * scale;
positions[outputIndex * 3 + 2] = -(y - yMid) * scale;
positions[outputIndex * 3] = x - xMid;
positions[outputIndex * 3 + 1] = z - zMid;
positions[outputIndex * 3 + 2] = -(y - yMid);
const rgb = hasRgb ? data.rgb?.[index] : undefined;
if (rgb) {
const divisor = Math.max(...rgb) > 255 ? 65535 : 255;
@@ -251,21 +247,11 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
scaleBar.hidden = true;
return;
}
const bounds = currentData.bounds;
const span = Math.max(
bounds.x_max - bounds.x_min,
bounds.y_max - bounds.y_min,
bounds.z_max - bounds.z_min,
1e-9,
);
const internalScale = 180 / span;
const width = viewerArea.clientWidth || 1;
const distance = camera.position.distanceTo(orbit.target);
const sceneUnitsPerPixel = (2 * Math.tan((camera.fov * Math.PI) / 360) * distance) / width;
const metersPerPixel = sceneUnitsPerPixel / internalScale;
const meters = prettyScaleDistance(100 * metersPerPixel);
const metersPerPixel = targetPlaneMetersPerPixel(distance, viewerArea.clientHeight);
const meters = niceScaleDistance(100 * metersPerPixel);
scaleBar.hidden = false;
scaleBar.style.width = `${(meters * internalScale) / sceneUnitsPerPixel}px`;
scaleBar.style.width = `${meters / metersPerPixel}px`;
scaleText.textContent = meters >= 1000 ? `${meters / 1000} km` : `${meters} m`;
}
+10 -10
View File
@@ -90,8 +90,8 @@
"semantic_hash": ""
},
"docs/wiki/concepts/storage_paths.md": {
"mtime": 1783850555.0,
"ast_hash": "2cf2c535406b15d5f2d7d5cde19fd4d2",
"mtime": 1784276227.6785357,
"ast_hash": "a0fe28424a580b6dce5ddbcb66f886e5",
"semantic_hash": ""
},
"docs/wiki/concepts/ui_templates.md": {
@@ -110,8 +110,8 @@
"semantic_hash": ""
},
"docs/wiki/log.md": {
"mtime": 1784273614.860682,
"ast_hash": "509c18e4f8dc914ee6fc8d0abed6abfe",
"mtime": 1784276174.9796534,
"ast_hash": "a9bc801087f68a8cf53b4ccd025587ea",
"semantic_hash": ""
},
"docs/wiki/pages/A01_Home/A01_components.md": {
@@ -255,13 +255,13 @@
"semantic_hash": ""
},
"docs/wiki/pages/B04_wf1_Surface/B04_api.md": {
"mtime": 1784267047.0904605,
"ast_hash": "5558d1f973ce53a60c263b62f71b9b8d",
"mtime": 1784276164.8003862,
"ast_hash": "1ca8565f179fb4643bd4997cb6f1bc46",
"semantic_hash": ""
},
"docs/wiki/pages/B04_wf1_Surface/B04_backend.md": {
"mtime": 1784267712.4147556,
"ast_hash": "0de152bccf2967c5989e2ed9f0253781",
"mtime": 1784276156.6492395,
"ast_hash": "f3f818ea467831d6f60136ece8e3bb9b",
"semantic_hash": ""
},
"docs/wiki/pages/B04_wf1_Surface/B04_db.md": {
@@ -275,8 +275,8 @@
"semantic_hash": ""
},
"docs/wiki/pages/B04_wf1_Surface/B04_frontend.md": {
"mtime": 1784273544.11365,
"ast_hash": "c7505151247ab901363aaf50e942013c",
"mtime": 1784276170.2440593,
"ast_hash": "514a6d8a8e8a3b0093bbbad7df764651",
"semantic_hash": ""
},
"docs/wiki/pages/B05_wf2_Route/B05_api.md": {