move json to api, change tile loading api
This commit is contained in:
@@ -176,6 +176,9 @@ const tileCache = ref({}); // Cached .mound data (for shading sandbox)
|
||||
const loadedPngTiles = ref(new Set()); // Track which PNG tiles are displayed on map
|
||||
const currentTileData = ref(null);
|
||||
|
||||
const loadingState = ref({}); // {"-82.448446,40.051780": 'pending'}
|
||||
// Values: 'looking_up' | 'found' | 'processing' | 'complete' | 'failed'
|
||||
|
||||
// Geometry state
|
||||
const drawMode = ref(null); // 'line', 'ray', or null
|
||||
const drawPoints = ref([]);
|
||||
@@ -391,14 +394,12 @@ const BIBLIOGRAPHY = {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// KNOWN_SITES with citations added to descriptions
|
||||
// Citations use \cite{key} format for easy parsing
|
||||
|
||||
const KNOWN_SITES = [
|
||||
{
|
||||
"name": "Newark Octagon Earthworks",
|
||||
"coordinates": [[-82.4463745, 40.0519828]],
|
||||
"coordinates": [[-82.444270, 40.054705]],
|
||||
"description": "Part of the Newark Earthworks complex, the Octagon is precisely aligned to the 18.6-year lunar cycle \\cite{hively_horn_1982}. Connected to a 20-acre Observatory Circle, this geometric earthwork demonstrates sophisticated astronomical knowledge. The walls and gateways encode all eight lunar standstill rise and set points \\cite{mickelson_lepper_2007}. The Octagon's eight walls (each approximately 550 feet long) and Observatory Circle form one of only two known circle-octagon pairs in the Hopewell world, the other being High Bank Works \\cite{hively_horn_1984}. First comprehensively surveyed by Squier and Davis in the 1840s \\cite{squier_davis_1848}. Inscribed as a UNESCO World Heritage Site in September 2023 \\cite{unesco_2023}.",
|
||||
"type": "earthwork",
|
||||
"tiles": [
|
||||
@@ -413,7 +414,8 @@ const KNOWN_SITES = [
|
||||
'BS19870743',
|
||||
'BS19860743',
|
||||
'BS19880743',
|
||||
'BS19880742'
|
||||
'BS19880742',
|
||||
'BS19830746'
|
||||
],
|
||||
|
||||
"overlay": [
|
||||
@@ -442,7 +444,7 @@ const KNOWN_SITES = [
|
||||
},
|
||||
{
|
||||
"name": "Van Voorhis Walls",
|
||||
"coordinates": [[-82.446375, 40.051983], [-82.447, 40.048], [-82.448, 40.045], [-82.45, 40.04]],
|
||||
"coordinates": [[ -82.459139,40.028334]],
|
||||
"description": "The best-preserved section of the Great Hopewell Road, extending 2.5 miles south from the Newark Octagon to Ramp Creek \\cite{lepper_1995}. This confirmed earthwork consists of parallel walls approximately 60 meters (200 feet) apart, aligned on an azimuth of approximately 212° toward Chillicothe \\cite{schwarz_2016}. First documented by James and Charles Salisbury in 1862, who followed the walls for 6 miles through 'tangled swamps and over hills, still keeping their undeviating course' \\cite{salisbury_salisbury_1862}. LiDAR analysis suggests the road was sunken between the walls \\cite{romain_burks_2008}. Test excavations in 2009 revealed a thin layer of white limestone that may have paved the road \\cite{lepper_2024}. Still visible above ground in woodland areas too swampy to farm.",
|
||||
"type": "road_confirmed",
|
||||
"tiles": ['BS19820746', 'BS19820745', 'BS19820743', 'BS19820742', 'BS19860742', 'BS19880742']
|
||||
@@ -712,7 +714,7 @@ async function loadPngTiles() {
|
||||
for (const tileName of TILE_NAMES) {
|
||||
try {
|
||||
// Fetch metadata JSON for bounds
|
||||
const metaResponse = await fetch(`/png/${tileName}.json`);
|
||||
const metaResponse = await fetch(`/tiles/JSON/${tileName}.json`);
|
||||
if (!metaResponse.ok) {
|
||||
console.warn(`No metadata for ${tileName}, skipping`);
|
||||
continue;
|
||||
@@ -720,7 +722,7 @@ async function loadPngTiles() {
|
||||
const meta = await metaResponse.json();
|
||||
|
||||
// Check PNG exists
|
||||
const pngUrl = `/png/${tileName}.png`;
|
||||
const pngUrl = `/tiles/PNG/${tileName}.png`;
|
||||
const pngResponse = await fetch(pngUrl, { method: 'HEAD' });
|
||||
if (!pngResponse.ok) {
|
||||
console.warn(`No PNG for ${tileName}, skipping`);
|
||||
@@ -774,7 +776,7 @@ async function loadMoundData(tileName) {
|
||||
console.log(`Loading mound data for ${tileName}...`);
|
||||
|
||||
try {
|
||||
const data = await parseMoundFile(`/mound/${tileName}.mound`);
|
||||
const data = await parseMoundFile(`/tiles/mound/${tileName}.mound`);
|
||||
tileCache.value[tileName] = data;
|
||||
console.log(`Cached mound data for ${tileName}`);
|
||||
return data;
|
||||
@@ -1049,28 +1051,67 @@ function clearAllGeometry() {
|
||||
// Request a tile for a specific location
|
||||
async function requestTile() {
|
||||
const { lng, lat } = contextMenu.value.lngLat;
|
||||
const stateKey = `${lat},${lng}`;
|
||||
|
||||
// TODO: Call backend endpoint
|
||||
// For now, just log what we would send
|
||||
const requestData = {
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
// Backend will:
|
||||
// 1. Transform to Ohio State Plane South (EPSG:3735) in US Survey Feet
|
||||
// 2. Call https://maps.ohio.gov/arcgis/rest/services/OGRIP/3DepTiles/MapServer/0/query
|
||||
// 3. Extract TileName, County, Block
|
||||
// 4. Return tile info and download URL
|
||||
// Initialize loading state
|
||||
loadingState.value[stateKey] = 'looking_up';
|
||||
|
||||
// Open SSE connection
|
||||
const eventSource = new EventSource(`/tiles/request?lat=${lat}&lng=${lng}`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.status) {
|
||||
case 'looking_up':
|
||||
loadingState.value[stateKey] = 'looking_up';
|
||||
break;
|
||||
|
||||
case 'found':
|
||||
loadingState.value[stateKey] = 'found';
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
loadingState.value[stateKey] = 'processing';
|
||||
break;
|
||||
|
||||
case 'ready':
|
||||
loadingState.value[stateKey] = 'complete';
|
||||
// Load the tile data into cache
|
||||
loadMoundData(data.tile_id);
|
||||
eventSource.close();
|
||||
// Clean up loading state after 10 seconds
|
||||
setTimeout(() => {
|
||||
delete loadingState.value[stateKey];
|
||||
}, 10000);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
loadingState.value[stateKey] = 'failed';
|
||||
console.error('Tile request failed:', data.message);
|
||||
// TODO: Show toast notification with error message
|
||||
eventSource.close();
|
||||
// Clean up loading state after 10 seconds
|
||||
setTimeout(() => {
|
||||
delete loadingState.value[stateKey];
|
||||
}, 10000);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
loadingState.value[stateKey] = 'failed';
|
||||
console.error('SSE connection error:', error);
|
||||
// TODO: Show toast notification with error message
|
||||
eventSource.close();
|
||||
// Clean up loading state after 10 seconds
|
||||
setTimeout(() => {
|
||||
delete loadingState.value[stateKey];
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
console.log('=== Tile Request ===');
|
||||
console.log('Would send to backend:', requestData);
|
||||
console.log('Backend endpoint: POST /api/tiles/request');
|
||||
|
||||
// Close the context menu
|
||||
contextMenu.value.visible = false;
|
||||
|
||||
// TODO: Show loading indicator
|
||||
// TODO: Handle response and update UI
|
||||
}
|
||||
|
||||
// Load mound data for interactive shading
|
||||
|
||||
210
ui/src/App1.vue
210
ui/src/App1.vue
@@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<canvas ref="canvas" class="canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
export default {
|
||||
name: 'App1',
|
||||
setup() {
|
||||
const canvas = ref(null);
|
||||
let scene, camera, renderer, controls;
|
||||
|
||||
const parseMoundFile = async (url) => {
|
||||
const response = await fetch(url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
const view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
|
||||
const magic = String.fromCharCode(
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++)
|
||||
);
|
||||
|
||||
if (magic !== 'LIDR') {
|
||||
throw new Error('Invalid .mound file');
|
||||
}
|
||||
|
||||
const version = view.getUint32(offset, true); offset += 4;
|
||||
const pointCount = view.getUint32(offset, true); offset += 4;
|
||||
const triangleCount = view.getUint32(offset, true); offset += 4;
|
||||
const minX = view.getFloat32(offset, true); offset += 4;
|
||||
const minY = view.getFloat32(offset, true); offset += 4;
|
||||
const minZ = view.getFloat32(offset, true); offset += 4;
|
||||
const maxX = view.getFloat32(offset, true); offset += 4;
|
||||
const maxY = view.getFloat32(offset, true); offset += 4;
|
||||
const maxZ = view.getFloat32(offset, true); offset += 4;
|
||||
offset += 24; // Skip reserved
|
||||
|
||||
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
||||
offset += pointCount * 3 * 4;
|
||||
|
||||
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
||||
|
||||
return {
|
||||
pointCount,
|
||||
triangleCount,
|
||||
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
||||
positions,
|
||||
indices
|
||||
};
|
||||
};
|
||||
|
||||
const loadLidarTile = async () => {
|
||||
try {
|
||||
console.log('Loading BS19820747...');
|
||||
const data = await parseMoundFile('/tiles/BS19820747.mound');
|
||||
|
||||
console.log('Bounds:', data.bounds);
|
||||
console.log('Points:', data.pointCount);
|
||||
console.log('Triangles:', data.triangleCount);
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
// Calculate center and scale first
|
||||
const centerX = (data.bounds.minX + data.bounds.maxX) / 2;
|
||||
const centerY = (data.bounds.minY + data.bounds.maxY) / 2;
|
||||
const centerZ = (data.bounds.minZ + data.bounds.maxZ) / 2;
|
||||
|
||||
const spanX = data.bounds.maxX - data.bounds.minX;
|
||||
const spanY = data.bounds.maxY - data.bounds.minY;
|
||||
const spanZ = data.bounds.maxZ - data.bounds.minZ;
|
||||
const maxSpan = Math.max(spanX, spanY);
|
||||
|
||||
console.log('Center:', centerX, centerY, centerZ);
|
||||
console.log('Span X:', spanX, 'Y:', spanY, 'Z:', spanZ);
|
||||
console.log('Max span:', maxSpan);
|
||||
|
||||
// Transform vertices: center and scale
|
||||
const normalizeScale = 10 / maxSpan;
|
||||
const zScale = normalizeScale * (maxSpan * 0.1) / spanZ;
|
||||
|
||||
console.log('Normalize scale:', normalizeScale);
|
||||
console.log('Z scale:', zScale);
|
||||
|
||||
// Create new position array with transformed coordinates
|
||||
const transformedPositions = new Float32Array(data.positions.length);
|
||||
for (let i = 0; i < data.positions.length; i += 3) {
|
||||
transformedPositions[i] = (data.positions[i] - centerX) * normalizeScale; // X
|
||||
transformedPositions[i + 1] = (data.positions[i + 1] - centerY) * normalizeScale; // Y
|
||||
transformedPositions[i + 2] = (data.positions[i + 2] - centerZ) * zScale; // Z
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(transformedPositions, 3));
|
||||
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
console.log('First few transformed positions:', transformedPositions.slice(0, 15));
|
||||
|
||||
const material = new THREE.MeshLambertMaterial({
|
||||
color: 0x8B7355,
|
||||
side: THREE.DoubleSide,
|
||||
wireframe: true
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
|
||||
console.log('Mesh added to scene at origin');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load tile:', err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Scene
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x87CEEB);
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.set(0, -15, 10);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// Renderer
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas.value,
|
||||
antialias: true
|
||||
});
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// OrbitControls for pan, orbit, zoom
|
||||
controls = new OrbitControls(camera, canvas.value);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.target.set(0, 0, 0);
|
||||
|
||||
// Lights for the terrain
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(5, 5, 10);
|
||||
scene.add(directionalLight);
|
||||
|
||||
// Axes helper
|
||||
const axesHelper = new THREE.AxesHelper(15);
|
||||
scene.add(axesHelper);
|
||||
|
||||
// Load lidar
|
||||
loadLidarTile();
|
||||
|
||||
// Animation loop
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
console.log('Three.js initialized');
|
||||
console.log('Scene:', scene);
|
||||
console.log('Camera:', camera);
|
||||
});
|
||||
|
||||
return { canvas };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
398
ui/src/App2.vue
398
ui/src/App2.vue
@@ -1,398 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<canvas ref="canvas" class="canvas"></canvas>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<label>Light Azimuth (E-W)</label>
|
||||
<input
|
||||
v-model.number="controls_state.azimuth"
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
@input="onControlChange"
|
||||
/>
|
||||
<span class="value">{{ Math.round(controls_state.azimuth) }}°</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Light Altitude (N-S)</label>
|
||||
<input
|
||||
v-model.number="controls_state.altitude"
|
||||
type="range"
|
||||
min="0"
|
||||
max="90"
|
||||
step="1"
|
||||
@input="onControlChange"
|
||||
/>
|
||||
<span class="value">{{ Math.round(controls_state.altitude) }}°</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Light Intensity</label>
|
||||
<input
|
||||
v-model.number="controls_state.intensity"
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
@input="onControlChange"
|
||||
/>
|
||||
<span class="value">{{ controls_state.intensity.toFixed(1) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Height Exaggeration</label>
|
||||
<input
|
||||
v-model.number="controls_state.heightScale"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="10"
|
||||
step="0.1"
|
||||
@input="onControlChange"
|
||||
/>
|
||||
<span class="value">{{ controls_state.heightScale.toFixed(1) }}x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
export default {
|
||||
name: 'App1',
|
||||
setup() {
|
||||
const canvas = ref(null);
|
||||
let scene, camera, renderer, threeControls, mesh, directionalLight;
|
||||
|
||||
const controls_state = ref({
|
||||
azimuth: 45,
|
||||
altitude: 45,
|
||||
intensity: 0.8,
|
||||
heightScale: 1
|
||||
});
|
||||
|
||||
let geometryCache = null;
|
||||
let centerX, centerY, centerZ, spanZ, normalizeScale;
|
||||
|
||||
const parseMoundFile = async (url) => {
|
||||
const response = await fetch(url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
const view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
|
||||
const magic = String.fromCharCode(
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++),
|
||||
view.getUint8(offset++)
|
||||
);
|
||||
|
||||
if (magic !== 'LIDR') {
|
||||
throw new Error('Invalid .mound file');
|
||||
}
|
||||
|
||||
const version = view.getUint32(offset, true); offset += 4;
|
||||
const pointCount = view.getUint32(offset, true); offset += 4;
|
||||
const triangleCount = view.getUint32(offset, true); offset += 4;
|
||||
const minX = view.getFloat32(offset, true); offset += 4;
|
||||
const minY = view.getFloat32(offset, true); offset += 4;
|
||||
const minZ = view.getFloat32(offset, true); offset += 4;
|
||||
const maxX = view.getFloat32(offset, true); offset += 4;
|
||||
const maxY = view.getFloat32(offset, true); offset += 4;
|
||||
const maxZ = view.getFloat32(offset, true); offset += 4;
|
||||
offset += 24; // Skip reserved
|
||||
|
||||
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
||||
offset += pointCount * 3 * 4;
|
||||
|
||||
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
||||
|
||||
return {
|
||||
pointCount,
|
||||
triangleCount,
|
||||
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
||||
positions,
|
||||
indices
|
||||
};
|
||||
};
|
||||
|
||||
const loadLidarTile = async () => {
|
||||
try {
|
||||
console.log('Loading BS19820747...');
|
||||
const data = await parseMoundFile('/tiles/BS19820747.mound');
|
||||
|
||||
console.log('Bounds:', data.bounds);
|
||||
console.log('Points:', data.pointCount);
|
||||
console.log('Triangles:', data.triangleCount);
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
||||
// Calculate center and scale first
|
||||
centerX = (data.bounds.minX + data.bounds.maxX) / 2;
|
||||
centerY = (data.bounds.minY + data.bounds.maxY) / 2;
|
||||
centerZ = (data.bounds.minZ + data.bounds.maxZ) / 2;
|
||||
|
||||
const spanX = data.bounds.maxX - data.bounds.minX;
|
||||
const spanY = data.bounds.maxY - data.bounds.minY;
|
||||
spanZ = data.bounds.maxZ - data.bounds.minZ;
|
||||
const maxSpan = Math.max(spanX, spanY);
|
||||
|
||||
console.log('Center:', centerX, centerY, centerZ);
|
||||
console.log('Span X:', spanX, 'Y:', spanY, 'Z:', spanZ);
|
||||
console.log('Max span:', maxSpan);
|
||||
|
||||
// Transform vertices: center and scale
|
||||
normalizeScale = 10 / maxSpan;
|
||||
const zScale = normalizeScale * (maxSpan * 0.1) / spanZ;
|
||||
|
||||
console.log('Normalize scale:', normalizeScale);
|
||||
console.log('Z scale:', zScale);
|
||||
|
||||
// Create new position array with transformed coordinates
|
||||
const transformedPositions = new Float32Array(data.positions.length);
|
||||
for (let i = 0; i < data.positions.length; i += 3) {
|
||||
transformedPositions[i] = (data.positions[i] - centerX) * normalizeScale; // X
|
||||
transformedPositions[i + 1] = (data.positions[i + 1] - centerY) * normalizeScale; // Y
|
||||
transformedPositions[i + 2] = (data.positions[i + 2] - centerZ) * zScale; // Z
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(transformedPositions, 3));
|
||||
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
geometryCache = { geometry, baseZ: new Float32Array(data.positions.map((v, i) => i % 3 === 2 ? (v - centerZ) * zScale : 0)), zScale };
|
||||
|
||||
console.log('First few transformed positions:', transformedPositions.slice(0, 15));
|
||||
|
||||
const material = new THREE.MeshLambertMaterial({
|
||||
color: 0x8B7355,
|
||||
side: THREE.DoubleSide,
|
||||
wireframe: false
|
||||
});
|
||||
|
||||
mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
|
||||
// Apply initial height scale
|
||||
updateHeightScale();
|
||||
|
||||
console.log('Mesh added to scene at origin');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load tile:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateHeightScale = () => {
|
||||
if (!geometryCache) return;
|
||||
|
||||
const positions = geometryCache.geometry.attributes.position.array;
|
||||
const exaggeration = controls_state.value.heightScale;
|
||||
|
||||
// Only every 3rd value (Z coordinate)
|
||||
for (let i = 2; i < positions.length; i += 3) {
|
||||
positions[i] = geometryCache.baseZ[i] * exaggeration;
|
||||
}
|
||||
|
||||
geometryCache.geometry.attributes.position.needsUpdate = true;
|
||||
geometryCache.geometry.computeVertexNormals();
|
||||
};
|
||||
|
||||
const updateLightPosition = () => {
|
||||
if (!directionalLight) return;
|
||||
|
||||
const azimuth = (controls_state.value.azimuth * Math.PI) / 180;
|
||||
const altitude = (controls_state.value.altitude * Math.PI) / 180;
|
||||
|
||||
const distance = 20;
|
||||
const x = Math.cos(azimuth) * Math.cos(altitude) * distance;
|
||||
const y = Math.sin(azimuth) * Math.cos(altitude) * distance;
|
||||
const z = Math.sin(altitude) * distance;
|
||||
|
||||
directionalLight.position.set(x, y, z);
|
||||
directionalLight.intensity = controls_state.value.intensity;
|
||||
};
|
||||
|
||||
const onControlChange = () => {
|
||||
updateLightPosition();
|
||||
updateHeightScale();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Scene
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x87CEEB);
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
camera.position.set(0, -15, 10);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// Renderer
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas.value,
|
||||
antialias: true
|
||||
});
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// OrbitControls for pan, orbit, zoom
|
||||
threeControls = new OrbitControls(camera, canvas.value);
|
||||
threeControls.enableDamping = true;
|
||||
threeControls.dampingFactor = 0.05;
|
||||
threeControls.target.set(0, 0, 0);
|
||||
|
||||
// Lights for the terrain
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||
scene.add(ambientLight);
|
||||
|
||||
directionalLight = new THREE.DirectionalLight(0xffffff, controls_state.value.intensity);
|
||||
scene.add(directionalLight);
|
||||
|
||||
// Set initial light position
|
||||
updateLightPosition();
|
||||
|
||||
// Axes helper
|
||||
const axesHelper = new THREE.AxesHelper(15);
|
||||
scene.add(axesHelper);
|
||||
|
||||
// Load lidar
|
||||
loadLidarTile();
|
||||
|
||||
// Animation loop
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
threeControls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
console.log('Three.js initialized');
|
||||
console.log('Scene:', scene);
|
||||
console.log('Camera:', camera);
|
||||
});
|
||||
|
||||
return { canvas, controls_state, onControlChange };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 280px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.control-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.control-group input[type="range"] {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4A9EFF;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #5BADFF;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4A9EFF;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-moz-range-thumb:hover {
|
||||
background: #5BADFF;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user