Skip to content

Commit

Permalink
feat(#4): ✏️ add description in the sidebar and make it editable + sm…
Browse files Browse the repository at this point in the history
…all refactor

Now it is possible to see the Annotation Description in the sidebar, and if double-clicked, it can be edited. The Annotation is mirrored in the liev scene object. Lastly, there is a small refactor of the code to have some helpers
  • Loading branch information
franmagn committed Oct 15, 2025
1 parent 71614e8 commit 8b701d3
Showing 1 changed file with 187 additions and 6 deletions.
193 changes: 187 additions & 6 deletions src/AnnotationControl/annotationPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,53 @@ export function initAnnotationsPanel(viewer) {
return _uuidToIndex.get(uuid)
}

// Helper: safe accessors and utilities for jsTree and annotation descriptions
function _getJSTree() {
try {
return $('#jstree_scene').jstree && $('#jstree_scene').jstree()
} catch (e) {
return null
}
}

function _getJSTreeInstance() {
try {
return ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) ||
(typeof $.jstree !== 'undefined' && $.jstree.reference && $.jstree.reference('#jstree_scene')) ||
null
} catch (e) {
return null
}
}

function _getAnnotationsRoot() {
const t = _getJSTree()
try {
return t ? t.get_json('annotations') : null
} catch (e) {
return null
}
}

function _findNodeInAnnotationsRootByUUID(root, uuid) {
if (!root || !root.children || !uuid) return null
return root.children.find((c) => (c.data && c.data.uuid) === uuid) || null
}

function _getDescriptionForUUID(uuid, nodeData) {
// Prefer live annotation description, then node data (desc/description)
if (!uuid && !nodeData) return ''
try {
const live = _findLiveAnnotationByUUID(uuid)
if (live) {
if (live.data && typeof live.data.description !== 'undefined') return String(live.data.description || '')
if (typeof live.description !== 'undefined') return String(live.description || '')
}
} catch (e) {}
const ann = nodeData || {}
return (ann.description && String(ann.description)) || (ann.desc && String(ann.desc)) || ''
}

