Skip to content

Commit 76531ea

Browse files
committed
melhorias calibration
1 parent 5ed3c11 commit 76531ea

File tree

16 files changed

+534
-468
lines changed

16 files changed

+534
-468
lines changed

CLAUDE.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ npm run dev # Dev server with auto-restart (--watch)
2323
npm run migrate # Import JSON metadata + JPG images into SQLite
2424
npm run generate-pmtiles # Generate PMTiles for map markers
2525
npm run verify # Validate database integrity
26-
npm run estimate-slope-roll # Estimate mesh_rotation_z from elevation data
26+
npm run estimate-slope-roll # (Deprecated) Estimate mesh_rotation_z from elevation data
2727
npm test # Run tests (node:test built-in)
2828
npm run lint # ESLint (--max-warnings 0)
2929
npm run lint:fix # ESLint auto-fix
@@ -52,7 +52,7 @@ src/
5252
scripts/
5353
├── migrate.js # JSON+JPG → SQLite migration (7-phase)
5454
├── generate-pmtiles.js # PMTiles generation for mapping
55-
├── estimate-slope-roll.js # Estimate mesh_rotation_z from elevation between consecutive photos
55+
├── estimate-slope-roll.js # (Deprecated) Estimate mesh_rotation_z from elevation
5656
└── verify.js # Data validation
5757
5858
public/calibration/ # Calibration web interface
@@ -241,7 +241,7 @@ const imgDb = getProjectDb('alegrete.db'); // Cached per filename
241241
|-----------|--------|-------|---------|-------------|
242242
| Heading (Y) | `mesh_rotation_y` | 0–360 | 180 | Yaw correction applied to panorama sphere |
243243
| Pitch (X) | `mesh_rotation_x` | −30–30 | 0 | Pitch tilt correction |
244-
| Roll (Z) | `mesh_rotation_z` | −30–30 | 0 | Roll tilt correction (auto-estimated from slope) |
244+
| Roll (Z) | `mesh_rotation_z` | −30–30 | 0 | Roll tilt correction |
245245
| Camera height | `camera_height` | 0.1–20 | 2.5 | Height above ground in meters |
246246
| Distance scale | `distance_scale` | 0.1–5.0 | 1.0 | Multiplier for target distances |
247247
| Marker scale | `marker_scale` | 0.1–5.0 | 1.0 | Multiplier for navigation marker visual size |
@@ -254,14 +254,21 @@ const imgDb = getProjectDb('alegrete.db'); // Cached per filename
254254
### Three.js Rotation Order
255255
The panorama sphere uses Euler order `ZXY` — matrix `Rz·Rx·Ry` — meaning Y (heading) is applied first to pixels, then X (pitch), then Z (roll) in the corrected frame. Both `viewer.js` (calibration) and `street_view_viewer.js` (ebgeo_web) must use the same order.
256256

257-
### Elevation Delta in Projection
258-
When both camera and target have `ele` data, the elevation difference `ΔE = target.ele - camera.ele` offsets the target marker vertically: `y = -cameraHeight + ΔE`. This affects marker Y position, flatten ratio, and slant distance for marker sizing. Clamped to ±2m (`MAX_ELEVATION_DELTA`) to protect against GPS noise. When either `ele` is null, `ΔE = 0` (flat ground fallback).
257+
### Height Model (Flat Ground)
258+
All markers are projected using a **flat ground model** — GPS elevation (`ele`) is stored but **not used** for projection. The `ele` column is retained in the database and API responses for informational purposes only.
259259

