From e2b778cb8662278a7c46ff820ea98646e5e37b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Mon, 13 Oct 2025 13:03:54 +0200 Subject: [PATCH] feat(#8): :sparkles: Made functionaliry for Accepted filtering Created a separate panel for accepted filtering. When the button is clicked the colors of the points are either black for not accepted and white for accepted points. The panel also have a description for this. --- index.html | 7 ++ src/Accepted/accepted.css | 56 ++++++++++++ src/Accepted/accepted.js | 179 ++++++++++++++++++++++++++++++++++++++ src/potreeViewer.js | 57 +++++++++++- 4 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 src/Accepted/accepted.css create mode 100644 src/Accepted/accepted.js diff --git a/index.html b/index.html index b9c3d02..f29b3c6 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,13 @@ rel="stylesheet" href="/src/MeasurementControl/measurementsPanel.css" /> + + diff --git a/src/Accepted/accepted.css b/src/Accepted/accepted.css new file mode 100644 index 0000000..482defa --- /dev/null +++ b/src/Accepted/accepted.css @@ -0,0 +1,56 @@ +/* Accepted Filter: action button */ +#doAcceptedFilter { + display: flex; + width: 100%; + margin: 6px 0 10px; + padding: 10px 10px; + font-size: 13px; + font-weight: 500; + background-color: #3a3a3a; + color: #ffffff; + border: 1px solid #555; + border-radius: 4px; + cursor: pointer; + transition: + background-color 0.2s ease, + transform 0.1s ease; +} +#doAcceptedFilter:hover { + background-color: #505050; +} +#doAcceptedFilter:active { + transform: scale(0.97); + background-color: #606060; +} + +/* Legend container */ +#accepted_legend { + margin-top: 10px; + padding-left: 4px; + } + + /* Legend rows */ + #accepted_legend .legend-row { + display: flex; + align-items: center; + margin: 3px 0; + font-size: 13px; + color: #ddd; /* visible text */ + } + + /* Color boxes */ + #accepted_legend .legend-color { + width: 16px; + height: 16px; + border: 1px solid #777; + margin-right: 8px; + border-radius: 2px; + } + + #accepted_legend .legend-color.accepted { + background-color: #fff; + } + + #accepted_legend .legend-color.not-accepted { + background-color: #000; + } \ No newline at end of file diff --git a/src/Accepted/accepted.js b/src/Accepted/accepted.js new file mode 100644 index 0000000..0470f54 --- /dev/null +++ b/src/Accepted/accepted.js @@ -0,0 +1,179 @@ +function createAcceptedPanel() { + const container = document.getElementById('accepted_list') + if (container) return + + const menu = document.getElementById('potree_menu') + if (!menu) return + + const header = document.createElement('h3') + header.id = 'menu_accepted' + header.innerHTML = 'Accepted Filter' + + const panel = document.createElement('div') + panel.className = 'pv-menu-list' + panel.innerHTML = '
' + + // Place above Appearance if possible (same as Elevation) + const appearance = document.getElementById('menu_appearance') + if (appearance) { + menu.insertBefore(panel, appearance) + menu.insertBefore(header, panel) + } else { + menu.appendChild(header) + menu.appendChild(panel) + } + + if (window.$ && window.$(menu).accordion) { + try { + window.$(menu).accordion('refresh') + } catch (e) {} + } + + // Simple toggle if accordion doesn’t manage it + header.addEventListener('click', () => { + panel.style.display = panel.style.display === 'none' ? '' : 'none' + }) +} + +function ensureActivationButton(hooks) { + const list = document.getElementById('accepted_list') + if (!list) return + if (list.querySelector('#doAcceptedFilter')) return + + const btn = document.createElement('button') + btn.id = 'doAcceptedFilter' + btn.type = 'button' + btn.textContent = 'Activate accepted filter' + btn.addEventListener('click', () => { + if (hooks && typeof hooks.onActivateAccepted === 'function') { + hooks.onActivateAccepted(); + } + const legend = document.getElementById('accepted_legend'); + if (legend) legend.style.display = 'block'; // ensure legend shows + }); + + list.insertBefore(btn, list.firstChild) +} + +function ensureAcceptedLegend() { + const list = document.getElementById('accepted_list') + if (!list) return null + + let legend = list.querySelector('#accepted_legend') + if (!legend) { + legend = document.createElement('div') + legend.id = 'accepted_legend' + legend.style.display = 'none' // hidden until button click + legend.innerHTML = ` +
+
+ Accepted points +
+
+
+ Not accepted points +
+ ` + list.appendChild(legend) + } + return legend + } + + // Show/hide legend (called from viewer hook) + export function toggleAcceptedLegend(show) { + const legend = document.getElementById('accepted_legend') + if (legend) legend.style.display = show ? 'block' : 'none' + } + +// Ensure our list wrapper exists inside the Accepted panel +function ensureAcceptedListUL() { + const host = document.getElementById('accepted_list') + if (!host) return null + + let ul = host.querySelector('#accepted_ui') + if (!ul) { + ul = document.createElement('ul') + ul.id = 'accepted_ui' + ul.className = 'pv-menu-list' + host.appendChild(ul) + } + return ul +} + +// Move ALL current children from Potree's extra_container into our UL +function moveAcceptedChildrenOnce() { + const source = document.querySelector('#materials\\.extra_container') + const targetUL = ensureAcceptedListUL() + if (!source || !targetUL) return false + + // Move only the elements that should be visible in a pv-menu-list (divider + li) + const nodes = [...source.children].filter( + (n) => n.classList.contains('divider') || n.tagName.toLowerCase() === 'li' + ) + + if (nodes.length === 0) return false + + for (const n of nodes) { + targetUL.appendChild(n) // moving preserves event listeners + } + return true +} + +// Observe Potree's extra_container for FUTURE children; move them as they arrive +function observeAndMirrorExtraContainer() { + const source = document.querySelector('#materials\\.extra_container') + if (!source) return null + + const targetUL = ensureAcceptedListUL() + if (!targetUL) return null + + const childObserver = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.type !== 'childList') + continue + // On added nodes, move any
  • or .divider into our list + ;[...m.addedNodes].forEach((node) => { + if (!(node instanceof HTMLElement)) return + const isLi = node.tagName && node.tagName.toLowerCase() === 'li' + const isDivider = node.classList && node.classList.contains('divider') + if (isLi || isDivider) { + targetUL.appendChild(node) + } + }) + } + }) + + // Only watch direct children; Potree appends li/divider at this level + childObserver.observe(source, { childList: true }) + return childObserver +} + +export function initAcceptedControls(viewer, hooks = {}) { + // 1) Always render panel + button + createAcceptedPanel() + ensureActivationButton(hooks) + ensureAcceptedListUL() + ensureAcceptedLegend() + + // 2) Wait for Potree to create extra_container, then move children & keep mirroring +// const menu = +// document.getElementById('potree_menu') || +// document.getElementById('menu') || +// document.body + +// const onceObserver = new MutationObserver(() => { +// const source = document.querySelector('#materials\\.extra_container') +// if (source) { +// // stop the "finder" observer +// onceObserver.disconnect() + +// // Move whatever is present right now +// moveAcceptedChildrenOnce() + +// // Keep mirroring anything Potree adds later +// observeAndMirrorExtraContainer() +// } +// }) + + //onceObserver.observe(menu, { childList: true, subtree: true }) +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index fb709f0..dd81e01 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,5 +1,9 @@ -import { initElevationControls } from './ElevationControl/elevationControl.js' +import { + initElevationControls, + autoSelectFirstPointCloud +} from './ElevationControl/elevationControl.js' import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' +import { initAcceptedControls, toggleAcceptedLegend } from './Accepted/accepted.js' import { ecef } from './config.js' /** @@ -35,7 +39,54 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { $('#menu_filters').next().show() viewer.toggleSidebar() - initElevationControls(viewer) + initElevationControls(viewer, { + onActivateElevation: () => { + const pc = viewer.scene.pointclouds[0] + if (!pc) { + console.warn('[Elevation] No point cloud loaded yet.') + return + } + + // Switch to elevation coloring + pc.material.activeAttributeName = 'elevation' + pc.material.gradient = Potree.Gradients['VIRIDIS'] + // Build/refresh Potree's Materials/Elevation UI in the sidebar + autoSelectFirstPointCloud() + // One-shot render because default loop is disabled + viewer.render() + } + }) + + initAcceptedControls(viewer, { + onActivateAccepted: () => { + const pc = viewer.scene.pointclouds[0] + if (!pc) { + console.warn('[Accepted] No point cloud loaded yet.') + return + } + // Switch to attribute-based coloring using the 'accepted' attribute + pc.material.activeAttributeName = 'accepted' + pc.material.gradient = Potree.Gradients['GRAYSCALE'] + pc.material.gradientRange = [0, 1] + + // 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 })) + } + + + // One-shot render since default loop is disabled + viewer.render() + + toggleAcceptedLegend(true) + + } + }) + initMeasurementsPanel(viewer) }) @@ -58,8 +109,6 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { pc.material.activeAttributeName = 'elevation' pc.material.gradient = Potree.Gradients['VIRIDIS'] - e.pointcloud.projection = ecef - // Initialize camera position and target point (manually chosen) viewer.scene.view.setView( [1993552.9, 87954.487, 7134018.721], // Initial camera position