-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(#4): ✨ Made a function for storing annotations
The data is sent by API to the small express server and stored in a json. the objects in the json will be fetched when opening the app and displayed
- Loading branch information
Showing
3 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "version": 1, | ||
| "pointcloudKey": "_pointclouds_data_converted_metadata_json", | ||
| "folders": { | ||
| "General": [ | ||
| { | ||
| "title": "Test1", | ||
| "description": "Test1 description", | ||
| "position": [ | ||
| 1183815.569987793, | ||
| 63692.700009765635, | ||
| 6243873.499987793 | ||
| ], | ||
| "cameraPosition": null, | ||
| "cameraTarget": null, | ||
| "children": [] | ||
| } | ||
| ] | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| export function initAnnotationPersistence(viewer, options = {}) { | ||
| const jsonUrl = options.jsonUrl || '/annotations/annotations.json' | ||
| const defaultSaveUrl = (() => { | ||
| try { | ||
| if ( | ||
| typeof window !== 'undefined' && | ||
| window.location && | ||
| window.location.port === '5173' | ||
| ) { | ||
| // Dev fallback: post directly to API server to avoid proxy issues | ||
| return 'http://localhost:5174/api/annotations' | ||
| } | ||
| } catch {} | ||
| return '/api/annotations' | ||
| })() | ||
| const saveUrl = options.saveUrl || defaultSaveUrl | ||
| const autosave = Boolean(options.autosave) | ||
| const folderResolver = options.getAnnotationFolder || defaultGetFolder | ||
| const folderSetter = options.setAnnotationFolder || defaultSetFolder | ||
| const key = deriveKey(options.pointcloudUrl || options.pointcloudPath) | ||
| const STORAGE_KEY = `molloy_annotations_snapshot_${key}` | ||
|
|
||
| function defaultGetFolder(ann) { | ||
| return ann?.userData?.folder || 'General' | ||
| } | ||
| function defaultSetFolder(ann, folder) { | ||
| if (!ann.userData) ann.userData = {} | ||
| ann.userData.folder = folder || 'General' | ||
| } | ||
|
|
||
| function posToArray(p) { | ||
| if (!p) return null | ||
| if (Array.isArray(p)) return p | ||
| if (typeof p.x === 'number') return [p.x, p.y, p.z] | ||
| if (typeof p.toArray === 'function') return p.toArray() | ||
| return null | ||
| } | ||
|
|
||
| function serializeNode(ann) { | ||
| return { | ||
| title: ann.title || '', | ||
| description: ann.description || '', | ||
| position: posToArray( | ||
| ann.position || ann._position || ann?.marker?.position | ||
| ), | ||
| cameraPosition: posToArray(ann.cameraPosition), | ||
| cameraTarget: posToArray(ann.cameraTarget), | ||
| children: (ann.children || []).map(serializeNode) | ||
| } | ||
| } | ||
|
|
||
| function serializeGrouped() { | ||
| const root = viewer.scene.annotations | ||
| const folders = {} | ||
| for (const ann of root.children || []) { | ||
| const f = folderResolver(ann) || 'General' | ||
| if (!folders[f]) folders[f] = [] | ||
| folders[f].push(serializeNode(ann)) | ||
| } | ||
| return { version: 1, pointcloudKey: key, folders } | ||
| } | ||
|
|
||
| async function loadFromJson() { | ||
| try { | ||
| const res = await fetch(jsonUrl, { cache: 'no-store' }) | ||
| if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) | ||
| const data = await res.json() | ||
| applyJsonToScene(data) | ||
| // snapshot to localStorage for quick restore | ||
| try { | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) | ||
| } catch {} | ||
| } catch (err) { | ||
| // fallback to local snapshot | ||
| try { | ||
| const snap = localStorage.getItem(STORAGE_KEY) | ||
| if (snap) applyJsonToScene(JSON.parse(snap)) | ||
| } catch {} | ||
| } | ||
| } | ||
|
|
||
| function applyJsonToScene(json) { | ||
| if (!json || !json.folders) return | ||
| const root = viewer.scene.annotations | ||
| for (const ch of [...(root.children || [])]) root.remove(ch) | ||
|
|
||
| const folders = json.folders || {} | ||
| for (const folderName of Object.keys(folders)) { | ||
| const list = folders[folderName] || [] | ||
| for (const item of list) addAnnotationRec(root, item, folderName) | ||
| } | ||
| } | ||
|
|
||
| function addAnnotationRec(parent, item, folderName) { | ||
| const pos = item.position || [0, 0, 0] | ||
| const ann = new Potree.Annotation({ | ||
| position: new THREE.Vector3(pos[0], pos[1], pos[2]), | ||
| title: item.title || '' | ||
| }) | ||
| ann.description = item.description || '' | ||
| if (item.cameraPosition) | ||
| ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) | ||
| if (item.cameraTarget) | ||
| ann.cameraTarget = new THREE.Vector3(...item.cameraTarget) | ||
| if (folderSetter) folderSetter(ann, folderName) | ||
| parent.add(ann) | ||
| watchAnnotationDeep(ann) | ||
| for (const child of item.children || []) | ||
| addAnnotationRec(ann, child, folderName) | ||
| } | ||
|
|
||
| function deriveKey(urlLike) { | ||
| try { | ||
| const u = String(urlLike || '').trim() | ||
| if (!u) return 'default' | ||
| return u | ||
| .replace(/[^a-z0-9]+/gi, '_') | ||
| .toLowerCase() | ||
| .slice(-80) | ||
| } catch { | ||
| return 'default' | ||
| } | ||
| } | ||
|
|
||
| function getAnnotationsJSON() { | ||
| return serializeGrouped() | ||
| } | ||
|
|
||
| function snapshotToLocalStorage() { | ||
| try { | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeGrouped())) | ||
| } catch {} | ||
| } | ||
|
|
||
| async function saveToServer(payload) { | ||
| const data = payload || serializeGrouped() | ||
| try { | ||
| const headers = { 'Content-Type': 'application/json' } | ||
| const res = await fetch(saveUrl, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(data) | ||
| }) | ||
| if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) | ||
| return await res.json() | ||
| } catch (err) { | ||
| throw err | ||
| } | ||
| } | ||
|
|
||
| // Autosnapshot after add/remove | ||
| const root = viewer.scene.annotations | ||
| let snapshotTimer = null | ||
| const debouncedSnapshot = () => { | ||
| if (snapshotTimer) cancelAnimationFrame(snapshotTimer) | ||
| snapshotTimer = requestAnimationFrame(async () => { | ||
| snapshotToLocalStorage() | ||
| if (autosave) { | ||
| try { | ||
| await saveToServer() | ||
| } catch {} | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // Patch Potree.Annotation.prototype to watch all additions/removals in the tree | ||
| if (Potree?.Annotation && !Potree.Annotation.prototype._persistPatched) { | ||
| Potree.Annotation.prototype._persistPatched = true | ||
| const _protoAdd = Potree.Annotation.prototype.add | ||
| const _protoRemove = Potree.Annotation.prototype.remove | ||
| Potree.Annotation.prototype.add = function (...args) { | ||
| const child = args[0] | ||
| const r = _protoAdd.apply(this, args) | ||
| if (child) watchAnnotationDeep(child) | ||
| debouncedSnapshot() | ||
| return r | ||
| } | ||
| Potree.Annotation.prototype.remove = function (...args) { | ||
| const r = _protoRemove.apply(this, args) | ||
| debouncedSnapshot() | ||
| return r | ||
| } | ||
| } | ||
|
|
||
| function watchAnnotationDeep(ann) { | ||
| if (!ann || ann._persistWatched) return | ||
| ann._persistWatched = true | ||
| watchAnnotation(ann) | ||
| ;(ann.children || []).forEach(watchAnnotationDeep) | ||
| } | ||
|
|
||
| function defineWatchedProp(obj, key) { | ||
| try { | ||
| let _val = obj[key] | ||
| Object.defineProperty(obj, key, { | ||
| configurable: true, | ||
| enumerable: true, | ||
| get() { | ||
| return _val | ||
| }, | ||
| set(v) { | ||
| _val = v | ||
| debouncedSnapshot() | ||
| } | ||
| }) | ||
| } catch {} | ||
| } | ||
|
|
||
| function watchAnnotation(ann) { | ||
| defineWatchedProp(ann, 'title') | ||
| defineWatchedProp(ann, 'description') | ||
| defineWatchedProp(ann, 'cameraPosition') | ||
| defineWatchedProp(ann, 'cameraTarget') | ||
| } | ||
|
|
||
| loadFromJson() | ||
| watchAnnotationDeep(root) | ||
|
|
||
| return { | ||
| getAnnotationsJSON, | ||
| snapshotToLocalStorage, | ||
| loadFromJson, | ||
| serializeGrouped, | ||
| saveToServer | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters