Skip to content

Commit 123b4a5

Browse files
committed
efeitos de altura no 360
1 parent 361255d commit 123b4a5

File tree

16 files changed

+881
-287
lines changed

16 files changed

+881
-287
lines changed

CLAUDE.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +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
2627
npm test # Run tests (node:test built-in)
2728
npm run lint # ESLint (--max-warnings 0)
2829
npm run lint:fix # ESLint auto-fix
@@ -51,6 +52,7 @@ src/
5152
scripts/
5253
├── migrate.js # JSON+JPG → SQLite migration (7-phase)
5354
├── generate-pmtiles.js # PMTiles generation for mapping
55+
├── estimate-slope-roll.js # Estimate mesh_rotation_z from elevation between consecutive photos
5456
└── verify.js # Data validation
5557
5658
public/calibration/ # Calibration web interface
@@ -71,7 +73,7 @@ tests/
7173
- **projects** — slug, name, location, center coordinates, entry photo ID, photo count
7274
- **photos** — coordinates, heading, camera_height, mesh_rotation_y/x/z, distance_scale, calibration_reviewed, sequence number
7375
- **photos_rtree** — R-tree spatial index for geographic queries
74-
- **targets** — Navigation graph (source→target with distance, bearing, override bearing/distance, hidden flag)
76+
- **targets** — Navigation graph (source→target with distance, bearing, override bearing/distance/height, hidden flag)
7577

7678
### {slug}.db (Per-project images)
7779
- **images** — photo_id → full_webp BLOB + preview_webp BLOB
@@ -89,6 +91,7 @@ tests/
8991
- Renames `override_heading`/`override_pitch``override_bearing`/`override_distance` in `targets`
9092
- Clamps old `override_pitch < 0.5` values to `5m` default
9193
- Adds `hidden` column to `targets` (default 0)
94+
- Adds `override_height` column to `targets` (default NULL)
9295

9396
## API Endpoints
9497

@@ -112,7 +115,7 @@ tests/
112115
| `PUT /api/v1/photos/:uuid/distance-scale` | Update distance_scale (0.1–5.0) |
113116
| `PUT /api/v1/photos/:uuid/marker-scale` | Update marker_scale (0.1–5.0) |
114117
| `PUT /api/v1/photos/:uuid/reviewed` | Mark photo reviewed/unreviewed |
115-
| `PUT /api/v1/targets/:sourceId/:targetId/override` | Set bearing (0–360°) / distance (0.5–500 m) overrides |
118+
| `PUT /api/v1/targets/:sourceId/:targetId/override` | Set bearing (0–360°) / distance (0.5–500 m) / height (−10–10 m) overrides |
116119
| `DELETE /api/v1/targets/:sourceId/:targetId/override` | Clear overrides |
117120
| `PUT /api/v1/targets/:sourceId/:targetId/visibility` | Set target hidden state (hidden: bool) |
118121
| `GET /api/v1/photos/:uuid/nearby?radius=100` | Find nearby unconnected photos within radius |
@@ -141,7 +144,7 @@ tests/
141144
"lon": -55.79, "lat": -29.78, "ele": 100.0,
142145
"display_name": "IMG_0002",
143146
"next": true, "distance": 12.5, "bearing": 45.0,
144-
"override_bearing": null, "override_distance": null,
147+
"override_bearing": null, "override_distance": null, "override_height": null,
145148
"hidden": false, "is_original": true
146149
}]
147150
}
@@ -232,20 +235,27 @@ const imgDb = getProjectDb('alegrete.db'); // Cached per filename
232235
|-----------|--------|-------|---------|-------------|
233236
| Heading (Y) | `mesh_rotation_y` | 0–360 | 180 | Yaw correction applied to panorama sphere |
234237
| Pitch (X) | `mesh_rotation_x` | −30–30 | 0 | Pitch tilt correction |
235-
| Roll (Z) | `mesh_rotation_z` | −30–30 | 0 | Roll tilt correction |
238+
| Roll (Z) | `mesh_rotation_z` | −30–30 | 0 | Roll tilt correction (auto-estimated from slope) |
236239
| Camera height | `camera_height` | 0.1–20 | 2.5 | Height above ground in meters |
237240
| Distance scale | `distance_scale` | 0.1–5.0 | 1.0 | Multiplier for target distances |
238241
| Marker scale | `marker_scale` | 0.1–5.0 | 1.0 | Multiplier for navigation marker visual size |
239242
| Override bearing | `override_bearing` | 0–360 | NULL | Manual target bearing (degrees, 0=North) |
240243
| Override distance | `override_distance` | 0.5–500 | NULL | Manual target ground distance (meters) |
244+
| Override height | `override_height` | −10–10 | NULL | Manual target vertical offset (meters, positive = above ground) |
241245
| Hidden | `hidden` | 0/1 | 0 | Whether target is hidden from navigation (per source→target pair) |
242246
| Reviewed | `calibration_reviewed` | 0/1 | 0 | Whether calibration has been reviewed |
243247

