This commit is contained in:
2026-07-10 18:12:17 +09:00
parent 50d75fcfcd
commit e3d66e717c
28 changed files with 2542 additions and 99 deletions
@@ -0,0 +1,121 @@
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RENDER_OPTIONS } from "@config/config_frontend";
import type { SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
export interface SurfacePointCloudViewer {
root: HTMLElement;
render: (data: SurfacePointCloudSampleResponse | null) => void;
dispose: () => void;
}
export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
const root = document.createElement("div");
root.className = "b04-surface-viewer";
const canvas = document.createElement("canvas");
canvas.className = "b04-surface-viewer__canvas";
root.append(canvas);
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: RENDER_OPTIONS.antialias,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, RENDER_OPTIONS.maxPixelRatio));
const scene = new THREE.Scene();
scene.background = new THREE.Color(RENDER_OPTIONS.canvasClearColorLight);
const camera = new THREE.PerspectiveCamera(
RENDER_OPTIONS.cameraFov,
1,
RENDER_OPTIONS.cameraNear,
RENDER_OPTIONS.cameraFar,
);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
const grid = new THREE.GridHelper(
200,
20,
RENDER_OPTIONS.gridCenterColor,
RENDER_OPTIONS.gridLineColor,
);
scene.add(grid);
let pointsObject: THREE.Points | null = null;
let animationFrame = 0;
function resize(): void {
const rect = root.getBoundingClientRect();
const width = Math.max(1, Math.floor(rect.width));
const height = Math.max(1, Math.floor(rect.height));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function animate(): void {
resize();
controls.update();
renderer.render(scene, camera);
animationFrame = requestAnimationFrame(animate);
}
function clearPoints(): void {
if (!pointsObject) return;
pointsObject.geometry.dispose();
const material = pointsObject.material;
if (Array.isArray(material)) material.forEach((item) => item.dispose());
else material.dispose();
scene.remove(pointsObject);
pointsObject = null;
}
function render(data: SurfacePointCloudSampleResponse | null): void {
clearPoints();
if (!data || data.points.length === 0) return;
const bounds = data.bounds;
const cx = ((bounds.x_min ?? 0) + (bounds.x_max ?? 0)) / 2;
const cy = ((bounds.y_min ?? 0) + (bounds.y_max ?? 0)) / 2;
const cz = ((bounds.z_min ?? 0) + (bounds.z_max ?? 0)) / 2;
const positions = new Float32Array(data.points.length * 3);
data.points.forEach((point, index) => {
positions[index * 3] = point[0] - cx;
positions[index * 3 + 1] = point[2] - cz;
positions[index * 3 + 2] = point[1] - cy;
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.computeBoundingSphere();
const material = new THREE.PointsMaterial({
color: RENDER_OPTIONS.pointColor,
size: 1.6,
sizeAttenuation: false,
});
pointsObject = new THREE.Points(geometry, material);
scene.add(pointsObject);
const radius = geometry.boundingSphere?.radius ?? 100;
camera.position.set(radius * 0.9, radius * 0.7, radius * 1.4);
camera.near = Math.max(RENDER_OPTIONS.cameraNear, radius / 10000);
camera.far = Math.max(RENDER_OPTIONS.cameraFar, radius * 8);
camera.updateProjectionMatrix();
controls.target.set(0, 0, 0);
controls.update();
}
animationFrame = requestAnimationFrame(animate);
return {
root,
render,
dispose: () => {
cancelAnimationFrame(animationFrame);
clearPoints();
controls.dispose();
renderer.dispose();
},
};
}