|
|
@@ -1,6 +1,3 @@
|
|
|
-import * as THREE from 'three';
|
|
|
-import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
-
|
|
|
const parseResponse = async (r) => {
|
|
|
const contentType = r.headers.get('content-type') || '';
|
|
|
if (contentType.includes('application/json')) {
|
|
|
@@ -42,15 +39,10 @@ const API = {
|
|
|
};
|
|
|
|
|
|
let currentGroup = null;
|
|
|
-let scene, camera, renderer, sphere, controls;
|
|
|
-let raycaster = new THREE.Raycaster();
|
|
|
-let mouse = new THREE.Vector2();
|
|
|
-let markers = [];
|
|
|
-let tempMarker = null;
|
|
|
-let previewSprites = [];
|
|
|
-let tempPreview = null;
|
|
|
let scanPollInterval = null;
|
|
|
let controlsGloballyDisabled = false;
|
|
|
+let tempPreview = null;
|
|
|
+let selectedSampleEl = null;
|
|
|
|
|
|
function log(msg) {
|
|
|
const panel = document.getElementById('log-panel');
|
|
|
@@ -105,152 +97,252 @@ function escapeHtml(text) {
|
|
|
return div.innerHTML;
|
|
|
}
|
|
|
|
|
|
-function initThree() {
|
|
|
- const container = document.getElementById('panorama-container');
|
|
|
- scene = new THREE.Scene();
|
|
|
- camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
|
|
- camera.position.set(0, 0, 0.1);
|
|
|
- renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
- renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
- container.appendChild(renderer.domElement);
|
|
|
-
|
|
|
- controls = new OrbitControls(camera, renderer.domElement);
|
|
|
- controls.enableZoom = true;
|
|
|
- controls.enablePan = false;
|
|
|
- controls.rotateSpeed = -0.3;
|
|
|
- controls.minPolarAngle = 0.1;
|
|
|
- controls.maxPolarAngle = Math.PI - 0.1;
|
|
|
-
|
|
|
- renderer.domElement.addEventListener('click', onPanoramaClick);
|
|
|
- window.addEventListener('resize', onResize);
|
|
|
- animate();
|
|
|
+function setSelectedPreview(url) {
|
|
|
+ const img = document.getElementById('selected-preview');
|
|
|
+ if (url) {
|
|
|
+ img.src = url;
|
|
|
+ img.style.display = 'block';
|
|
|
+ } else {
|
|
|
+ img.src = '';
|
|
|
+ img.style.display = 'none';
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function onResize() {
|
|
|
- const container = document.getElementById('panorama-container');
|
|
|
- camera.aspect = container.clientWidth / container.clientHeight;
|
|
|
- camera.updateProjectionMatrix();
|
|
|
- renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
+function selectSample(sample) {
|
|
|
+ document.getElementById('inp-pan').value = sample.pan.toFixed(2);
|
|
|
+ document.getElementById('inp-tilt').value = sample.tilt.toFixed(2);
|
|
|
+ document.getElementById('inp-zoom').value = sample.zoom;
|
|
|
+ tempPreview = { path: sample.thumbnail };
|
|
|
+ setSelectedPreview(`/api/sample-image?path=${encodeURIComponent(sample.thumbnail)}`);
|
|
|
+ if (sampleCanvas) sampleCanvas.setSelected(sample.pan, sample.tilt);
|
|
|
}
|
|
|
|
|
|
-function loadPanorama(groupId) {
|
|
|
- clearMarkers();
|
|
|
- const loader = new THREE.TextureLoader();
|
|
|
- loader.load(`/api/panorama/${groupId}`,
|
|
|
- (texture) => {
|
|
|
- if (sphere) scene.remove(sphere);
|
|
|
- const geo = new THREE.SphereGeometry(50, 60, 40);
|
|
|
- geo.scale(-1, 1, 1);
|
|
|
- const mat = new THREE.MeshBasicMaterial({ map: texture });
|
|
|
- sphere = new THREE.Mesh(geo, mat);
|
|
|
- scene.add(sphere);
|
|
|
- log(`加载全景图: ${groupId}`);
|
|
|
- loadPoints(groupId);
|
|
|
- },
|
|
|
- undefined,
|
|
|
- () => {
|
|
|
- log('全景图未就绪,请先执行扫描');
|
|
|
- }
|
|
|
- );
|
|
|
-}
|
|
|
+class SampleCanvas {
|
|
|
+ constructor(canvasId, wrapperId) {
|
|
|
+ this.canvas = document.getElementById(canvasId);
|
|
|
+ this.wrapper = document.getElementById(wrapperId);
|
|
|
+ this.ctx = this.canvas.getContext('2d');
|
|
|
+ this.samples = [];
|
|
|
+ this.pans = [];
|
|
|
+ this.tilts = [];
|
|
|
+ this.sampleMap = new Map();
|
|
|
+ this.images = new Map();
|
|
|
+ this.cellW = 160;
|
|
|
+ this.cellH = 120;
|
|
|
+ this.captionH = 20;
|
|
|
+ this.scale = 1;
|
|
|
+ this.offsetX = 0;
|
|
|
+ this.offsetY = 0;
|
|
|
+ this.selectedPan = null;
|
|
|
+ this.selectedTilt = null;
|
|
|
+ this.isDragging = false;
|
|
|
+ this.dragStart = { x: 0, y: 0, ox: 0, oy: 0 };
|
|
|
+ this.pendingDraw = false;
|
|
|
+
|
|
|
+ this.resize();
|
|
|
+ window.addEventListener('resize', () => this.resize());
|
|
|
+ this.setupEvents();
|
|
|
+ }
|
|
|
|
|
|
-function clearMarkers() {
|
|
|
- markers.forEach(m => scene.remove(m));
|
|
|
- markers = [];
|
|
|
-}
|
|
|
+ resize() {
|
|
|
+ const rect = this.wrapper.getBoundingClientRect();
|
|
|
+ this.canvas.width = rect.width;
|
|
|
+ this.canvas.height = rect.height;
|
|
|
+ this.draw();
|
|
|
+ }
|
|
|
|
|
|
-function clearTempMarker() {
|
|
|
- if (tempMarker) {
|
|
|
- scene.remove(tempMarker);
|
|
|
- tempMarker = null;
|
|
|
+ setSamples(samples) {
|
|
|
+ this.samples = samples || [];
|
|
|
+ this.pans = Array.from(new Set(this.samples.map(s => s.pan))).sort((a, b) => a - b);
|
|
|
+ this.tilts = Array.from(new Set(this.samples.map(s => s.tilt))).sort((a, b) => b - a);
|
|
|
+ this.sampleMap = new Map();
|
|
|
+ this.samples.forEach(s => this.sampleMap.set(`${s.pan},${s.tilt}`, s));
|
|
|
+ this.images = new Map();
|
|
|
+ this.samples.forEach(s => {
|
|
|
+ const img = new Image();
|
|
|
+ img.crossOrigin = 'anonymous';
|
|
|
+ img.src = `/api/sample-image?path=${encodeURIComponent(s.thumbnail)}`;
|
|
|
+ img.onload = () => this.draw();
|
|
|
+ this.images.set(`${s.pan},${s.tilt}`, img);
|
|
|
+ });
|
|
|
+ this.fitToView();
|
|
|
+ this.draw();
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-function setTempMarker(pan, tilt) {
|
|
|
- clearTempMarker();
|
|
|
- tempMarker = addMarker(pan, tilt, 0x3b82f6);
|
|
|
-}
|
|
|
+ contentWidth() {
|
|
|
+ return this.pans.length * this.cellW;
|
|
|
+ }
|
|
|
|
|
|
-function clearPreviewSprites() {
|
|
|
- previewSprites.forEach(s => scene.remove(s));
|
|
|
- previewSprites = [];
|
|
|
-}
|
|
|
+ contentHeight() {
|
|
|
+ return this.tilts.length * (this.cellH + this.captionH);
|
|
|
+ }
|
|
|
|
|
|
-function addPreviewPlane(pan, tilt, imageUrl) {
|
|
|
- const loader = new THREE.TextureLoader();
|
|
|
- const mesh = new THREE.Mesh(
|
|
|
- new THREE.PlaneGeometry(1, 1),
|
|
|
- new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide, transparent: true, opacity: 0.95 })
|
|
|
- );
|
|
|
- mesh.visible = false;
|
|
|
- scene.add(mesh);
|
|
|
- previewSprites.push(mesh);
|
|
|
-
|
|
|
- loader.load(imageUrl, (texture) => {
|
|
|
- const imageWidth = texture.image.width || 100;
|
|
|
- const imageHeight = texture.image.height || 100;
|
|
|
- const aspect = imageWidth / imageHeight;
|
|
|
- const baseSize = 5;
|
|
|
- mesh.geometry.dispose();
|
|
|
- mesh.geometry = new THREE.PlaneGeometry(baseSize * aspect, baseSize);
|
|
|
- mesh.material.map = texture;
|
|
|
- mesh.material.needsUpdate = true;
|
|
|
-
|
|
|
- const r = 46;
|
|
|
- const phi = (90 - tilt) * Math.PI / 180;
|
|
|
- const theta = (pan - 90) * Math.PI / 180;
|
|
|
- const x = r * Math.sin(phi) * Math.cos(theta);
|
|
|
- const y = r * Math.cos(phi);
|
|
|
- const z = r * Math.sin(phi) * Math.sin(theta);
|
|
|
- mesh.position.set(x, y, z);
|
|
|
- // 让平面法线朝外(贴合球面)
|
|
|
- mesh.lookAt(x * 2, y * 2, z * 2);
|
|
|
- mesh.visible = true;
|
|
|
- mesh.userData = { type: 'preview', pan, tilt, imageUrl };
|
|
|
- }, undefined, (err) => {
|
|
|
- log(`加载预览图失败: ${err.message || err}`);
|
|
|
- });
|
|
|
+ fitToView() {
|
|
|
+ const pw = this.canvas.width / this.contentWidth();
|
|
|
+ const ph = this.canvas.height / this.contentHeight();
|
|
|
+ this.scale = Math.min(pw, ph, 1);
|
|
|
+ this.offsetX = (this.canvas.width - this.contentWidth() * this.scale) / 2;
|
|
|
+ this.offsetY = (this.canvas.height - this.contentHeight() * this.scale) / 2;
|
|
|
+ this.updateZoomLabel();
|
|
|
+ }
|
|
|
|
|
|
- return mesh;
|
|
|
-}
|
|
|
+ resetView() {
|
|
|
+ this.fitToView();
|
|
|
+ this.draw();
|
|
|
+ }
|
|
|
|
|
|
-function addMarker(pan, tilt, color = 0x22c55e) {
|
|
|
- const r = 49.5;
|
|
|
- const phi = (90 - tilt) * Math.PI / 180;
|
|
|
- const theta = (pan - 90) * Math.PI / 180;
|
|
|
- const x = r * Math.sin(phi) * Math.cos(theta);
|
|
|
- const y = r * Math.cos(phi);
|
|
|
- const z = r * Math.sin(phi) * Math.sin(theta);
|
|
|
- const geo = new THREE.SphereGeometry(0.4, 16, 16);
|
|
|
- const mat = new THREE.MeshBasicMaterial({ color });
|
|
|
- const mesh = new THREE.Mesh(geo, mat);
|
|
|
- mesh.position.set(x, y, z);
|
|
|
- scene.add(mesh);
|
|
|
- markers.push(mesh);
|
|
|
- return mesh;
|
|
|
-}
|
|
|
+ updateZoomLabel() {
|
|
|
+ const label = document.getElementById('zoom-level');
|
|
|
+ if (label) label.textContent = `${Math.round(this.scale * 100)}%`;
|
|
|
+ }
|
|
|
|
|
|
-function onPanoramaClick(event) {
|
|
|
- if (!sphere) return;
|
|
|
- const rect = renderer.domElement.getBoundingClientRect();
|
|
|
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
|
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
|
- raycaster.setFromCamera(mouse, camera);
|
|
|
- const intersects = raycaster.intersectObject(sphere);
|
|
|
- if (intersects.length === 0) return;
|
|
|
- const p = intersects[0].point.clone().normalize();
|
|
|
- const pan = ((Math.atan2(p.x, -p.z) * 180 / Math.PI) + 360) % 360;
|
|
|
- const tilt = Math.asin(Math.max(-1, Math.min(1, p.y))) * 180 / Math.PI;
|
|
|
- document.getElementById('inp-pan').value = pan.toFixed(2);
|
|
|
- document.getElementById('inp-tilt').value = tilt.toFixed(2);
|
|
|
- tempPreview = null;
|
|
|
- setTempMarker(pan, tilt);
|
|
|
+ setupEvents() {
|
|
|
+ this.canvas.addEventListener('wheel', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ const rect = this.canvas.getBoundingClientRect();
|
|
|
+ const mx = e.clientX - rect.left;
|
|
|
+ const my = e.clientY - rect.top;
|
|
|
+ const factor = e.deltaY < 0 ? 1.1 : 0.9;
|
|
|
+ const newScale = Math.max(0.1, Math.min(5.0, this.scale * factor));
|
|
|
+ this.offsetX = mx - (mx - this.offsetX) * (newScale / this.scale);
|
|
|
+ this.offsetY = my - (my - this.offsetY) * (newScale / this.scale);
|
|
|
+ this.scale = newScale;
|
|
|
+ this.updateZoomLabel();
|
|
|
+ this.draw();
|
|
|
+ }, { passive: false });
|
|
|
+
|
|
|
+ this.canvas.addEventListener('mousedown', (e) => {
|
|
|
+ if (e.button !== 0) return;
|
|
|
+ this.isDragging = true;
|
|
|
+ this.dragStart = { x: e.clientX, y: e.clientY, ox: this.offsetX, oy: this.offsetY };
|
|
|
+ this.wrapper.style.cursor = 'grabbing';
|
|
|
+ });
|
|
|
+
|
|
|
+ window.addEventListener('mousemove', (e) => {
|
|
|
+ if (!this.isDragging) return;
|
|
|
+ this.offsetX = this.dragStart.ox + (e.clientX - this.dragStart.x);
|
|
|
+ this.offsetY = this.dragStart.oy + (e.clientY - this.dragStart.y);
|
|
|
+ this.draw();
|
|
|
+ });
|
|
|
+
|
|
|
+ window.addEventListener('mouseup', () => {
|
|
|
+ if (this.isDragging) {
|
|
|
+ this.isDragging = false;
|
|
|
+ this.wrapper.style.cursor = 'grab';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.canvas.addEventListener('click', (e) => {
|
|
|
+ if (this.isDragging) return;
|
|
|
+ const rect = this.canvas.getBoundingClientRect();
|
|
|
+ const mx = e.clientX - rect.left;
|
|
|
+ const my = e.clientY - rect.top;
|
|
|
+ const worldX = (mx - this.offsetX) / this.scale;
|
|
|
+ const worldY = (my - this.offsetY) / this.scale;
|
|
|
+ const col = Math.floor(worldX / this.cellW);
|
|
|
+ const row = Math.floor(worldY / (this.cellH + this.captionH));
|
|
|
+ if (col < 0 || col >= this.pans.length || row < 0 || row >= this.tilts.length) return;
|
|
|
+ const pan = this.pans[col];
|
|
|
+ const tilt = this.tilts[row];
|
|
|
+ const s = this.sampleMap.get(`${pan},${tilt}`);
|
|
|
+ if (s) selectSample(s);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ setSelected(pan, tilt) {
|
|
|
+ this.selectedPan = pan;
|
|
|
+ this.selectedTilt = tilt;
|
|
|
+ this.draw();
|
|
|
+ }
|
|
|
+
|
|
|
+ draw() {
|
|
|
+ if (this.pendingDraw) return;
|
|
|
+ this.pendingDraw = true;
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ this.pendingDraw = false;
|
|
|
+ this._draw();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ _draw() {
|
|
|
+ const ctx = this.ctx;
|
|
|
+ const w = this.canvas.width;
|
|
|
+ const h = this.canvas.height;
|
|
|
+ ctx.clearRect(0, 0, w, h);
|
|
|
+
|
|
|
+ if (this.samples.length === 0) {
|
|
|
+ ctx.fillStyle = '#94a3b8';
|
|
|
+ ctx.font = '14px sans-serif';
|
|
|
+ ctx.fillText('暂无扫描样本,请先执行 360° 扫描', 20, 30);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rowH = this.cellH + this.captionH;
|
|
|
+ const startCol = Math.floor((-this.offsetX / this.scale) / this.cellW);
|
|
|
+ const endCol = Math.ceil((w - this.offsetX) / this.scale / this.cellW);
|
|
|
+ const startRow = Math.floor((-this.offsetY / this.scale) / rowH);
|
|
|
+ const endRow = Math.ceil((h - this.offsetY) / this.scale / rowH);
|
|
|
+
|
|
|
+ ctx.save();
|
|
|
+ ctx.translate(this.offsetX, this.offsetY);
|
|
|
+ ctx.scale(this.scale, this.scale);
|
|
|
+
|
|
|
+ for (let r = Math.max(0, startRow); r <= Math.min(this.tilts.length - 1, endRow); r++) {
|
|
|
+ for (let c = Math.max(0, startCol); c <= Math.min(this.pans.length - 1, endCol); c++) {
|
|
|
+ const pan = this.pans[c];
|
|
|
+ const tilt = this.tilts[r];
|
|
|
+ const x = c * this.cellW;
|
|
|
+ const y = r * rowH;
|
|
|
+ const s = this.sampleMap.get(`${pan},${tilt}`);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#0f172a';
|
|
|
+ ctx.fillRect(x, y, this.cellW, rowH);
|
|
|
+
|
|
|
+ const img = this.images.get(`${pan},${tilt}`);
|
|
|
+ if (img && img.complete && img.naturalWidth) {
|
|
|
+ const sx = 0, sy = 0, sw = img.naturalWidth, sh = img.naturalHeight;
|
|
|
+ const dw = this.cellW - 4;
|
|
|
+ const dh = this.cellH - 4;
|
|
|
+ const scale = Math.min(dw / sw, dh / sh);
|
|
|
+ const iw = sw * scale;
|
|
|
+ const ih = sh * scale;
|
|
|
+ const ix = x + 2 + (dw - iw) / 2;
|
|
|
+ const iy = y + 2 + (dh - ih) / 2;
|
|
|
+ ctx.drawImage(img, ix, iy, iw, ih);
|
|
|
+ } else {
|
|
|
+ ctx.fillStyle = '#1e293b';
|
|
|
+ ctx.fillRect(x + 2, y + 2, this.cellW - 4, this.cellH - 4);
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#cbd5e1';
|
|
|
+ ctx.font = '11px sans-serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText(`P:${pan.toFixed(0)} T:${tilt.toFixed(0)}`, x + this.cellW / 2, y + this.cellH + 14);
|
|
|
+
|
|
|
+ if (this.selectedPan === pan && this.selectedTilt === tilt) {
|
|
|
+ ctx.strokeStyle = '#22c55e';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.strokeRect(x + 1, y + 1, this.cellW - 2, rowH - 2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.restore();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function animate() {
|
|
|
- requestAnimationFrame(animate);
|
|
|
- if (controls) controls.update();
|
|
|
- renderer.render(scene, camera);
|
|
|
+let sampleCanvas = null;
|
|
|
+
|
|
|
+async function loadSamples(groupId) {
|
|
|
+ try {
|
|
|
+ const data = await API.get(`/api/samples/${groupId}`);
|
|
|
+ if (!sampleCanvas) {
|
|
|
+ sampleCanvas = new SampleCanvas('sample-canvas', 'sample-grid-wrapper');
|
|
|
+ }
|
|
|
+ sampleCanvas.setSamples(data.samples || []);
|
|
|
+ } catch (e) {
|
|
|
+ log(`加载扫描样本失败: ${e.message}`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async function loadGroups() {
|
|
|
@@ -283,12 +375,17 @@ async function loadGroups() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function resetSampleZoom() {
|
|
|
+ if (sampleCanvas) sampleCanvas.resetView();
|
|
|
+}
|
|
|
+
|
|
|
function onGroupChange() {
|
|
|
currentGroup = document.getElementById('group-select').value;
|
|
|
- clearTempMarker();
|
|
|
- clearPreviewSprites();
|
|
|
+ selectedSampleEl = null;
|
|
|
tempPreview = null;
|
|
|
- loadPanorama(currentGroup);
|
|
|
+ setSelectedPreview(null);
|
|
|
+ resetSampleZoom();
|
|
|
+ loadSamples(currentGroup);
|
|
|
renderVideos(currentGroup);
|
|
|
loadPoints(currentGroup);
|
|
|
}
|
|
|
@@ -317,15 +414,9 @@ function renderVideos(groupId) {
|
|
|
async function loadPoints(groupId) {
|
|
|
try {
|
|
|
const data = await API.get(`/api/points/${groupId}`);
|
|
|
- clearMarkers();
|
|
|
- clearPreviewSprites();
|
|
|
const ul = document.getElementById('points');
|
|
|
ul.innerHTML = '';
|
|
|
data.points.forEach(p => {
|
|
|
- addMarker(p.pan, p.tilt, 0x22c55e);
|
|
|
- if (p.preview_image) {
|
|
|
- addPreviewPlane(p.pan, p.tilt, `/api/preview-image?path=${encodeURIComponent(p.preview_image)}`);
|
|
|
- }
|
|
|
const li = document.createElement('li');
|
|
|
|
|
|
const span = document.createElement('span');
|
|
|
@@ -407,7 +498,8 @@ document.getElementById('btn-scan').addEventListener('click', async () => {
|
|
|
if (!controlsGloballyDisabled) scanBtn.disabled = false;
|
|
|
if (prog.state === 'done') {
|
|
|
log('扫描完成');
|
|
|
- loadPanorama(scannedGroup);
|
|
|
+ resetSampleZoom();
|
|
|
+ loadSamples(scannedGroup);
|
|
|
loadPoints(scannedGroup);
|
|
|
} else if (prog.state === 'failed') {
|
|
|
log(`扫描失败: ${prog.error || 'unknown'}`);
|
|
|
@@ -462,26 +554,13 @@ document.getElementById('btn-poll-stop').addEventListener('click', async () => {
|
|
|
|
|
|
async function runPreview(groupId, pan, tilt, zoom, pointId = null) {
|
|
|
const payload = { pan, tilt, zoom };
|
|
|
- if (pointId !== null) {
|
|
|
- payload.point_id = pointId;
|
|
|
- }
|
|
|
const result = await API.post(`/api/preview/${groupId}`, payload);
|
|
|
log(`预览位置: P=${pan.toFixed(1)} T=${tilt.toFixed(1)} Z=${zoom}`);
|
|
|
if (result.snapshot_url) {
|
|
|
log(`预览抓拍已保存: ${result.snapshot_path}`);
|
|
|
- if (pointId === null) {
|
|
|
- // 未保存的临时点:记录本次预览图,保存时可关联
|
|
|
- if (tempPreview && tempPreview.mesh) {
|
|
|
- scene.remove(tempPreview.mesh);
|
|
|
- previewSprites = previewSprites.filter(s => s !== tempPreview.mesh);
|
|
|
- }
|
|
|
- clearTempMarker();
|
|
|
- const mesh = addPreviewPlane(pan, tilt, result.snapshot_url);
|
|
|
- tempPreview = { pan, tilt, url: result.snapshot_url, path: result.snapshot_path, mesh };
|
|
|
- } else {
|
|
|
- // 已保存点:刷新列表以显示最新预览图
|
|
|
- loadPoints(groupId);
|
|
|
- }
|
|
|
+ // 预览抓拍显示在右侧,但不更新保存点图片
|
|
|
+ setSelectedPreview(result.snapshot_url);
|
|
|
+ tempPreview = { path: result.snapshot_path };
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
@@ -554,8 +633,9 @@ document.getElementById('btn-add').addEventListener('click', async () => {
|
|
|
try {
|
|
|
await API.post(`/api/points/${currentGroup}`, payload);
|
|
|
log('扫描点已保存');
|
|
|
- clearTempMarker();
|
|
|
+ selectedSampleEl = null;
|
|
|
tempPreview = null;
|
|
|
+ setSelectedPreview(null);
|
|
|
loadPoints(currentGroup);
|
|
|
} catch (e) {
|
|
|
log(`保存失败: ${e.message}`);
|
|
|
@@ -563,7 +643,26 @@ document.getElementById('btn-add').addEventListener('click', async () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
-initThree();
|
|
|
+document.getElementById('btn-zoom-in').addEventListener('click', () => {
|
|
|
+ if (sampleCanvas) {
|
|
|
+ sampleCanvas.scale = Math.min(5.0, sampleCanvas.scale * 1.2);
|
|
|
+ sampleCanvas.updateZoomLabel();
|
|
|
+ sampleCanvas.draw();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+document.getElementById('btn-zoom-out').addEventListener('click', () => {
|
|
|
+ if (sampleCanvas) {
|
|
|
+ sampleCanvas.scale = Math.max(0.1, sampleCanvas.scale / 1.2);
|
|
|
+ sampleCanvas.updateZoomLabel();
|
|
|
+ sampleCanvas.draw();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+document.getElementById('btn-zoom-reset').addEventListener('click', () => {
|
|
|
+ resetSampleZoom();
|
|
|
+});
|
|
|
+
|
|
|
setControlsDisabled(true);
|
|
|
loadGroups();
|
|
|
setInterval(updateStatus, 2000);
|