244248
### Three.js Rotation Order
245249
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.
246250

251+
### Elevation Delta in Projection
252+
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).
253+
254+
### Slope Roll Estimation
255+
`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>`.
256+
247257
### Target Override Projection
248-
Override markers use a **ground-plane model**: bearing + ground distance are projected onto `y = -cameraHeight` plane, NOT spherical coordinates. Both `navigator.js` (calibration) and `navigation/navigator.js` (ebgeo_web) use `projectFromOverride()` for this.
258+
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.
249259

250260
## Calibration UI Architecture
251261

@@ -273,10 +283,10 @@ app.js (orchestrator)
273283
3. Grid toggle — perspective grid on/off
274284
4. Save/Discard buttons — enabled when dirty
275285
5. Review actions — mark reviewed, reviewed → next
276-
6. **Collapsible**: Parametros de Calibracao — 6 sliders (rotation_y/x/z, height, distance/marker_scale)
286+
6. **Collapsible**: Parametros de Calibração — 6 sliders (rotation_y/x/z, height, distance/marker_scale)
277287
7. **Collapsible**: Aplicar ao Projeto — batch update buttons
278288
8. **Collapsible**: Targets (N) — clickable target list with override/hidden badges
279-
9. Override editor — bearing/distance sliders, set-from-click, hide/show, delete (when target selected)
289+
9. Override editor — bearing/distance/height sliders, set-from-click, hide/show, delete (when target selected)
280290
10. **Collapsible**: Fotos Proximas (N) — nearby unconnected photos with Preview toggle
281291
11. Fotos do Projeto — full photo list with review status
282292

EXPANDING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ curl -o test_preview.webp "http://localhost:8081/api/v1/photos/<entryPhotoId>/im
208208
curl -o test_full.webp "http://localhost:8081/api/v1/photos/<entryPhotoId>/image?quality=full"
209209
```
210210

211-
### Testar calibracao
211+
### Testar Calibração
212212

213213
Abra `http://localhost:8081/calibration/` no navegador e selecione o novo projeto no seletor.
214214

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"dev": "node --env-file=.env --watch src/server.js",
1010
"migrate": "node scripts/migrate.js",
1111
"recalculate-targets": "node scripts/recalculate-targets.js",
12+
"estimate-slope-roll": "node scripts/estimate-slope-roll.js",
1213
"generate-pmtiles": "node scripts/generate-pmtiles.js",
1314
"verify": "node scripts/verify.js",
1415
"test": "node --test tests/**/*.test.js",

public/calibration/js/api.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,23 @@ export async function saveCameraHeight(photoId, cameraHeight) {
8383
}
8484