260-
### Slope Roll Estimation
261-
`scripts/estimate-slope-roll.js` estimates `mesh_rotation_z` (roll) from elevation data between a photo and its `next` target: `θ = atan2(ΔE, distance)`. Slopes beyond `--max-angle` (default 15°, recommended 10°) are **discarded** as GPS noise (set to 0), not clamped. Supports `--dry-run`, `--clear`, `--project <slug>`, `--max-angle <N>`.
260+
- **GPS targets**: projected onto ground plane at `y = -cameraHeight` (all targets at same level)
261+
- **Override targets**: projected onto `y = -cameraHeight + overrideHeight` (manual height control)
262+
- **`override_height`**: relative to camera ground level (0 = ground, positive = above ground)
263+
- **`camera_height`**: distance from ground to camera lens (default 2.5m)
264+
265+
Both `projector.calculateFlattenRatio()` and `projector.calculateMarkerSize()` accept a `heightOffset` parameter (default 0) used only by override targets via `overrideHeight`.
262266

263267
### Target Override Projection
264-
Override markers use a **ground-plane model**: bearing + ground distance + height are projected onto `y = -cameraHeight + overrideHeight` plane, NOT spherical coordinates. Both `navigator.js` (calibration) and `navigation/navigator.js` (ebgeo_web) use `projectFromOverride()` for this. Override markers do **not** use GPS elevation delta — the height is manually controlled via a slider in the calibration UI. When `override_height` is NULL/0, the marker sits on the ground plane exactly where the user clicked.
268+
Override markers use a **ground-plane model**: bearing + ground distance + height are projected onto `y = -cameraHeight + overrideHeight` plane, NOT spherical coordinates. Both `navigator.js` (calibration) and `navigation/navigator.js` (ebgeo_web) use `projectFromOverride()` for this. When `override_height` is NULL/0, the marker sits on the ground plane exactly where the user clicked.
269+
270+
### Slope Roll Estimation (Deprecated)
271+
`scripts/estimate-slope-roll.js` previously estimated `mesh_rotation_z` from elevation data. This is deprecated — elevation data is no longer used for projection. The standalone script remains available with a deprecation warning; its `--clear` mode can reset `mesh_rotation_z` to 0.
265272

266273
## Calibration UI Architecture
267274

@@ -318,12 +325,12 @@ app.js (orchestrator)
318325

319326
Migration (`scripts/migrate.js`) processes source data in 7 phases:
320327
1. Read JSON metadata files from `METADATA/*.json`
321-
2. Read JPG images from `IMG/*.jpg`
322-
3. Assign photos to nearest project center by distance
323-
4. Generate navigation graph (50m radius, 15-degree angular separation)
324-
5. Convert images to WebP (full + preview sizes)
325-
6. Write to SQLite databases (index.db + per-project DBs)
326-
7. Verify data integrity
328+
2. Assign photos to nearest project center by distance
329+
3. Compute sequence numbers per project
330+
4. Generate deterministic UUIDs
331+
5. Adaptive spatial analysis — navigation graph (sector-based, per-project adaptive radius)
332+
6. Populate metadata + targets in index.db
333+
7. Process images into per-project databases (JPG → WebP conversion)
327334

328335
## Deployment
329336

public/calibration/index.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,34 @@
449449
font-size: 11px;
450450
}
451451

452+
.cal-panel__btn--icon {
453+
background: transparent;
454+
border: 1px solid #45475a;
455+
color: #a6adc8;
456+
padding: 1px 6px;
457+
font-size: 11px;
458+
font-family: monospace;
459+
border-radius: 4px;
460+
cursor: pointer;
461+
vertical-align: middle;
462+
margin-left: 6px;
463+
}
464+
465+
.cal-panel__btn--icon:hover {
466+
background: #45475a;
467+
color: #cdd6f4;
468+
}
469+
470+
.cal-panel__btn--danger {
471+
color: #f38ba8;
472+
border-color: #f38ba8;
473+
}
474+
475+
.cal-panel__btn--danger:hover {
476+
background: #f38ba8;
477+
color: #1e1e2e;
478+
}
479+
452480
.cal-panel__btn--ghost {
453481
background: transparent;
454482
border: 1px solid #45475a;
@@ -801,6 +829,16 @@
801829
flex: 1;
802830
}
803831

