Skip to content

Commit

Permalink
perf(#8): ⚡ Optimize elevation control and accepted filtering
Browse files Browse the repository at this point in the history
Changes styles, hooks, css, scrolling, visuals to make it a better user experience
  • Loading branch information
mariewah committed Oct 13, 2025
1 parent e2b778c commit eae6463
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 55 deletions.
6 changes: 3 additions & 3 deletions src/Accepted/accepted.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
padding: 10px 10px;
font-size: 13px;
font-weight: 500;
background-color: #3a3a3a;
background-color: #636262;
color: #ffffff;
border: 1px solid #555;
border-radius: 4px;
Expand All @@ -16,11 +16,11 @@
transform 0.1s ease;
}
#doAcceptedFilter:hover {
background-color: #505050;
background-color: #8f8f8f;
}
#doAcceptedFilter:active {
transform: scale(0.97);
background-color: #606060;
background-color: #a8a6a6;
}

/* Legend container */
Expand Down
34 changes: 34 additions & 0 deletions src/ElevationControl/elevationControl.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#btnDoElevationControl {
display: flex;
width: 100%;
margin: 6px 0 10px;
padding: 10px 10px;
font-size: 13px;
font-weight: 500;
background-color: #636262;
color: #ffffff;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
transition:
background-color 0.2s ease,
transform 0.1s ease;
}

#btnDoElevationControl:hover {
background-color: #aeaeae;
}

#btnDoElevationControl:active {
transform: scale(0.97);
background-color: #c1c1c1;
}

#elevation_ui { list-style: none; margin: 0; padding: 0; }
#elevation_list .divider {
height: 1px;
margin: 6px 0;
background: rgba(255,255,255,0.08);
border: 0;
}

94 changes: 51 additions & 43 deletions src/ElevationControl/elevationControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,40 +37,47 @@ window.createElevationPanel = function createElevationPanel(viewer) {
}
}