8585
/**
86-
* Saves a target bearing/distance override.
86+
* Saves a target bearing/distance/height override.
8787
* @param {string} sourceId - Source photo UUID
8888
* @param {string} targetId - Target photo UUID
8989
* @param {number} bearing - Override bearing in degrees (0-360)
9090
* @param {number} distance - Override ground distance in meters (0.5-500)
91+
* @param {number} [height=0] - Vertical offset in meters (-10 to +10)
9192
* @returns {Promise<Object>} Server response
9293
*/
93-
export async function saveTargetOverride(sourceId, targetId, bearing, distance) {
94+
export async function saveTargetOverride(sourceId, targetId, bearing, distance, height = 0) {
9495
const response = await fetch(`${BASE}/targets/${sourceId}/${targetId}/override`, {
9596
method: 'PUT',
9697
headers: { 'Content-Type': 'application/json' },
97-
body: JSON.stringify({ override_bearing: bearing, override_distance: distance }),
98+
body: JSON.stringify({
99+
override_bearing: bearing,
100+
override_distance: distance,
101+
override_height: height || null,
102+
}),
98103
});
99104
if (!response.ok) {
100105
const text = await response.text().catch(() => '');

public/calibration/js/app.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async function showProjectSelector() {
105105
});
106106

107107
projectSelector.innerHTML = `
108-
<h1 class="project-selector__title">Street View 360 — Calibracao</h1>
108+
<h1 class="project-selector__title">Street View 360 — Calibração</h1>
109109
<p class="project-selector__subtitle">Selecione um projeto para iniciar</p>
110110
<div class="project-selector__grid">
111111
${projects.map(p => {
@@ -498,8 +498,9 @@ async function handleSave() {
498498
const original = state.originalTargetOverrides.get(targetId);
499499
const origB = original?.bearing ?? null;
500500
const origD = original?.distance ?? null;
501+
const origH = original?.height ?? 0;
501502

502-
if (edited.bearing !== origB || edited.distance !== origD) {
503+
if (edited.bearing !== origB || edited.distance !== origD || (edited.height ?? 0) !== origH) {
503504
if (edited.bearing === null && edited.distance === null) {
504505
// Override cleared
505506
promises.push(
@@ -510,7 +511,8 @@ async function handleSave() {
510511
promises.push(
511512
saveTargetOverride(
512513
state.currentPhotoId, targetId,
513-
edited.bearing, edited.distance
514+
edited.bearing, edited.distance,
515+
edited.height ?? 0
514516
)
515517
);
516518
}

public/calibration/js/calibration-panel.js

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
state, onChange, isDirty,
88
setMeshRotationY, setCameraHeight,
99
setMeshRotationX, setMeshRotationZ, setDistanceScale, setMarkerScale,
10-
setTargetOverride, clearTargetOverrideEdit,
10+
setTargetOverride, setTargetOverrideHeight, clearTargetOverrideEdit,
1111
selectTarget, deselectTarget, setSetFromClickMode,
1212
setTargetHidden, isTargetHidden,
1313
getCurrentPhotoIndex, resetAllReviewedState,
@@ -372,7 +372,7 @@ function renderSlidersSection(s) {
372372
</div>
373373
`;
374374

375-
return renderCollapsibleSection('sliders', 'Parametros de Calibracao', content);
375+
return renderCollapsibleSection('sliders', 'Parametros de Calibração', content);
376376
}
377377

378378
function renderBatchSection(s) {
@@ -464,15 +464,16 @@ function renderTargetItem(target, s) {
464464
function renderOverrideEditor(target, s) {
465465
const edited = s.editedTargetOverrides.get(target.id);
466466
const original = s.originalTargetOverrides.get(target.id);
467-
const effective = edited || original || { bearing: null, distance: null };
467+
const effective = edited || original || { bearing: null, distance: null, height: 0 };
468468

469469
const hasEffectiveOverride = effective.bearing !== null;
470470
const hidden = isTargetHidden(target.id);
471471
const isManual = target.is_original === false;
472472

473-
// bearing (degrees), distance = ground distance (meters)
473+
// bearing (degrees), distance = ground distance (meters), height = vertical offset (meters)
474474
const bearingVal = effective.bearing ?? 0;
475475
const distanceVal = effective.distance ?? 5;
476+
const heightVal = effective.height ?? 0;
476477

477478
return `
478479
<div class="cal-panel__section cal-panel__section--override">
@@ -498,6 +499,16 @@ function renderOverrideEditor(target, s) {
498499
min="0.5" max="500" step="0.1"
499500
value="${distanceVal.toFixed(1)}" />
500501
</div>
502+
503+
<div class="cal-panel__slider-group">
504+
<label class="cal-panel__label">Altura (m)</label>
505+
<input type="range" id="override-height-slider" class="cal-panel__slider"
506+
min="-5" max="5" step="0.1"
507+
value="${heightVal}" />
508+
<input type="number" id="override-height-input" class="cal-panel__input cal-panel__input--narrow"
509+
min="-10" max="10" step="0.1"
510+
value="${heightVal.toFixed(1)}" />
511+
</div>
501512
` : `
502513
<p class="cal-panel__hint">Sem override definido. Clique no viewer para posicionar.</p>
503514
`}
@@ -864,23 +875,28 @@ function attachEvents() {
864875
}
865876
});
866877

867-
// Override bearing/distance sliders
878+
// Override bearing/distance/height sliders
868879
const bearingSlider = document.getElementById('override-bearing-slider');
869880
const bearingInput = document.getElementById('override-bearing-input');
870881
const distanceSlider = document.getElementById('override-distance-slider');
871882
const distanceInput = document.getElementById('override-distance-input');
883+
const heightSlider = document.getElementById('override-height-slider');
884+
const heightInput = document.getElementById('override-height-input');
885+
886+
/** Helper to read current height value from slider */
887+
const getHeightVal = () => parseFloat(heightSlider?.value ?? 0);
872888

873889
if (bearingSlider) {
874890
bearingSlider.addEventListener('input', (e) => {
875891
const bearing = parseFloat(e.target.value);
876892
if (bearingInput) bearingInput.value = bearing.toFixed(1);
877893
const distVal = parseFloat(distanceSlider?.value ?? 0);
878-
setTargetOverride(state.selectedTargetId, bearing, distVal, true);
894+
setTargetOverride(state.selectedTargetId, bearing, distVal, getHeightVal(), true);
879895
});
880896
bearingSlider.addEventListener('change', (e) => {
881897
const bearing = parseFloat(e.target.value);
882898
const distVal = parseFloat(distanceSlider?.value ?? 0);
883-
setTargetOverride(state.selectedTargetId, bearing, distVal);
899+
setTargetOverride(state.selectedTargetId, bearing, distVal, getHeightVal());
884900
});
885901
}
886902

@@ -890,7 +906,7 @@ function attachEvents() {
890906
if (isNaN(bearing)) bearing = 0;
891907
bearing = Math.max(0, Math.min(360, bearing));
892908
const distVal = parseFloat(distanceSlider?.value ?? 0);
893-
setTargetOverride(state.selectedTargetId, bearing, distVal);
909+
setTargetOverride(state.selectedTargetId, bearing, distVal, getHeightVal());
894910
});
895911
}
896912

