Skip to content

Commit 5fe32a4

Browse files
committed
melhora calibration
1 parent 76531ea commit 5fe32a4

File tree

6 files changed

+1043
-124
lines changed

6 files changed

+1043
-124
lines changed

public/calibration/js/app.js

Lines changed: 201 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
selectTarget, deselectTarget, setSetFromClickMode,
1919
setProjectContext, setCalibrationReviewed, getNextPhotoId, getPrevPhotoId,
2020
setNearbyPhotos, isTargetHidden, refreshTargets,
21+
setMeshRotationX, setMeshRotationY, setMeshRotationZ,
22+
setTargetHidden as stateSetTargetHidden,
2123
} from './state.js';
2224
import {
2325
initViewer, loadProgressive, setMeshRotationY as viewerSetMeshRotationY,
@@ -34,9 +36,11 @@ import {
3436
initMinimap, updateCamera, updateTargets, setSelectedTarget,
3537
updateNearbyPhotos,
3638
} from './minimap.js';
37-
import { initPanel, showToast, setGridToggleState, clearNearbyPreview, getNearbyPreviewState } from './calibration-panel.js';
39+
import { initPanel, showToast, setSphericalGridToggleState, clearNearbyPreview, getNearbyPreviewState } from './calibration-panel.js';
3840
import {
3941
initPreviewViewer, showPreview, hidePreview, showAddButton,
42+
showRearView, updateRearViewRotation, showTargetActions, updateHideButtonState,
43+
syncRearViewCamera, setRearViewTargets,
4044
} from './preview-viewer.js';
4145

4246
// ============================================================================
@@ -71,6 +75,8 @@ document.addEventListener('DOMContentLoaded', () => {
7175

7276
// Keyboard shortcuts
7377
document.addEventListener('keydown', onKeyDown);
78+
document.addEventListener('keyup', onKeyUp);
79+
window.addEventListener('blur', onWindowBlur);
7480

7581
// Prevent data loss on tab close
7682
window.addEventListener('beforeunload', (e) => {
@@ -200,6 +206,7 @@ async function startCalibration(photoId) {
200206
// Configure navigator
201207
setCameraConfig(metadata.camera);
202208
setTargets(metadata.targets);
209+
setRearViewTargets(metadata.targets, metadata.camera);
203210

204211
// Reset camera to look straight at the image center (lon=0).
205212
// The navigator adds imageHeading when computing world yaw,
@@ -216,6 +223,14 @@ async function startCalibration(photoId) {
216223
const fullUrl = getPhotoImageUrl(photoId, 'full');
217224
await loadProgressive(previewUrl, fullUrl);
218225

226+
// Show rear view in preview viewer
227+
showRearView(
228+
photoId,
229+
metadata.camera?.mesh_rotation_y ?? 180,
230+
metadata.camera?.mesh_rotation_x ?? 0,
231+
metadata.camera?.mesh_rotation_z ?? 0,
232+
);
233+
219234
// Update minimap
220235
updateCamera(metadata.camera);
221236
updateTargets(metadata.targets);
@@ -264,15 +279,24 @@ function initializeSubsystems() {
264279
initPanel(panelContainer, {
265280
onSave: handleSave,
266281
onDiscard: handleDiscard,
267-
onMeshRotationPreview: (degrees) => viewerSetMeshRotationY(degrees),
282+
onMeshRotationPreview: (degrees) => {
283+
viewerSetMeshRotationY(degrees);
284+
updateRearViewRotation(degrees, state.editedMeshRotationX ?? 0, state.editedMeshRotationZ ?? 0);
285+
},
268286
onCameraHeightPreview: (height) => {
269287
// Update navigator camera config live for ground-plane projection preview
270288
if (state.currentMetadata?.camera) {
271289
setCameraConfig({ ...state.currentMetadata.camera, height, distance_scale: state.editedDistanceScale, marker_scale: state.editedMarkerScale });
272290
}
273291
},
274-
onMeshRotationXPreview: (degrees) => viewerSetMeshRotationX(degrees),
275-
onMeshRotationZPreview: (degrees) => viewerSetMeshRotationZ(degrees),
292+
onMeshRotationXPreview: (degrees) => {
293+
viewerSetMeshRotationX(degrees);
294+
updateRearViewRotation(state.editedMeshRotationY ?? 180, degrees, state.editedMeshRotationZ ?? 0);
295+
},
296+
onMeshRotationZPreview: (degrees) => {
297+
viewerSetMeshRotationZ(degrees);
298+
updateRearViewRotation(state.editedMeshRotationY ?? 180, state.editedMeshRotationX ?? 0, degrees);
299+
},
276300
onDistanceScalePreview: (scale) => {
277301
// Update navigator camera config with new distance_scale
278302
if (state.currentMetadata?.camera) {
@@ -290,10 +314,8 @@ function initializeSubsystems() {
290314
onNextPhoto: handleNextPhoto,
291315
onPrevPhoto: handlePrevPhoto,
292316
onBackToProjects: () => showProjectSelector(),
293-
onGridToggle: (visible) => {
294-
setGridVisible(visible);
295-
setGroundGridVisible(visible);
296-
},
317+
onSphericalGridToggle: (visible) => setGridVisible(visible),
318+
onGroundGridToggle: (visible) => setGroundGridVisible(visible),
297319
onAddTarget: handleAddTarget,
298320
onDeleteTarget: handleDeleteTarget,
299321
onNearbyPreviewToggle: handleNearbyPreviewToggle,
@@ -329,26 +351,53 @@ function initializeSubsystems() {
329351
clearNearbyPreview();
330352
showAddButton(false);
331353

332-
// Fetch target photo metadata to get its mesh_rotation_y
354+
// Fetch target photo metadata to get its mesh_rotation_y/x/z
333355
fetchPhotoMetadata(target.id).then(meta => {
334356
// Only show if still the same target
335357
if (state.selectedTargetId === target.id) {
336358
showPreview(
337359
target.id,
338360
target.display_name || target.id.slice(0, 8),
339-
meta.camera?.mesh_rotation_y ?? 180
361+
meta.camera?.mesh_rotation_y ?? 180,
362+
meta.camera?.mesh_rotation_x ?? 0,
363+
meta.camera?.mesh_rotation_z ?? 0,
340364
);
365+
showTargetActions(true, {
366+
onHide: () => {
367+
const hidden = isTargetHidden(state.selectedTargetId);
368+
stateSetTargetHidden(state.selectedTargetId, !hidden);
369+
updateHideButtonState(!hidden);
370+
},
371+
onSetFromClick: () => {
372+
setSetFromClickMode(!state.setFromClickMode);
373+
refreshCursor();
374+
},
375+
isHidden: isTargetHidden(target.id),
376+
});
341377
}
342378
}).catch(() => {
343379
// Still show without correct mesh rotation
344380
if (state.selectedTargetId === target.id) {
345381
showPreview(target.id, target.display_name || target.id.slice(0, 8));
382+
showTargetActions(true, {
383+
onHide: () => {
384+
const hidden = isTargetHidden(state.selectedTargetId);
385+
stateSetTargetHidden(state.selectedTargetId, !hidden);
386+
updateHideButtonState(!hidden);
387+
},
388+
onSetFromClick: () => {
389+
setSetFromClickMode(!state.setFromClickMode);
390+
refreshCursor();
391+
},
392+
isHidden: isTargetHidden(target.id),
393+
});
346394
}
347395
});
348396
}
349397
} else if (!s.selectedTargetId) {
350398
lastPreviewTargetId = null;
351-
// Only hide preview if no nearby preview is active
399+
showTargetActions(false);
400+
// Switch back to rear view (or hide if no nearby preview)
352401
const { previewingId } = getNearbyPreviewState();
353402
if (!previewingId) {
354403
hidePreview();
@@ -365,6 +414,11 @@ function onViewerRender(cameraState) {
365414
// Update navigator projection each frame
366415
updateCameraState(cameraState);
367416
updateNavigator(cameraState);
417+
418+
// Sync rear view camera direction with main viewer (opposite direction)
419+
const lonDeg = (cameraState.yaw * 180) / Math.PI;
420+
const latDeg = (cameraState.pitch * 180) / Math.PI;
421+
syncRearViewCamera(lonDeg, latDeg, cameraState.fov);
368422
}
369423

370424
// ============================================================================
@@ -736,6 +790,7 @@ async function refreshTargetsAndNearby() {
736790
// Update navigator and minimap with new targets
737791
setCameraConfig(metadata.camera);
738792
setTargets(metadata.targets);
793+
setRearViewTargets(metadata.targets, metadata.camera);
739794
updateTargets(metadata.targets);
740795

741796
// Re-fetch nearby photos
@@ -803,16 +858,21 @@ function handleNearbySelect(nearbyPhoto) {
803858
// KEYBOARD SHORTCUTS
804859
// ============================================================================
805860

861+
// ── Held-key state for smooth WASD rotation ──
862+
const heldKeys = new Set();
863+
let heldKeysAnimId = null;
864+
let lastHeldKeyTime = 0;
865+
const ROTATION_RATE = 20; // degrees per second
866+
806867
function onKeyDown(e) {
807868
// Don't handle shortcuts when typing in inputs
808869
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
809870

810871
// Ctrl+S / Cmd+S = Save
811872
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
812873
e.preventDefault();
813-
if (isDirty()) {
814-
handleSave();
815-
}
874+
if (isDirty()) handleSave();
875+
return;
816876
}
817877

818878
// Escape = Cancel modes / Deselect target
@@ -823,38 +883,147 @@ function onKeyDown(e) {
823883
} else if (state.selectedTargetId) {
824884
deselectTarget();
825885
}
886+
return;
826887
}
827888

828-
// Review workflow shortcuts
829-
// R = Toggle reviewed
830-
if (e.key === 'r' && !e.ctrlKey && !e.metaKey) {
831-
handleMarkReviewed(!state.calibrationReviewed);
889+
// ── Smooth rotation keys (WASD) — held for continuous rotation ──
890+
if (['w', 'a', 's', 'd'].includes(e.key)) {
891+
e.preventDefault();
892+
if (!heldKeys.has(e.key)) {
893+
heldKeys.add(e.key);
894+
if (!heldKeysAnimId) startHeldKeysLoop();
895+
}
896+
return;
832897
}
833898

834-
// N or ] = Next photo
835-
if ((e.key === 'n' || e.key === ']') && !e.ctrlKey && !e.metaKey) {
836-
handleNextPhoto();
837-
}
899+
// ── Instant action shortcuts ──
838900

839-
// P or [ or Q = Previous photo
840-
if ((e.key === 'p' || e.key === '[' || e.key === 'q') && !e.ctrlKey && !e.metaKey) {
841-
handlePrevPhoto();
901+
// Q = Previous photo
902+
if (e.key === 'q') handlePrevPhoto();
903+
904+
// E = Save + mark reviewed + next
905+
if (e.key === 'e') handleMarkReviewedAndNext();
906+
907+
// R = Toggle hide selected marker (requires selected target)
908+
if (e.key === 'r' && state.selectedTargetId) {
909+
stateSetTargetHidden(state.selectedTargetId, !isTargetHidden(state.selectedTargetId));
842910
}
843911

844-
// E = Mark reviewed + go to next (efficient workflow)
845-
if (e.key === 'e' && !e.ctrlKey && !e.metaKey) {
846-
handleMarkReviewedAndNext();
912+
// F = Toggle set-from-click mode (requires selected target)
913+
if (e.key === 'f' && state.selectedTargetId) {
914+
setSetFromClickMode(!state.setFromClickMode);
915+
refreshCursor();
847916
}
848917

849-
// G = Toggle perspective grid
850-
if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
918+
// G = Toggle spherical grid
919+
if (e.key === 'g') {
851920
const newState = !isGridVisible();
852921
setGridVisible(newState);
853-
setGroundGridVisible(newState);
854-
setGridToggleState(newState);
922+
setSphericalGridToggleState(newState);
923+
}
924+
925+
// Z = Reset mesh_rotation_z to 0
926+
if (e.key === 'z') {
927+
resetMeshRotationZ();
928+
}
929+
930+
// X = Reset mesh_rotation_x to 0
931+
if (e.key === 'x') {
932+
resetMeshRotationX();
855933
}
856934
}
857935

936+
function onKeyUp(e) {
937+
if (['w', 'a', 's', 'd'].includes(e.key)) {
938+
heldKeys.delete(e.key);
939+
if (heldKeys.size === 0) {
940+
stopHeldKeysLoop();
941+
flushHeldKeyState();
942+
}
943+
}
944+
}
945+
946+
function onWindowBlur() {
947+
if (heldKeys.size > 0) {
948+
heldKeys.clear();
949+
stopHeldKeysLoop();
950+
flushHeldKeyState();
951+
}
952+
}
953+
954+
function startHeldKeysLoop() {
955+
lastHeldKeyTime = 0;
956+
function tick(timestamp) {
957+
if (heldKeys.size === 0) {
958+
heldKeysAnimId = null;
959+
return;
960+
}
961+
if (!lastHeldKeyTime) lastHeldKeyTime = timestamp;
962+
const dt = Math.min((timestamp - lastHeldKeyTime) / 1000, 0.1);
963+
lastHeldKeyTime = timestamp;
964+
const delta = ROTATION_RATE * dt;
965+
966+
if (heldKeys.has('w')) adjustMeshRotationZ(+delta, true);
967+
if (heldKeys.has('s')) adjustMeshRotationZ(-delta, true);
968+
if (heldKeys.has('a')) adjustMeshRotationX(+delta, true);
969+
if (heldKeys.has('d')) adjustMeshRotationX(-delta, true);
970+
971+
heldKeysAnimId = requestAnimationFrame(tick);
972+
}
973+
heldKeysAnimId = requestAnimationFrame(tick);
974+
}
975+
976+
function stopHeldKeysLoop() {
977+
if (heldKeysAnimId) {
978+
cancelAnimationFrame(heldKeysAnimId);
979+
heldKeysAnimId = null;
980+
}
981+
lastHeldKeyTime = 0;
982+
}
983+
984+
/** Triggers panel re-render after held-key rotation ends. */
985+
function flushHeldKeyState() {
986+
const x = state.editedMeshRotationX;
987+
if (x !== null && x !== undefined) {
988+
setMeshRotationX(x); // non-silent → triggers notify/re-render
989+
} else {
990+
const z = state.editedMeshRotationZ;
991+
if (z !== null && z !== undefined) {
992+
setMeshRotationZ(z);
993+
}
994+
}
995+
}
996+
997+
function adjustMeshRotationX(delta, silent = false) {
998+
const current = state.editedMeshRotationX ?? 0;
999+
const newVal = Math.max(-30, Math.min(30, current + delta));
1000+
setMeshRotationX(newVal, silent);
1001+
viewerSetMeshRotationX(newVal);
1002+
updateRearViewRotation(state.editedMeshRotationY ?? 180, newVal, state.editedMeshRotationZ ?? 0);
1003+
}
1004+
1005+
function adjustMeshRotationZ(delta, silent = false) {
1006+
const current = state.editedMeshRotationZ ?? 0;
1007+
const newVal = Math.max(-30, Math.min(30, current + delta));
1008+
setMeshRotationZ(newVal, silent);
1009+
viewerSetMeshRotationZ(newVal);
1010+
updateRearViewRotation(state.editedMeshRotationY ?? 180, state.editedMeshRotationX ?? 0, newVal);
1011+
}
1012+
1013+
function resetMeshRotationX() {
1014+
const original = state.originalMeshRotationX ?? 0;
1015+
setMeshRotationX(original);
1016+
viewerSetMeshRotationX(original);
1017+
updateRearViewRotation(state.editedMeshRotationY ?? 180, original, state.editedMeshRotationZ ?? 0);
1018+
}
1019+
1020+
function resetMeshRotationZ() {
1021+
const original = state.originalMeshRotationZ ?? 0;
1022+
setMeshRotationZ(original);
1023+
viewerSetMeshRotationZ(original);
1024+
updateRearViewRotation(state.editedMeshRotationY ?? 180, state.editedMeshRotationX ?? 0, original);
1025+
}
1026+
8581027
async function handleMarkReviewedAndNext() {
8591028
if (isDirty()) {
8601029
await handleSave();

0 commit comments

Comments
 (0)