832+
.cal-dialog__actions .cal-panel__btn--destructive {
833+
background: #f38ba8;
834+
color: #1e1e2e;
835+
font-weight: 600;
836+
}
837+
838+
.cal-dialog__actions .cal-panel__btn--destructive:hover {
839+
background: #eba0ac;
840+
}
841+
804842
/* ================================================================
805843
REVIEW WORKFLOW — Navigation bar
806844
================================================================ */

public/calibration/js/api.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,22 @@ export async function createTarget(sourceId, targetId) {
336336
return response.json();
337337
}
338338

339+
/**
340+
* Soft-deletes a photo (removes from navigation, keeps data for recovery).
341+
* @param {string} photoId - Photo UUID
342+
* @returns {Promise<Object>} Server response with deletedPhotoId, projectSlug, newPhotoCount
343+
*/
344+
export async function deletePhoto(photoId) {
345+
const response = await fetch(`${BASE}/photos/${photoId}`, {
346+
method: 'DELETE',
347+
});
348+
if (!response.ok) {
349+
const text = await response.text().catch(() => '');
350+
throw new Error(`Failed to delete photo ${photoId} (HTTP ${response.status}): ${text}`);
351+
}
352+
return response.json();
353+
}
354+
339355
/**
340356
* Deletes a manually-created target connection.
341357
* @param {string} sourceId - Source photo UUID

public/calibration/js/app.js

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
saveTargetOverride, clearTargetOverride,
1111
setPhotoReviewed, fetchProjectPhotos,
1212
saveTargetVisibility, fetchNearbyPhotos, createTarget, deleteTargetConnection,
13+
deletePhoto,
1314
} from './api.js';
1415
import {
1516
state, isDirty, loadPhoto, discardChanges, markSaved, onChange,
@@ -297,6 +298,7 @@ function initializeSubsystems() {
297298
onDeleteTarget: handleDeleteTarget,
298299
onNearbyPreviewToggle: handleNearbyPreviewToggle,
299300
onNearbySelect: handleNearbySelect,
301+
onDeletePhoto: handleDeletePhoto,
300302
});
301303

302304
// Initialize preview viewer (shows target photo when selected)
@@ -310,14 +312,19 @@ function initializeSubsystems() {
310312
},
311313
});
312314

313-
// Sync minimap target selection and preview viewer with state
315+
// Sync minimap target selection and preview viewer with state.
316+
// Track last fetched target to avoid re-fetching on every notify (e.g. slider changes).
317+
let lastPreviewTargetId = null;
318+
314319
onChange((s) => {
315320
setSelectedTarget(s.selectedTargetId);
316321

317-
// Show/hide preview viewer based on selected target
318-
if (s.selectedTargetId && s.currentMetadata?.targets) {
322+
// Show/hide preview viewer only when the selected target changes
323+
if (s.selectedTargetId && s.selectedTargetId !== lastPreviewTargetId && s.currentMetadata?.targets) {
319324
const target = s.currentMetadata.targets.find(t => t.id === s.selectedTargetId);
320325
if (target) {
326+
lastPreviewTargetId = s.selectedTargetId;
327+
321328
// Clear nearby preview when selecting a real target
322329
clearNearbyPreview();
323330
showAddButton(false);
@@ -340,6 +347,7 @@ function initializeSubsystems() {
340347
});
341348
}
342349
} else if (!s.selectedTargetId) {
350+
lastPreviewTargetId = null;
343351
// Only hide preview if no nearby preview is active
344352
const { previewingId } = getNearbyPreviewState();
345353
if (!previewingId) {
@@ -380,6 +388,7 @@ function onSetFromClick(groundOverride) {
380388
showToast(`Override definido: bearing=${bearingDeg.toFixed(1)}°, dist=${distance.toFixed(1)}m`, 'success');
381389
}
382390

391+
383392
// ============================================================================
384393
// NAVIGATION
385394
// ============================================================================
@@ -638,6 +647,81 @@ async function handleDeleteTarget(targetId) {
638647
}
639648
}
640649

650+
// ============================================================================
651+
// DELETE PHOTO (soft-delete)
652+
// ============================================================================
653+
654+
async function handleDeletePhoto() {
655+
const photoId = state.currentPhotoId;
656+
const displayName = state.currentMetadata?.camera?.display_name || photoId?.slice(0, 8);
657+
658+
const confirmed = await showDeletePhotoDialog(displayName);
659+
if (!confirmed) return;
660+
661+
try {
662+
const result = await deletePhoto(photoId);
663+
showToast(`Foto ${displayName} excluida`, 'success');
664+
665+
// Refresh project photo list
666+
const slug = state.currentProjectSlug || result.projectSlug;
667+
if (slug) {
668+
await loadProjectContext(slug);
669+
}
670+
671+
// Navigate to previous (or next if at start, or back to projects)
672+
const prevId = getPrevPhotoId();
673+
const nextId = getNextPhotoId();
674+
if (prevId) {
675+
startCalibration(prevId);
676+
} else if (nextId) {
677+
startCalibration(nextId);
678+
} else {
679+
// No photos left — go back to project selector
680+
showProjectSelector();
681+
}
682+
} catch (err) {
683+
console.error('Failed to delete photo:', err);
684+
showToast(`Erro ao excluir foto: ${err.message}`, 'error');
685+
}
686+
}
687+
688+
/**
689+
* Shows a large destructive confirmation dialog for deleting a photo.
690+
* @param {string} displayName - Display name of the photo being deleted
691+
* @returns {Promise<boolean>} Whether the user confirmed
692+
*/
693+
function showDeletePhotoDialog(displayName) {
694+
return new Promise((resolve) => {
695+
const overlay = document.createElement('div');
696+
overlay.className = 'cal-dialog-overlay';
697+
overlay.innerHTML = `
698+
<div class="cal-dialog">
699+
<h3 class="cal-dialog__title">Excluir foto permanentemente?</h3>
700+
<p class="cal-dialog__text">
701+
<strong>${displayName}</strong><br><br>
702+
Todas as conexoes desta foto serao removidas.
703+
Ela nao aparecera mais na navegacao.<br><br>
704+
<em>O PMTiles precisara ser regenerado para remover o ponto do mapa.</em>
705+
</p>
706+
<div class="cal-dialog__actions">
707+
<button class="cal-panel__btn cal-panel__btn--destructive" data-action="confirm">Excluir</button>
708+
<button class="cal-panel__btn cal-panel__btn--ghost" data-action="cancel">Cancelar</button>
709+
</div>
710+
</div>
711+
`;
712+
713+
overlay.addEventListener('click', (e) => {
714+
const btn = e.target.closest('[data-action]');
715+
if (btn) {
716+
overlay.remove();
717+
resolve(btn.dataset.action === 'confirm');
718+
}
719+
});
720+
721+
document.body.appendChild(overlay);
722+
});
723+
}
724+
641725
/**
642726
* Refreshes targets and nearby photos without reloading the panorama.
643727
* Preserves the current camera view and calibration edits.
@@ -731,7 +815,7 @@ function onKeyDown(e) {
731815
}
732816
}
733817

734-
// Escape = Deselect target
818+
// Escape = Cancel modes / Deselect target
735819
if (e.key === 'Escape') {
736820
if (state.setFromClickMode) {
737821
setSetFromClickMode(false);
@@ -752,8 +836,8 @@ function onKeyDown(e) {
752836
handleNextPhoto();
753837
}
754838

755-
// P or [ = Previous photo
756-
if ((e.key === 'p' || e.key === '[') && !e.ctrlKey && !e.metaKey) {
839+
// P or [ or Q = Previous photo
840+
if ((e.key === 'p' || e.key === '[' || e.key === 'q') && !e.ctrlKey && !e.metaKey) {
757841
handlePrevPhoto();
758842
}
759843

0 commit comments

Comments
 (0)