@@ -899,12 +915,12 @@ function attachEvents() {
899915
const distance = parseFloat(e.target.value);
900916
if (distanceInput) distanceInput.value = distance.toFixed(1);
901917
const bearingVal = parseFloat(bearingSlider?.value ?? 0);
902-
setTargetOverride(state.selectedTargetId, bearingVal, distance, true);
918+
setTargetOverride(state.selectedTargetId, bearingVal, distance, getHeightVal(), true);
903919
});
904920
distanceSlider.addEventListener('change', (e) => {
905921
const distance = parseFloat(e.target.value);
906922
const bearingVal = parseFloat(bearingSlider?.value ?? 0);
907-
setTargetOverride(state.selectedTargetId, bearingVal, distance);
923+
setTargetOverride(state.selectedTargetId, bearingVal, distance, getHeightVal());
908924
});
909925
}
910926

@@ -914,7 +930,28 @@ function attachEvents() {
914930
if (isNaN(distance)) distance = 5;
915931
distance = Math.max(0.5, Math.min(500, distance));
916932
const bearingVal = parseFloat(bearingSlider?.value ?? 0);
917-
setTargetOverride(state.selectedTargetId, bearingVal, distance);
933+
setTargetOverride(state.selectedTargetId, bearingVal, distance, getHeightVal());
934+
});
935+
}
936+
937+
if (heightSlider) {
938+
heightSlider.addEventListener('input', (e) => {
939+
const height = parseFloat(e.target.value);
940+
if (heightInput) heightInput.value = height.toFixed(1);
941+
setTargetOverrideHeight(state.selectedTargetId, height, true);
942+
});
943+
heightSlider.addEventListener('change', (e) => {
944+
const height = parseFloat(e.target.value);
945+
setTargetOverrideHeight(state.selectedTargetId, height);
946+
});
947+
}
948+
949+
if (heightInput) {
950+
heightInput.addEventListener('change', (e) => {
951+
let height = parseFloat(e.target.value);
952+
if (isNaN(height)) height = 0;
953+
height = Math.max(-10, Math.min(10, height));
954+
setTargetOverrideHeight(state.selectedTargetId, height);
918955
});
919956
}
920957

0 commit comments

Comments
 (0)