BIIIIG FIXES
This commit is contained in:
@@ -41,7 +41,7 @@ Example layout for 100 points and 50 triangles:
|
|||||||
Total file size: 1864 bytes
|
Total file size: 1864 bytes
|
||||||
|
|
||||||
Coordinate System:
|
Coordinate System:
|
||||||
Input: Ohio State Plane North (EPSG:3734) in US Survey Feet
|
Input: Ohio State Plane South (EPSG:3735) in US Survey Feet
|
||||||
Output: Web Mercator (EPSG:3857) in meters
|
Output: Web Mercator (EPSG:3857) in meters
|
||||||
This ensures compatibility with MapLibre GL JS and web mapping standards.
|
This ensures compatibility with MapLibre GL JS and web mapping standards.
|
||||||
"""
|
"""
|
||||||
@@ -74,9 +74,9 @@ def transform_to_webmercator(x, y, z):
|
|||||||
"""Transform Ohio State Plane coordinates to Web Mercator (EPSG:3857)."""
|
"""Transform Ohio State Plane coordinates to Web Mercator (EPSG:3857)."""
|
||||||
print("Transforming coordinates to Web Mercator...")
|
print("Transforming coordinates to Web Mercator...")
|
||||||
|
|
||||||
# Ohio State Plane North (EPSG:3734) in US Survey Feet to Web Mercator (EPSG:3857) in meters
|
# Ohio State Plane South (EPSG:3735) in US Survey Feet to Web Mercator (EPSG:3857) in meters
|
||||||
# Newark is in the North zone
|
# The lidar data uses FIPS 3402 (Ohio South) per metadata
|
||||||
transformer = Transformer.from_crs("EPSG:3734", "EPSG:3857", always_xy=True)
|
transformer = Transformer.from_crs("EPSG:3735", "EPSG:3857", always_xy=True)
|
||||||
|
|
||||||
# Transform x,y (easting, northing) to Web Mercator meters
|
# Transform x,y (easting, northing) to Web Mercator meters
|
||||||
merc_x, merc_y = transformer.transform(x, y)
|
merc_x, merc_y = transformer.transform(x, y)
|
||||||
|
|||||||
506
ui/src/App.vue
506
ui/src/App.vue
@@ -1,15 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div ref="mapContainer" class="map-container"></div>
|
<div ref="mapContainer" class="map-container"></div>
|
||||||
<canvas ref="threeCanvas" class="three-canvas"></canvas>
|
|
||||||
|
|
||||||
<ShadingSandbox
|
<ShadingSandbox
|
||||||
:visible="sandboxVisible"
|
:visible="sandboxVisible"
|
||||||
ref="ShadingSandbox"
|
:offscreen="sandboxOffscreen"
|
||||||
|
:initial-settings="DEFAULT_RENDER_SETTINGS"
|
||||||
|
ref="sandboxRef"
|
||||||
@close="sandboxVisible = false"
|
@close="sandboxVisible = false"
|
||||||
@renderComplete="onRenderComplete"
|
@renderComplete="onRenderComplete"
|
||||||
@error="onRenderError"
|
@error="onRenderError"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="layer-controls">
|
<div class="layer-controls">
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
<label>Base Map:</label>
|
<label>Base Map:</label>
|
||||||
@@ -35,50 +37,234 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import * as THREE from 'three';
|
|
||||||
import ShadingSandbox from './ShadingSandbox.vue';
|
import ShadingSandbox from './ShadingSandbox.vue';
|
||||||
|
|
||||||
export default {
|
// Default render settings - used everywhere
|
||||||
name: 'App',
|
const DEFAULT_RENDER_SETTINGS = {
|
||||||
components: {
|
azimuth: 90,
|
||||||
ShadingSandbox
|
altitude: 60,
|
||||||
},
|
intensity: 1.2,
|
||||||
setup() {
|
heightScale: 3,
|
||||||
|
terrainColor: 0x9A9996
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refs
|
||||||
const mapContainer = ref(null);
|
const mapContainer = ref(null);
|
||||||
const threeCanvas = ref(null);
|
const sandboxRef = ref(null);
|
||||||
const ShadingSandbox = ref(null);
|
const sandboxVisible = ref(false);
|
||||||
let map = null;
|
const sandboxOffscreen = ref(false);
|
||||||
let scene = null;
|
|
||||||
let camera = null;
|
|
||||||
let renderer = null;
|
|
||||||
let lidarMeshes = [];
|
|
||||||
const baseLayer = ref('osm');
|
const baseLayer = ref('osm');
|
||||||
const showOctagon = ref(true);
|
const showOctagon = ref(true);
|
||||||
const showLidar = ref(true);
|
const showLidar = ref(true);
|
||||||
|
|
||||||
const sandboxVisible = ref(false);
|
|
||||||
const currentTileData = ref(null);
|
|
||||||
const tileCache = ref({});
|
const tileCache = ref({});
|
||||||
|
const currentTileData = ref(null);
|
||||||
|
|
||||||
// Newark Octagon coordinates (converted from DMS to decimal)
|
// Map instance
|
||||||
|
let map = null;
|
||||||
|
|
||||||
|
// Tile names to load
|
||||||
|
const TILE_NAMES = [
|
||||||
|
'BS19820747',
|
||||||
|
'BS19820748',
|
||||||
|
'BS19830747',
|
||||||
|
'BS19830748'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Newark Octagon coordinates
|
||||||
const octagonCoords = [
|
const octagonCoords = [
|
||||||
[-82.44133, 40.05431], // 40°03'15.5"N 82°26'28.8"W
|
[-82.44133, 40.05431],
|
||||||
[-82.44264, 40.05311], // 40°03'11.2"N 82°26'33.5"W
|
[-82.44264, 40.05311],
|
||||||
[-82.44464, 40.05242], // 40°03'08.7"N 82°26'40.7"W
|
[-82.44464, 40.05242],
|
||||||
[-82.44631, 40.05342], // 40°03'12.3"N 82°26'46.7"W
|
[-82.44631, 40.05342],
|
||||||
[-82.44728, 40.05500], // 40°03'18.0"N 82°26'50.2"W
|
[-82.44728, 40.05500],
|
||||||
[-82.44589, 40.05633], // 40°03'22.8"N 82°26'45.2"W
|
[-82.44589, 40.05633],
|
||||||
[-82.44389, 40.05697], // 40°03'25.1"N 82°26'38.0"W
|
[-82.44389, 40.05697],
|
||||||
[-82.44192, 40.05589], // 40°03'21.2"N 82°26'30.9"W
|
[-82.44192, 40.05589],
|
||||||
[-82.44133, 40.05431], // Close the polygon
|
[-82.44133, 40.05431], // Close the polygon
|
||||||
];
|
];
|
||||||
|
|
||||||
const octagonCenter = [-82.44383, 40.05469];
|
const octagonCenter = [-82.44383, 40.05469];
|
||||||
|
|
||||||
|
// Coordinate conversion utilities
|
||||||
|
function webMercatorToLonLat(x, y) {
|
||||||
|
const R = 6378137; // Earth's radius in meters
|
||||||
|
const lon = (x / R) * (180 / Math.PI);
|
||||||
|
const lat = (2 * Math.atan(Math.exp(y / R)) - Math.PI / 2) * (180 / Math.PI);
|
||||||
|
return [lon, lat];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse .mound binary file
|
||||||
|
async function parseMoundFile(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 bytes
|
||||||
|
|
||||||
|
const positions = new Float32Array(buffer, offset, pointCount * 3);
|
||||||
|
offset += pointCount * 3 * 4;
|
||||||
|
|
||||||
|
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
pointCount,
|
||||||
|
triangleCount,
|
||||||
|
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
|
||||||
|
positions,
|
||||||
|
indices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and render all lidar tiles
|
||||||
|
async function loadLidarTiles() {
|
||||||
|
console.log('Loading and rendering tiles...');
|
||||||
|
|
||||||
|
for (const tileName of TILE_NAMES) {
|
||||||
|
try {
|
||||||
|
console.log(`Loading ${tileName}...`);
|
||||||
|
const data = await parseMoundFile(`/tiles/${tileName}.mound`);
|
||||||
|
|
||||||
|
// Cache tile data
|
||||||
|
tileCache.value[tileName] = data;
|
||||||
|
|
||||||
|
// Render using ShadingSandbox
|
||||||
|
if (sandboxRef.value) {
|
||||||
|
const result = await sandboxRef.value.renderTileWithSettings(
|
||||||
|
data,
|
||||||
|
DEFAULT_RENDER_SETTINGS,
|
||||||
|
512
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`Rendered ${tileName} in ${result.renderTime}ms (${result.width}x${result.height})`);
|
||||||
|
|
||||||
|
// Convert bounds to lat/lon
|
||||||
|
const sw = webMercatorToLonLat(data.bounds.minX, data.bounds.minY);
|
||||||
|
const ne = webMercatorToLonLat(data.bounds.maxX, data.bounds.maxY);
|
||||||
|
|
||||||
|
// Add image source to map
|
||||||
|
map.addSource(`tile-${tileName}`, {
|
||||||
|
type: 'image',
|
||||||
|
url: result.dataURL,
|
||||||
|
coordinates: [
|
||||||
|
[sw[0], ne[1]], // top-left
|
||||||
|
[ne[0], ne[1]], // top-right
|
||||||
|
[ne[0], sw[1]], // bottom-right
|
||||||
|
[sw[0], sw[1]] // bottom-left
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add layer
|
||||||
|
map.addLayer({
|
||||||
|
id: `tile-layer-${tileName}`,
|
||||||
|
type: 'raster',
|
||||||
|
source: `tile-${tileName}`,
|
||||||
|
paint: {
|
||||||
|
'raster-opacity': 0.8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Added ${tileName} to map at [${sw[0].toFixed(5)}, ${sw[1].toFixed(5)}] - [${ne[0].toFixed(5)}, ${ne[1].toFixed(5)}]`);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to render ${tileName}:`, result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to load ${tileName}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All tiles loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI handlers
|
||||||
|
function updateBaseLayer() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
if (baseLayer.value === 'osm') {
|
||||||
|
map.setLayoutProperty('osm-layer', 'visibility', 'visible');
|
||||||
|
map.setLayoutProperty('satellite-layer', 'visibility', 'none');
|
||||||
|
} else {
|
||||||
|
map.setLayoutProperty('osm-layer', 'visibility', 'none');
|
||||||
|
map.setLayoutProperty('satellite-layer', 'visibility', 'visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOctagon() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const visibility = showOctagon.value ? 'visible' : 'none';
|
||||||
|
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
|
||||||
|
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLidar() {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const visibility = showLidar.value ? 'visible' : 'none';
|
||||||
|
for (const tileName of TILE_NAMES) {
|
||||||
|
const layerId = `tile-layer-${tileName}`;
|
||||||
|
if (map.getLayer(layerId)) {
|
||||||
|
map.setLayoutProperty(layerId, 'visibility', visibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSandbox() {
|
||||||
|
const firstTile = Object.keys(tileCache.value)[0];
|
||||||
|
if (firstTile) {
|
||||||
|
currentTileData.value = tileCache.value[firstTile];
|
||||||
|
sandboxVisible.value = true;
|
||||||
|
|
||||||
|
// Load tile data after sandbox mounts
|
||||||
|
setTimeout(() => {
|
||||||
|
if (sandboxRef.value) {
|
||||||
|
sandboxRef.value.loadTileData(currentTileData.value);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRenderComplete(data) {
|
||||||
|
console.log('Render complete:', {
|
||||||
|
size: `${data.width}x${data.height}`,
|
||||||
|
renderTime: data.renderTime,
|
||||||
|
settings: data.settings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRenderError(err) {
|
||||||
|
console.error('Renderer error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
map = new maplibregl.Map({
|
map = new maplibregl.Map({
|
||||||
container: mapContainer.value,
|
container: mapContainer.value,
|
||||||
@@ -97,7 +283,7 @@ export default {
|
|||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
||||||
],
|
],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
attribution: 'Esri, Maxar, Earthstar Geographics, USDA FSA, USGS, Aerogrid, IGN, IGP, and the GIS User Community'
|
attribution: 'Esri, Maxar, Earthstar Geographics'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
@@ -119,8 +305,8 @@ export default {
|
|||||||
zoom: 15
|
zoom: 15
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('load', () => {
|
map.on('load', async () => {
|
||||||
// Add octagon source and layer
|
// Add octagon overlay
|
||||||
map.addSource('octagon', {
|
map.addSource('octagon', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: {
|
data: {
|
||||||
@@ -152,249 +338,26 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Three.js
|
// Initialize sandbox offscreen for tile rendering
|
||||||
initThreeJS();
|
sandboxOffscreen.value = true;
|
||||||
loadLidarTiles();
|
|
||||||
|
|
||||||
// Update Three.js on map move
|
|
||||||
map.on('move', updateThreeCamera);
|
|
||||||
map.on('zoom', updateThreeCamera);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateBaseLayer = () => {
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
if (baseLayer.value === 'osm') {
|
|
||||||
map.setLayoutProperty('osm-layer', 'visibility', 'visible');
|
|
||||||
map.setLayoutProperty('satellite-layer', 'visibility', 'none');
|
|
||||||
} else {
|
|
||||||
map.setLayoutProperty('osm-layer', 'visibility', 'none');
|
|
||||||
map.setLayoutProperty('satellite-layer', 'visibility', 'visible');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleOctagon = () => {
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
const visibility = showOctagon.value ? 'visible' : 'none';
|
|
||||||
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
|
|
||||||
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleLidar = () => {
|
|
||||||
lidarMeshes.forEach(mesh => {
|
|
||||||
mesh.visible = showLidar.value;
|
|
||||||
});
|
|
||||||
if (renderer) renderer.render(scene, camera);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert lat/lon to Web Mercator meters (EPSG:3857)
|
|
||||||
const lonLatToWebMercator = (lon, lat) => {
|
|
||||||
const x = lon * 20037508.34 / 180;
|
|
||||||
let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
|
|
||||||
y = y * 20037508.34 / 180;
|
|
||||||
return { x, y };
|
|
||||||
};
|
|
||||||
|
|
||||||
const initThreeJS = () => {
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
|
|
||||||
camera = new THREE.OrthographicCamera(
|
|
||||||
-1, 1, 1, -1, 0.1, 10000
|
|
||||||
);
|
|
||||||
camera.position.z = 1;
|
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({
|
|
||||||
canvas: threeCanvas.value,
|
|
||||||
alpha: true,
|
|
||||||
antialias: true
|
|
||||||
});
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
renderer.setClearColor(0x000000, 0);
|
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
|
||||||
scene.add(ambientLight);
|
|
||||||
|
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
||||||
directionalLight.position.set(1, 1, 1);
|
|
||||||
scene.add(directionalLight);
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
updateThreeCamera();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
if (!renderer || !camera) return;
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
updateThreeCamera();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateThreeCamera = () => {
|
|
||||||
if (!map || !camera || !renderer) return;
|
|
||||||
|
|
||||||
const bounds = map.getBounds();
|
|
||||||
const ne = bounds.getNorthEast();
|
|
||||||
const sw = bounds.getSouthWest();
|
|
||||||
|
|
||||||
// Convert lat/lon bounds to Web Mercator meters
|
|
||||||
const neMerc = lonLatToWebMercator(ne.lng, ne.lat);
|
|
||||||
const swMerc = lonLatToWebMercator(sw.lng, sw.lat);
|
|
||||||
|
|
||||||
camera.left = swMerc.x;
|
|
||||||
camera.right = neMerc.x;
|
|
||||||
camera.top = neMerc.y;
|
|
||||||
camera.bottom = swMerc.y;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 loadLidarTiles = async () => {
|
|
||||||
const tiles = [
|
|
||||||
'BS19820747',
|
|
||||||
'BS19820748',
|
|
||||||
'BS19830747',
|
|
||||||
'BS19830748'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const tileName of tiles) {
|
|
||||||
try {
|
|
||||||
console.log(`Loading ${tileName}...`);
|
|
||||||
const data = await parseMoundFile(`/tiles/${tileName}.mound`);
|
|
||||||
|
|
||||||
// Cache the tile data for sandbox use
|
|
||||||
tileCache.value[tileName] = data;
|
|
||||||
|
|
||||||
console.log(`${tileName} bounds:`, data.bounds);
|
|
||||||
console.log(`First few positions:`, data.positions.slice(0, 15));
|
|
||||||
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
|
||||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.positions, 3));
|
|
||||||
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
|
|
||||||
geometry.computeVertexNormals();
|
|
||||||
|
|
||||||
const material = new THREE.MeshLambertMaterial({
|
|
||||||
color: 0x8B7355,
|
|
||||||
side: THREE.DoubleSide,
|
|
||||||
wireframe: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(geometry, material);
|
|
||||||
scene.add(mesh);
|
|
||||||
lidarMeshes.push(mesh);
|
|
||||||
|
|
||||||
console.log(`Loaded ${tileName}: ${data.pointCount} points, ${data.triangleCount} triangles`);
|
|
||||||
console.log(`Mesh position:`, mesh.position);
|
|
||||||
console.log(`Mesh in scene:`, scene.children.length);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to load ${tileName}:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Map bounds:', map.getBounds().toArray());
|
|
||||||
console.log('Camera:', camera);
|
|
||||||
|
|
||||||
updateThreeCamera();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSandbox = () => {
|
|
||||||
// Open sandbox with first available tile
|
|
||||||
const firstTile = Object.keys(tileCache.value)[0];
|
|
||||||
if (firstTile) {
|
|
||||||
currentTileData.value = tileCache.value[firstTile];
|
|
||||||
sandboxVisible.value = true;
|
sandboxVisible.value = true;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Load tile data into renderer after it mounts
|
// Load and render tiles
|
||||||
setTimeout(() => {
|
await loadLidarTiles();
|
||||||
if (ShadingSandbox.value) {
|
|
||||||
ShadingSandbox.value.loadTileData(currentTileData.value);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRenderComplete = (data) => {
|
// Hide sandbox
|
||||||
console.log('Render complete:', {
|
sandboxVisible.value = false;
|
||||||
size: data.size,
|
sandboxOffscreen.value = false;
|
||||||
renderTime: data.renderTime,
|
});
|
||||||
settings: data.settings
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const onRenderError = (err) => {
|
|
||||||
console.error('Renderer error:', err);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize);
|
if (map) {
|
||||||
if (renderer) {
|
map.remove();
|
||||||
renderer.dispose();
|
map = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
mapContainer,
|
|
||||||
threeCanvas,
|
|
||||||
ShadingSandbox,
|
|
||||||
baseLayer,
|
|
||||||
showOctagon,
|
|
||||||
showLidar,
|
|
||||||
updateBaseLayer,
|
|
||||||
toggleOctagon,
|
|
||||||
toggleLidar,
|
|
||||||
sandboxVisible,
|
|
||||||
currentTileData,
|
|
||||||
openSandbox,
|
|
||||||
onRenderComplete,
|
|
||||||
onRenderError
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -415,15 +378,6 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.three-canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-controls {
|
.layer-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="modal-container">
|
<div v-if="visible" :class="['modal-container', { 'offscreen': offscreen }]">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Shading Sandbox</h2>
|
<h2>Shading Sandbox</h2>
|
||||||
<button class="close-btn" @click="close">✕</button>
|
<button class="close-btn" @click="close">✕</button>
|
||||||
@@ -143,6 +143,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
offscreen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
initialSettings: {
|
initialSettings: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
@@ -249,22 +253,12 @@ const handleResize = () => {
|
|||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
||||||
// Maintain tile aspect ratio in frustum
|
// Use exact normalized terrain bounds (no padding = no black bars)
|
||||||
if (geometryCache && geometryCache.tileAspect) {
|
if (geometryCache) {
|
||||||
const viewSize = 6;
|
camera.left = -geometryCache.normalizedSpanX / 2;
|
||||||
const tileAspect = geometryCache.tileAspect;
|
camera.right = geometryCache.normalizedSpanX / 2;
|
||||||
|
camera.top = geometryCache.normalizedSpanY / 2;
|
||||||
if (tileAspect > 1) {
|
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
||||||
camera.left = -viewSize * tileAspect;
|
|
||||||
camera.right = viewSize * tileAspect;
|
|
||||||
camera.top = viewSize;
|
|
||||||
camera.bottom = -viewSize;
|
|
||||||
} else {
|
|
||||||
camera.left = -viewSize;
|
|
||||||
camera.right = viewSize;
|
|
||||||
camera.top = viewSize / tileAspect;
|
|
||||||
camera.bottom = -viewSize / tileAspect;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback: square frustum if no tile loaded yet
|
// Fallback: square frustum if no tile loaded yet
|
||||||
const viewSize = 6;
|
const viewSize = 6;
|
||||||
@@ -344,12 +338,20 @@ const loadTileData = (tileData) => {
|
|||||||
baseZ[i + 2] = transformedPositions[i + 2]; // Only Z values
|
baseZ[i + 2] = transformedPositions[i + 2]; // Only Z values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate exact bounds of normalized terrain (no padding = no black bars)
|
||||||
|
const normalizedSpanX = spanX * normalizeScale;
|
||||||
|
const normalizedSpanY = spanY * normalizeScale;
|
||||||
|
|
||||||
geometryCache = {
|
geometryCache = {
|
||||||
geometry,
|
geometry,
|
||||||
baseZ,
|
baseZ,
|
||||||
spanZ,
|
spanZ,
|
||||||
zScale,
|
zScale,
|
||||||
tileAspect
|
tileAspect,
|
||||||
|
normalizedSpanX,
|
||||||
|
normalizedSpanY,
|
||||||
|
// Store original Web Mercator bounds for MapLibre positioning
|
||||||
|
originalBounds: { ...tileData.bounds }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create material and mesh
|
// Create material and mesh
|
||||||
@@ -362,22 +364,10 @@ const loadTileData = (tileData) => {
|
|||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
|
|
||||||
// Configure camera frustum to match tile aspect ratio
|
// Configure camera frustum to match tile aspect ratio
|
||||||
const viewSize = 6; // Base size for the view
|
camera.left = -normalizedSpanX / 2;
|
||||||
|
camera.right = normalizedSpanX / 2;
|
||||||
// Adjust frustum based on tile aspect ratio
|
camera.top = normalizedSpanY / 2;
|
||||||
if (tileAspect > 1) {
|
camera.bottom = -normalizedSpanY / 2;
|
||||||
// Wider than tall
|
|
||||||
camera.left = -viewSize * tileAspect;
|
|
||||||
camera.right = viewSize * tileAspect;
|
|
||||||
camera.top = viewSize;
|
|
||||||
camera.bottom = -viewSize;
|
|
||||||
} else {
|
|
||||||
// Taller than wide
|
|
||||||
camera.left = -viewSize;
|
|
||||||
camera.right = viewSize;
|
|
||||||
camera.top = viewSize / tileAspect;
|
|
||||||
camera.bottom = -viewSize / tileAspect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust near/far to accommodate height exaggeration
|
// Adjust near/far to accommodate height exaggeration
|
||||||
const maxZExtent = (spanZ * zScale / 2) * 20; // Max exaggeration
|
const maxZExtent = (spanZ * zScale / 2) * 20; // Max exaggeration
|
||||||
@@ -469,17 +459,29 @@ const renderTile = async () => {
|
|||||||
const originalHeight = renderer.domElement.height;
|
const originalHeight = renderer.domElement.height;
|
||||||
const originalPixelRatio = renderer.getPixelRatio();
|
const originalPixelRatio = renderer.getPixelRatio();
|
||||||
|
|
||||||
// Set render size (square)
|
// Calculate render dimensions based on tile aspect ratio
|
||||||
renderer.setSize(size, size);
|
let renderWidth = size;
|
||||||
|
let renderHeight = size;
|
||||||
|
if (geometryCache) {
|
||||||
|
const tileAspect = geometryCache.normalizedSpanX / geometryCache.normalizedSpanY;
|
||||||
|
if (tileAspect > 1) {
|
||||||
|
renderHeight = Math.round(size / tileAspect);
|
||||||
|
} else {
|
||||||
|
renderWidth = Math.round(size * tileAspect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set render size
|
||||||
|
renderer.setSize(renderWidth, renderHeight);
|
||||||
renderer.setPixelRatio(1); // No pixel ratio multiplier for export
|
renderer.setPixelRatio(1); // No pixel ratio multiplier for export
|
||||||
|
|
||||||
// Update camera aspect for square
|
// Update camera to match exact tile bounds (no black bars)
|
||||||
const aspect = 1;
|
if (geometryCache) {
|
||||||
const viewSize = 6;
|
camera.left = -geometryCache.normalizedSpanX / 2;
|
||||||
camera.left = -viewSize * aspect;
|
camera.right = geometryCache.normalizedSpanX / 2;
|
||||||
camera.right = viewSize * aspect;
|
camera.top = geometryCache.normalizedSpanY / 2;
|
||||||
camera.top = viewSize;
|
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
||||||
camera.bottom = -viewSize;
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
@@ -493,13 +495,8 @@ const renderTile = async () => {
|
|||||||
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
||||||
renderer.setPixelRatio(originalPixelRatio);
|
renderer.setPixelRatio(originalPixelRatio);
|
||||||
|
|
||||||
// Restore camera aspect
|
// Restore camera
|
||||||
const canvasAspect = originalWidth / originalHeight;
|
handleResize();
|
||||||
camera.left = -viewSize * canvasAspect;
|
|
||||||
camera.right = viewSize * canvasAspect;
|
|
||||||
camera.top = viewSize;
|
|
||||||
camera.bottom = -viewSize;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
const renderTime = Math.round(endTime - startTime);
|
const renderTime = Math.round(endTime - startTime);
|
||||||
@@ -510,7 +507,9 @@ const renderTile = async () => {
|
|||||||
dataURL,
|
dataURL,
|
||||||
settings: { ...settings },
|
settings: { ...settings },
|
||||||
size,
|
size,
|
||||||
renderTime
|
renderTime,
|
||||||
|
width: renderWidth,
|
||||||
|
height: renderHeight
|
||||||
});
|
});
|
||||||
|
|
||||||
return dataURL;
|
return dataURL;
|
||||||
@@ -556,16 +555,29 @@ const renderTileWithSettings = async (tileData, renderSettings, resolution = 102
|
|||||||
const originalHeight = renderer.domElement.height;
|
const originalHeight = renderer.domElement.height;
|
||||||
const originalPixelRatio = renderer.getPixelRatio();
|
const originalPixelRatio = renderer.getPixelRatio();
|
||||||
|
|
||||||
// Set render size (square for export)
|
// Calculate render dimensions based on tile aspect ratio
|
||||||
renderer.setSize(resolution, resolution);
|
let renderWidth = resolution;
|
||||||
|
let renderHeight = resolution;
|
||||||
|
if (geometryCache) {
|
||||||
|
const tileAspect = geometryCache.normalizedSpanX / geometryCache.normalizedSpanY;
|
||||||
|
if (tileAspect > 1) {
|
||||||
|
renderHeight = Math.round(resolution / tileAspect);
|
||||||
|
} else {
|
||||||
|
renderWidth = Math.round(resolution * tileAspect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set render size
|
||||||
|
renderer.setSize(renderWidth, renderHeight);
|
||||||
renderer.setPixelRatio(1);
|
renderer.setPixelRatio(1);
|
||||||
|
|
||||||
// Update camera for square render output
|
// Update camera to match exact tile bounds (no black bars)
|
||||||
const viewSize = 6;
|
if (geometryCache) {
|
||||||
camera.left = -viewSize;
|
camera.left = -geometryCache.normalizedSpanX / 2;
|
||||||
camera.right = viewSize;
|
camera.right = geometryCache.normalizedSpanX / 2;
|
||||||
camera.top = viewSize;
|
camera.top = geometryCache.normalizedSpanY / 2;
|
||||||
camera.bottom = -viewSize;
|
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
||||||
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
@@ -578,27 +590,12 @@ const renderTileWithSettings = async (tileData, renderSettings, resolution = 102
|
|||||||
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
renderer.setSize(originalWidth / originalPixelRatio, originalHeight / originalPixelRatio);
|
||||||
renderer.setPixelRatio(originalPixelRatio);
|
renderer.setPixelRatio(originalPixelRatio);
|
||||||
|
|
||||||
// Restore camera with tile aspect ratio
|
// Restore camera with exact normalized bounds
|
||||||
if (geometryCache && geometryCache.tileAspect) {
|
if (geometryCache) {
|
||||||
const tileAspect = geometryCache.tileAspect;
|
camera.left = -geometryCache.normalizedSpanX / 2;
|
||||||
|
camera.right = geometryCache.normalizedSpanX / 2;
|
||||||
if (tileAspect > 1) {
|
camera.top = geometryCache.normalizedSpanY / 2;
|
||||||
camera.left = -viewSize * tileAspect;
|
camera.bottom = -geometryCache.normalizedSpanY / 2;
|
||||||
camera.right = viewSize * tileAspect;
|
|
||||||
camera.top = viewSize;
|
|
||||||
camera.bottom = -viewSize;
|
|
||||||
} else {
|
|
||||||
camera.left = -viewSize;
|
|
||||||
camera.right = viewSize;
|
|
||||||
camera.top = viewSize / tileAspect;
|
|
||||||
camera.bottom = -viewSize / tileAspect;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: square frustum
|
|
||||||
camera.left = -viewSize;
|
|
||||||
camera.right = viewSize;
|
|
||||||
camera.top = viewSize;
|
|
||||||
camera.bottom = -viewSize;
|
|
||||||
}
|
}
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
@@ -609,7 +606,9 @@ const renderTileWithSettings = async (tileData, renderSettings, resolution = 102
|
|||||||
success: true,
|
success: true,
|
||||||
dataURL,
|
dataURL,
|
||||||
settings: { ...settings },
|
settings: { ...settings },
|
||||||
renderTime
|
renderTime,
|
||||||
|
width: renderWidth,
|
||||||
|
height: renderHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -700,7 +699,8 @@ defineExpose({
|
|||||||
updateHeightScale();
|
updateHeightScale();
|
||||||
},
|
},
|
||||||
getSettings: () => ({ ...settings }),
|
getSettings: () => ({ ...settings }),
|
||||||
isReady: () => tileLoaded.value
|
isReady: () => tileLoaded.value,
|
||||||
|
getBounds: () => geometryCache?.originalBounds ?? null
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -720,6 +720,11 @@ defineExpose({
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-container.offscreen {
|
||||||
|
left: -10000px;
|
||||||
|
top: -10000px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in New Issue
Block a user