/**
* Select the fist pointcloud in the sidebar so that the Elevation section is built
*/
function autoSelectFirstPointCloud() {
const cloudIcon = document.querySelector(
'#scene_objects i.jstree-themeicon-custom'
)
if (cloudIcon) {
cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true }))
return true
}
return false
}
function ensureActivationButton(hooks) {
const list = document.getElementById('elevation_list')
if (!list) return
if (list.querySelector('#btnDoElevationControl')) return

/**
* Disable any further clicks of the pointcloud icon in the sidebar
*/
function disableFirstPointCloudNode() {
// find the <li> that holds the first cloud icon
const icon = document.querySelector(
'#scene_objects i.jstree-themeicon-custom'
)
const li = icon ? icon.closest('li') : null
if (!li) return
//visually/DOM disable anchor clicks
const a = li.querySelector('a')
if (a) {
a.style.pointerEvents = 'none'
a.style.opacity = 0.5
a.classList.remove('jstree-clicked')
}
const btn = document.createElement('button')
btn.id = 'btnDoElevationControl'
btn.type = 'button'
btn.textContent = 'Activate elevation control'

btn.addEventListener('click', () => {
if (hooks && typeof hooks.onActivateElevation === 'function') {
hooks.onActivateElevation()
}
})

// put the button at the very top of the elevation list
list.insertBefore(btn, list.firstChild)
}

//(re)connect the elevation labels to the slider after the container is moved (was not handled by default)

// /**
// * Disable any further clicks of the pointcloud icon in the sidebar
// */
// function disableFirstPointCloudNode() {
// // find the <li> that holds the first cloud icon
// const icon = document.querySelector(
// '#scene_objects i.jstree-themeicon-custom'
// )
// const li = icon ? icon.closest('li') : null
// if (!li) return
// //visually/DOM disable anchor clicks
// const a = li.querySelector('a')
// if (a) {
// a.style.pointerEvents = 'none'
// a.style.opacity = 0.5
// a.classList.remove('jstree-clicked')
// }
// }

// (re)connect the elevation labels to the slider after the container is moved
function rebindElevationLabel() {
const slider = window.jQuery ? window.jQuery('#sldHeightRange') : null
const label = document.getElementById('lblHeightRange')
Expand All @@ -82,14 +89,12 @@ function rebindElevationLabel() {
label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`
}

// Adjust slider limits
slider.slider({
min: -10000,
max: 0,
values: [-10000, 0]
})

//clear any old namespaced handlers and attach fresh ones
slider.off('slide.custom slidestop.custom change.custom')
slider.on('slide.custom', update)
slider.on('slidestop.custom change.custom', update)
Expand All @@ -102,36 +107,39 @@ function moveElevationContainer() {
const elevationContainer = document.querySelector(
'#materials\\.elevation_container'
)
if (!elevationContainer) return false
if (!elevationContainer || !target) return false
target.appendChild(elevationContainer)
rebindElevationLabel()
return true
}



//initiate and orchestrate all funcitons to render the Evelation control section of the sidebar propperly
export function initElevationControls(viewer) {
//Creates the section
export function initElevationControls(viewer, hooks = {}) {
// 1) Ensure the panel exists
createElevationPanel(viewer)

//Only move the ElevationContainer if the source container to exist
// 2) Add the activation button (top of list)
ensureActivationButton(hooks)

// 3) Observe and move Potree’s internal elevation UI when it appears
const menu =
document.getElementById('potree_menu') ||
document.getElementById('menu') ||
document.body

const observer = new MutationObserver(() => {
const found = document.querySelector('#materials\\.elevation_container')
if (found) {
observer.disconnect()
//Move and rebind once it exists
const ok = moveElevationContainer()
if (!ok) console.warn('[Elevation] moveElevationContainer failed')
}
})
observer.observe(menu, { childList: true, subtree: true })

//Trigger Potree to build Materials UI by selecting the first point cloud (if nothing selected yet)
if (autoSelectFirstPointCloud()) {
//Prevent multiple clicks on the cloud icon
disableFirstPointCloudNode()
}
//disableFirstPointCloudNode()


}
115 changes: 106 additions & 9 deletions src/potreeViewer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
initElevationControls,
autoSelectFirstPointCloud
} from './ElevationControl/elevationControl.js'
import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js'
import { initAcceptedControls, toggleAcceptedLegend } from './Accepted/accepted.js'
Expand Down Expand Up @@ -51,7 +50,13 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) {
pc.material.activeAttributeName = 'elevation'
pc.material.gradient = Potree.Gradients['VIRIDIS']
// Build/refresh Potree's Materials/Elevation UI in the sidebar
autoSelectFirstPointCloud()
suppressSidebarAutoScroll(() => {
const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom');
if (cloudIcon) {
cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
});

// One-shot render because default loop is disabled
viewer.render()
}
Expand All @@ -71,13 +76,12 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) {

// Ensure Materials UI is present/updated (your Elevation panel pattern)
// If you have a shared autoSelectFirstPointCloud(), call it here.
const cloudIcon = document.querySelector(
'#scene_objects i.jstree-themeicon-custom'
)
if (cloudIcon) {
cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}

suppressSidebarAutoScroll(() => {
const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom');
if (cloudIcon) {
cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
});

// One-shot render since default loop is disabled
viewer.render()
Expand Down Expand Up @@ -183,3 +187,96 @@ function overrideShaderForGradient(pc) {
this.needsUpdate = true
}
}

// Freeze all scrollable ancestors of a given root during an action (e.g., jsTree select)
function suppressSidebarAutoScroll(action, holdMs = 350) {
// anchor on the tree root; fall back to the menu if not found
const treeRoot =
document.querySelector('#scene_objects') ||
document.querySelector('#potree_menu') ||
document.body;

// collect ALL scrollable ancestors (including the tree UL itself)
const scrollers = [];
let el = treeRoot;
while (el) {
const sh = el.scrollHeight;
const ch = el.clientHeight;
const canScroll = sh && ch && sh > ch;
if (canScroll) scrollers.push(el);
el = el.parentElement;
}
if (!scrollers.length) { action(); return; }

// snapshot state for each scroller
const states = scrollers.map((s) => ({
el: s,
top: s.scrollTop,
left: s.scrollLeft,
overflow: s.style.overflow,
}));

// guard scroll by snapping back; also hide overflow to avoid flicker
const handlers = new Map();
states.forEach(({ el, top, left }) => {
const onScroll = () => { el.scrollTop = top; el.scrollLeft = left; };
el.addEventListener('scroll', onScroll, { passive: true });
el.style.overflow = 'hidden';
handlers.set(el, onScroll);
});

// temporarily neutralize scrollIntoView/focus scrolling
const origScrollIntoView = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = function () { /* no-op */ };

const origFocus = HTMLElement.prototype.focus;
HTMLElement.prototype.focus = function (opts) {
// force preventScroll behavior even if caller didn't ask
try { origFocus.call(this, { ...(opts || {}), preventScroll: true }); }
catch { origFocus.call(this); }
};

// also neutralize window scrolls briefly (arrow keys, space, etc.)
const preventWindowScroll = (e) => {
const keys = ['ArrowDown','ArrowUp','PageDown','PageUp','Home','End',' '];
if (e.type === 'keydown' && !keys.includes(e.key)) return;
e.preventDefault();
};
window.addEventListener('wheel', preventWindowScroll, { passive: false });
window.addEventListener('touchmove', preventWindowScroll, { passive: false });
window.addEventListener('keydown', preventWindowScroll, { passive: false });

try {
action();
} finally {
const until = performance.now() + holdMs;

const restoreLoop = () => {
// keep snapping until the selection animations/handlers settle
states.forEach(({ el, top, left }) => { el.scrollTop = top; el.scrollLeft = left; });

// if a jsTree node grabbed focus, blur it so it won't re-scroll later
const active = document.activeElement;
if (active && active.closest && active.closest('#scene_objects')) {
active.blur();
}

if (performance.now() < until) {
requestAnimationFrame(restoreLoop);
} else {
// full restore
Element.prototype.scrollIntoView = origScrollIntoView;
HTMLElement.prototype.focus = origFocus;
window.removeEventListener('wheel', preventWindowScroll);
window.removeEventListener('touchmove', preventWindowScroll);
window.removeEventListener('keydown', preventWindowScroll);
states.forEach(({ el, overflow }) => {
const h = handlers.get(el);
if (h) el.removeEventListener('scroll', h);
el.style.overflow = overflow;
});
}
};
requestAnimationFrame(restoreLoop);
}
}

0 comments on commit eae6463

Please sign in to comment.