function _renameJSTreeNode(nodeId, text) {
try {
if (window.$ && $.jstree) {
Expand Down Expand Up @@ -126,10 +173,9 @@ export function initAnnotationsPanel(viewer) {
function updateAnnotationsList() {
// Implementation for listing annotations
targetContainer.innerHTML = ''
const annotationsTree =
$('#jstree_scene').jstree && $('#jstree_scene').jstree()
const annotationsTree = _getJSTree()
if (!annotationsTree) return
let annotationsRoot = annotationsTree.get_json('annotations')
let annotationsRoot = _getAnnotationsRoot()
if (
!annotationsRoot ||
!annotationsRoot.children ||
Expand All @@ -142,8 +188,8 @@ export function initAnnotationsPanel(viewer) {
return
}
// Assign monotonic indices for any node UUIDs encountered and rename unlabeled nodes
try {
const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n }))
try {
const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n }))
for (const it of items) {
const uuid = (it.data && it.data.uuid) || null
if (!uuid) continue
Expand Down Expand Up @@ -218,7 +264,7 @@ export function initAnnotationsPanel(viewer) {
annPos ||
vecToArray(ann.cameraTarget) ||
viewer.scene.view.getPivot()
viewer.scene.view.setView(camPos, target, 4000) //animation duration in ms
viewer.scene.view.setView(camPos, target, 1000) //animation duration in ms
}
}
row.appendChild(jumpBtn)
Expand Down Expand Up @@ -285,6 +331,28 @@ export function initAnnotationsPanel(viewer) {

row.appendChild(label)
row.appendChild(delBtn)
// Description view (shows "No description" when empty) and supports dblclick edit
try {
const ann = node.data || {}
const descText = _getDescriptionForUUID(ann.uuid, ann)
const display = descText.trim() ? descText : 'Annotation Description'
const desc = document.createElement('div')
desc.className = 'annotation-desc'
desc.textContent = display
desc.dataset.uuid = (ann && ann.uuid) || ''
desc.dataset.raw = descText.trim()
desc.style.fontSize = '0.85em'
desc.style.marginLeft = '8px'
desc.addEventListener('dblclick', (ev) => {
ev.stopPropagation()
const u = desc.dataset.uuid
if (u) startInlineDescriptionEditForUUID(u)
})
row.appendChild(desc)
} catch (e) {
// ignore description rendering errors
}

// show saved camera and point info
try {
const ann = node.data || {}
Expand Down Expand Up @@ -390,6 +458,119 @@ export function initAnnotationsPanel(viewer) {
})
}

// Start inline editing for an annotation description (multiline)
function startInlineDescriptionEditForUUID(uuid) {
if (!uuid) return
const descEl = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`)
if (!descEl) return
let oldText = descEl.dataset.raw || descEl.textContent || ''
try {
const live = _findLiveAnnotationByUUID(uuid)
if (live) {
if (live.data && typeof live.data.description !== 'undefined') {
oldText = String(live.data.description || '')
} else if (typeof live.description !== 'undefined') {
oldText = String(live.description || '')
}
}
} catch (e) {}

const ta = document.createElement('textarea')
ta.className = 'annotation-edit-textarea'
// Prefill textarea with existing description
ta.value = oldText
ta.rows = 1
ta.style.width = '100%'
descEl.replaceWith(ta)
ta.focus()
try {
ta.select()
} catch (e) {}

function finish(commit) {
const newText = commit ? (ta.value.trim() || '') : oldText
const displayText = newText ? newText : 'Annotation Description'
const newDesc = document.createElement('div')
newDesc.className = 'annotation-desc'
newDesc.textContent = displayText
newDesc.dataset.uuid = uuid
newDesc.dataset.raw = newText
newDesc.style.fontSize = '0.85em'
newDesc.style.marginLeft = '8px'
newDesc.addEventListener('dblclick', (ev) => {
ev.stopPropagation()
startInlineDescriptionEditForUUID(uuid)
})

try {
if (ta.isConnected) {
ta.replaceWith(newDesc)
} else {
const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`)
if (existing) existing.textContent = displayText
}
} catch (e) {
try {
const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`)
if (existing) existing.textContent = displayText
} catch (_) {}
}

_commitEditedDescription(uuid, newText)
}

ta.addEventListener('blur', () => finish(true))
ta.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
// Ctrl+Enter or Cmd+Enter to commit multiline
finish(true)
} else if (e.key === 'Escape') {
finish(false)
}
})
}

function _commitEditedDescription(uuid, description) {
if (!uuid) return
try {
const tree = _getJSTree()
if (tree) {
const annotationsRoot = _getAnnotationsRoot()
const node = _findNodeInAnnotationsRootByUUID(annotationsRoot, uuid)
if (node) {
node.data = node.data || {}
node.data.description = description

try {
const jsTreeInst = _getJSTreeInstance()
if (jsTreeInst) {
const jsNode = jsTreeInst.get_node(node.id)
if (jsNode) {
jsNode.original = jsNode.original || {}
jsNode.original.data = jsNode.original.data || {}
jsNode.original.data.description = description
}
}
} catch (e) {
// If we couldn't access the internal node, continue anyway
}
}
}
} catch (e) {}

try {
const live = _findLiveAnnotationByUUID(uuid)
if (live) {
live.data = live.data || {}
live.data.description = description
if (typeof live.description !== 'undefined') live.description = description
if (typeof live.desc !== 'undefined') live.desc = description
}
} catch (e) {}

setTimeout(updateAnnotationsList, 0)
}

function _commitEditedName(uuid, name) {
if (!uuid) return
// update jsTree node text
Expand Down

0 comments on commit 8b701d3

Please sign in to comment.