From 192f967b683c329435037f2c435e657ce7e31714 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Wed, 22 Apr 2026 15:50:37 +0200 Subject: [PATCH 01/25] fix(LoaderComponents): Add loader and new logic for components loader --- .../ObjectTree/Views/ModelComponents.vue | 147 +++++++++++++++++- app/composables/use_tree_filter.js | 31 ++-- app/stores/data.js | 32 +++- 3 files changed, 184 insertions(+), 26 deletions(-) diff --git a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue index 26b15711..1c62d55b 100644 --- a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue +++ b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue @@ -8,6 +8,7 @@ import { useDataStyleStore } from "@ogw_front/stores/data_style"; import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer"; import { useTreeFilter } from "@ogw_front/composables/use_tree_filter"; import { useTreeviewStore } from "@ogw_front/stores/treeview"; +const LOADER_DELAY_MS = 50; const { id: viewId } = defineProps({ id: { type: String, required: true } }); const emit = defineEmits(["show-menu"]); @@ -17,13 +18,56 @@ const dataStyleStore = useDataStyleStore(); const hybridViewerStore = useHybridViewerStore(); const treeviewStore = useTreeviewStore(); -const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === viewId)); +const currentView = computed(() => + treeviewStore.opened_views.find((view) => view.id === viewId), +); const opened = computed({ get: () => currentView.value?.opened || [], - set: (val) => treeviewStore.setOpened(viewId, val), + set: (value) => treeviewStore.setOpened(viewId, value), }); const items = dataStore.refFormatedMeshComponents(viewId); + +const localItems = ref([]); + +function sortItems(itemList) { + const field = sortType.value === "name" ? "title" : "id"; + const options = { numeric: true, sensitivity: "base" }; + return itemList.toSorted((itemA, itemB) => { + const titleA = String(itemA[field] || itemA.id || ""); + const titleB = String(itemB[field] || itemB.id || ""); + return titleA.localeCompare(titleB, undefined, options); + }); +} + +watch( + items, + (newItems) => { + if (!newItems) { + localItems.value = []; + return; + } + localItems.value = newItems.map((newCategory) => { + const existing = localItems.value.find( + (category) => category.id === newCategory.id, + ); + if (existing) { + existing.title = newCategory.title || newCategory.id; + return existing; + } + return reactive({ + ...newCategory, + title: newCategory.title || newCategory.id, + children: [], + manualChildren: [], + }); + }); + }, + { immediate: true }, +); + +const displayItems = localItems; + const mesh_components_selection = dataStyleStore.visibleMeshComponents(viewId); const { @@ -34,7 +78,99 @@ const { availableFilterOptions, toggleSort, customFilter, -} = useTreeFilter(items); +} = useTreeFilter(displayItems); + +function restoreManualState() { + for (const category of localItems.value) { + category.children = category.manualChildren || []; + } +} + +function saveManualState() { + for (const category of localItems.value) { + category.manualChildren = [...category.children]; + } +} + +async function performGlobalSearch(query) { + const allComponents = await dataStore.getAllMeshComponents(viewId); + const searchMatch = query.toLowerCase(); + const matches = allComponents.filter( + (component) => + component.title.toLowerCase().includes(searchMatch) || + component.id.toLowerCase().includes(searchMatch), + ); + + const byType = {}; + for (const match of matches) { + if (!byType[match.category]) { + byType[match.category] = []; + } + byType[match.category].push(match); + } + + for (const type of Object.keys(byType)) { + byType[type] = sortItems(byType[type]); + } + + for (const category of localItems.value) { + category.children = byType[category.id] || []; + } + + const idsToOpen = Object.keys(byType).filter( + (type) => byType[type].length > 0, + ); + if (idsToOpen.length > 0) { + opened.value = [...new Set([...opened.value, ...idsToOpen])]; + } +} + +watch(search, async (newSearch, oldSearch) => { + if (!newSearch) { + restoreManualState(); + return; + } + + if (!oldSearch) { + saveManualState(); + } + + await performGlobalSearch(newSearch); +}); + +watch(opened, (newOpened, oldOpened) => { + if (search.value) { + return; + } + const closed = oldOpened.filter((itemId) => !newOpened.includes(itemId)); + for (const itemId of closed) { + const category = localItems.value.find( + (existingCategory) => existingCategory.id === itemId, + ); + if (category) { + category.children = []; + category.manualChildren = []; + } + } +}); + +async function loadChildren(item) { + if (search.value) { + return; + } + const target = localItems.value.find((category) => category.id === item.id); + if (!target) { + return; + } + + // oxlint-disable-next-line promise/avoid-new + await new Promise((resolve) => { + setTimeout(resolve, LOADER_DELAY_MS); + }); + const children = await dataStore.getMeshComponentsByType(viewId, target.id); + target.children = sortItems(children); + target.manualChildren = [...target.children]; +} async function onSelectionChange(current) { const previous = mesh_components_selection.value; @@ -58,7 +194,9 @@ function showContextMenu(event, item) { emit("show-menu", { event, itemId: actualItem.category ? actualItem.id : viewId, - context_type: actualItem.category ? "model_component" : "model_component_type", + context_type: actualItem.category + ? "model_component" + : "model_component_type", modelId: viewId, modelComponentType: actualItem.category ? undefined : actualItem.id, }); @@ -84,6 +222,7 @@ function showContextMenu(event, item) { :items="processedItems" :search="search" :custom-filter="customFilter" + :load-children="loadChildren" class="transparent-treeview" item-value="id" select-strategy="independent" diff --git a/app/composables/use_tree_filter.js b/app/composables/use_tree_filter.js index 527f386b..975ec3bf 100644 --- a/app/composables/use_tree_filter.js +++ b/app/composables/use_tree_filter.js @@ -2,30 +2,29 @@ function customFilter(value, searchQuery, item) { if (!searchQuery) { return true; } + if (!item || !item.raw) { + return false; + } const query = searchQuery.toLowerCase(); const title = (item.raw.title || "").toLowerCase(); const idValue = String(value || "").toLowerCase(); return title.includes(query) || idValue.includes(query); } -function sortAndFormatItems(items, sortType) { +function sortAndFormatItems(itemList, sortType) { + if (!itemList || !Array.isArray(itemList)) { + return []; + } const field = sortType === "name" ? "title" : "id"; - return items.map((category) => { - const children = (category.children || []).toSorted((itemA, itemB) => { - const valueA = itemA[field] || ""; - const valueB = itemB[field] || ""; - return valueA.localeCompare(valueB, undefined, { - numeric: true, - sensitivity: "base", - }); + const options = { numeric: true, sensitivity: "base" }; + + return itemList + .filter((item) => item !== null && item !== undefined) + .toSorted((itemA, itemB) => { + const fieldA = String(itemA[field] || itemA.id || ""); + const fieldB = String(itemB[field] || itemB.id || ""); + return fieldA.localeCompare(fieldB, undefined, options); }); - return { - ...category, - id: category.id, - title: category.title || category.id, - children, - }; - }); } function useTreeFilter(rawItems, options = {}) { diff --git a/app/stores/data.js b/app/stores/data.js index 8a8dd4f5..fc2a9911 100644 --- a/app/stores/data.js +++ b/app/stores/data.js @@ -59,15 +59,33 @@ export const useDataStore = defineStore("data", () => { .map((type) => ({ id: type, title: componentTitles[type], - children: componentsByType[type].map((meshComponent) => ({ - id: meshComponent.geode_id, - title: meshComponent.name, - category: meshComponent.type, - is_active: meshComponent.is_active, - })), + children: [], })); } + async function getMeshComponentsByType(modelId, type) { + const components = await database.model_components + .where("[id+type]") + .equals([modelId, type]) + .toArray(); + return components.map((meshComponent) => ({ + id: meshComponent.geode_id, + title: meshComponent.name, + category: meshComponent.type, + is_active: meshComponent.is_active, + })); + } + + async function getAllMeshComponents(modelId) { + const items = await database.model_components.where("id").equals(modelId).toArray(); + return items.map((meshComponent) => ({ + id: meshComponent.geode_id, + title: meshComponent.name, + category: meshComponent.type, + is_active: meshComponent.is_active, + })); + } + function refFormatedMeshComponents(modelId) { return useObservable( liveQuery(() => formatedMeshComponents(modelId)), @@ -234,6 +252,8 @@ export const useDataStore = defineStore("data", () => { meshComponentType, formatedMeshComponents, refFormatedMeshComponents, + getMeshComponentsByType, + getAllMeshComponents, registerObject, deregisterObject, addItem, From 4d21ae7f145d328c958ef3387873d55fed917b9a Mon Sep 17 00:00:00 2001 From: SpliiT <106495600+SpliiT@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:51:38 +0000 Subject: [PATCH 02/25] Apply prepare changes --- .../ObjectTree/Views/ModelComponents.vue | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue index 1c62d55b..057a270c 100644 --- a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue +++ b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue @@ -18,9 +18,7 @@ const dataStyleStore = useDataStyleStore(); const hybridViewerStore = useHybridViewerStore(); const treeviewStore = useTreeviewStore(); -const currentView = computed(() => - treeviewStore.opened_views.find((view) => view.id === viewId), -); +const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === viewId)); const opened = computed({ get: () => currentView.value?.opened || [], set: (value) => treeviewStore.setOpened(viewId, value), @@ -48,9 +46,7 @@ watch( return; } localItems.value = newItems.map((newCategory) => { - const existing = localItems.value.find( - (category) => category.id === newCategory.id, - ); + const existing = localItems.value.find((category) => category.id === newCategory.id); if (existing) { existing.title = newCategory.title || newCategory.id; return existing; @@ -117,9 +113,7 @@ async function performGlobalSearch(query) { category.children = byType[category.id] || []; } - const idsToOpen = Object.keys(byType).filter( - (type) => byType[type].length > 0, - ); + const idsToOpen = Object.keys(byType).filter((type) => byType[type].length > 0); if (idsToOpen.length > 0) { opened.value = [...new Set([...opened.value, ...idsToOpen])]; } @@ -144,9 +138,7 @@ watch(opened, (newOpened, oldOpened) => { } const closed = oldOpened.filter((itemId) => !newOpened.includes(itemId)); for (const itemId of closed) { - const category = localItems.value.find( - (existingCategory) => existingCategory.id === itemId, - ); + const category = localItems.value.find((existingCategory) => existingCategory.id === itemId); if (category) { category.children = []; category.manualChildren = []; @@ -194,9 +186,7 @@ function showContextMenu(event, item) { emit("show-menu", { event, itemId: actualItem.category ? actualItem.id : viewId, - context_type: actualItem.category - ? "model_component" - : "model_component_type", + context_type: actualItem.category ? "model_component" : "model_component_type", modelId: viewId, modelComponentType: actualItem.category ? undefined : actualItem.id, }); From 9c0ba900c7e9c98ec5f54329ebf725490f634a15 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Thu, 23 Apr 2026 14:20:44 +0200 Subject: [PATCH 03/25] big changes : new treeview system --- .../Viewer/ObjectTree/Base/CommonTreeView.vue | 149 ++++++++++ .../Viewer/ObjectTree/Base/ItemLabel.vue | 56 ++-- app/components/Viewer/ObjectTree/Box.vue | 17 +- app/components/Viewer/ObjectTree/Layout.vue | 2 +- .../Viewer/ObjectTree/Views/GlobalObjects.vue | 33 ++- .../ObjectTree/Views/ModelComponents.vue | 260 ++++++------------ app/composables/use_model_components.js | 70 +++++ app/composables/use_tree_filter.js | 2 +- app/composables/use_virtual_tree.js | 170 ++++++++++++ app/stores/data.js | 21 ++ 10 files changed, 558 insertions(+), 222 deletions(-) create mode 100644 app/components/Viewer/ObjectTree/Base/CommonTreeView.vue create mode 100644 app/composables/use_model_components.js create mode 100644 app/composables/use_virtual_tree.js diff --git a/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue b/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue new file mode 100644 index 00000000..53e709ab --- /dev/null +++ b/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/app/components/Viewer/ObjectTree/Base/ItemLabel.vue b/app/components/Viewer/ObjectTree/Base/ItemLabel.vue index acbbce2f..4e285f93 100644 --- a/app/components/Viewer/ObjectTree/Base/ItemLabel.vue +++ b/app/components/Viewer/ObjectTree/Base/ItemLabel.vue @@ -1,34 +1,48 @@ diff --git a/app/composables/use_model_components.js b/app/composables/use_model_components.js new file mode 100644 index 00000000..4ff54f8f --- /dev/null +++ b/app/composables/use_model_components.js @@ -0,0 +1,70 @@ +import { compareSelections } from "@ogw_front/utils/treeview"; +import { useDataStore } from "@ogw_front/stores/data"; +import { useDataStyleStore } from "@ogw_front/stores/data_style"; +import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer"; + +export function useModelComponents(viewId) { + const dataStore = useDataStore(); + const dataStyleStore = useDataStyleStore(); + const hybridViewerStore = useHybridViewerStore(); + + const items = dataStore.refFormatedMeshComponents(viewId); + const componentsCache = ref(undefined); + const localCategories = ref([]); + + onMounted(async () => { + const data = await dataStore.fetchAllMeshComponents(viewId); + componentsCache.value = markRaw(data); + }); + + watch( + items, + (newItems) => { + if (!newItems) { + localCategories.value = []; + return; + } + localCategories.value = newItems.map((newCategory) => { + const existing = localCategories.value.find( + (category) => category.id === newCategory.id, + ); + if (existing) { + existing.title = newCategory.title || newCategory.id; + return existing; + } + return reactive({ + ...newCategory, + title: newCategory.title || newCategory.id, + }); + }); + }, + { immediate: true }, + ); + + const selection = dataStyleStore.visibleMeshComponents(viewId); + + async function updateVisibility(current) { + const previous = selection.value; + const { added, removed } = compareSelections(current, previous); + + if (added.length === 0 && removed.length === 0) { + return; + } + + if (added.length > 0) { + await dataStyleStore.setModelComponentsVisibility(viewId, added, true); + } + if (removed.length > 0) { + await dataStyleStore.setModelComponentsVisibility(viewId, removed, false); + } + hybridViewerStore.remoteRender(); + } + + return { + items, + componentsCache, + localCategories, + selection, + updateVisibility, + }; +} diff --git a/app/composables/use_tree_filter.js b/app/composables/use_tree_filter.js index 975ec3bf..ec5cb1f3 100644 --- a/app/composables/use_tree_filter.js +++ b/app/composables/use_tree_filter.js @@ -79,4 +79,4 @@ function useTreeFilter(rawItems, options = {}) { }; } -export { customFilter, useTreeFilter }; +export { customFilter, useTreeFilter, sortAndFormatItems }; diff --git a/app/composables/use_virtual_tree.js b/app/composables/use_virtual_tree.js new file mode 100644 index 00000000..77674e02 --- /dev/null +++ b/app/composables/use_virtual_tree.js @@ -0,0 +1,170 @@ +export function useVirtualTree(props, emit) { + const actualItemProps = computed(() => ({ + value: "id", + title: "title", + children: "children", + height: 44, + ...toValue(props.itemProps), + })); + + const actualSelection = computed(() => ({ + selectable: false, + strategy: "classic", + ...toValue(props.selection), + })); + + const openedSet = computed(() => new Set(toValue(props.opened))); + const selectedSet = computed(() => new Set(toValue(props.selected))); + + function toggleOpen(item) { + const id = item[actualItemProps.value.value]; + const openedArray = toValue(props.opened) || []; + const newOpened = new Set(openedArray); + if (newOpened.has(id)) { + newOpened.delete(id); + } else { + newOpened.add(id); + } + emit("update:opened", [...newOpened]); + } + + function getAllChildrenIds(item, ids = []) { + const children = item[actualItemProps.value.children]; + if (children) { + for (const child of children) { + ids.push(child[actualItemProps.value.value]); + getAllChildrenIds(child, ids); + } + } + return ids; + } + + function isSelected(item) { + if (selectedSet.value.has(item[actualItemProps.value.value])) { + return true; + } + if (actualSelection.value.strategy === "classic") { + const childrenIds = getAllChildrenIds(item); + return ( + childrenIds.length > 0 && + childrenIds.every((id) => selectedSet.value.has(id)) + ); + } + return false; + } + + function getIndeterminate(item) { + if (actualSelection.value.strategy !== "classic") { + return false; + } + const childrenIds = getAllChildrenIds(item); + if (childrenIds.length === 0) { + return false; + } + + const selectedChildren = childrenIds.filter((id) => + selectedSet.value.has(id), + ); + return ( + selectedChildren.length > 0 && + selectedChildren.length < childrenIds.length + ); + } + + function toggleSelect(item) { + const id = item[actualItemProps.value.value]; + const selectedArray = toValue(props.selected) || []; + const newSelected = new Set(selectedArray); + const isCurrentlySelected = newSelected.has(id) || isSelected(item); + + if (actualSelection.value.strategy === "classic") { + const childrenIds = getAllChildrenIds(item); + if (isCurrentlySelected) { + newSelected.delete(id); + for (const childId of childrenIds) { + newSelected.delete(childId); + } + } else { + newSelected.add(id); + for (const childId of childrenIds) { + newSelected.add(childId); + } + } + } else if (isCurrentlySelected) { + newSelected.delete(id); + } else { + newSelected.add(id); + } + emit("update:selected", [...newSelected]); + } + + function flattenTree(itemsList, depth = 0, result = []) { + const search = toValue(props.search); + const lowerSearch = search ? search.toLowerCase() : ""; + const customFilter = toValue(props.customFilter); + + for (const item of itemsList) { + const id = item[actualItemProps.value.value]; + const children = item[actualItemProps.value.children]; + const hasChildren = children && children.length > 0; + + const isOpen = openedSet.value.has(id); + + if (lowerSearch) { + const matches = customFilter + ? customFilter(id, search, { raw: item }) + : (item[actualItemProps.value.title] || "") + .toLowerCase() + .includes(lowerSearch) || + String(id).toLowerCase().includes(lowerSearch); + + if (!hasChildren && !matches) { + continue; + } + + if (hasChildren) { + const subtree = []; + flattenTree(children, depth + 1, subtree); + if (subtree.length === 0 && !matches) { + continue; + } + + result.push({ + raw: item, + id, + depth, + isOpen: true, + isLeaf: false, + }); + result.push(...subtree); + continue; + } + } + + result.push({ + raw: item, + id, + depth, + isOpen, + isLeaf: !hasChildren, + }); + + if (isOpen && hasChildren) { + flattenTree(children, depth + 1, result); + } + } + return result; + } + + const displayItems = computed(() => flattenTree(toValue(props.items) || [])); + + return { + actualItemProps, + actualSelection, + displayItems, + toggleOpen, + toggleSelect, + isSelected, + getIndeterminate, + }; +} diff --git a/app/stores/data.js b/app/stores/data.js index fc2a9911..114bcc5f 100644 --- a/app/stores/data.js +++ b/app/stores/data.js @@ -86,6 +86,18 @@ export const useDataStore = defineStore("data", () => { })); } + async function fetchAllMeshComponents(modelId) { + const components = await getAllMeshComponents(modelId); + const byType = {}; + for (const component of components) { + if (!byType[component.category]) { + byType[component.category] = []; + } + byType[component.category].push(component); + } + return byType; + } + function refFormatedMeshComponents(modelId) { return useObservable( liveQuery(() => formatedMeshComponents(modelId)), @@ -224,6 +236,14 @@ export const useDataStore = defineStore("data", () => { return await getMeshComponentGeodeIds(modelId, "Block"); } + async function getMeshComponentGeodeIds(modelId, type) { + const components = await database.model_components + .where("[id+type]") + .equals([modelId, type]) + .toArray(); + return components.map((component) => component.geode_id); + } + async function getMeshComponentsViewerIds(modelId, meshComponentGeodeIds) { const components = await database.model_components .where("[id+geode_id]") @@ -272,5 +292,6 @@ export const useDataStore = defineStore("data", () => { exportStores, importStores, clear, + fetchAllMeshComponents, }; }); From 647292f4e76a9b31369fd306ad3c5d2ee54c21aa Mon Sep 17 00:00:00 2001 From: SpliiT <106495600+SpliiT@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:21:26 +0000 Subject: [PATCH 04/25] Apply prepare changes --- .../Viewer/ObjectTree/Base/CommonTreeView.vue | 14 +++---------- app/components/Viewer/ObjectTree/Box.vue | 4 +--- .../Viewer/ObjectTree/Views/GlobalObjects.vue | 13 +++--------- .../ObjectTree/Views/ModelComponents.vue | 18 ++++------------- app/composables/use_model_components.js | 4 +--- app/composables/use_virtual_tree.js | 20 +++++-------------- 6 files changed, 17 insertions(+), 56 deletions(-) diff --git a/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue b/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue index 53e709ab..0a086d15 100644 --- a/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue +++ b/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue @@ -53,16 +53,10 @@ const {
-
+
-
+
{{ item.raw[actualItemProps.title] || item.id }} diff --git a/app/components/Viewer/ObjectTree/Box.vue b/app/components/Viewer/ObjectTree/Box.vue index 15016d13..404a1984 100644 --- a/app/components/Viewer/ObjectTree/Box.vue +++ b/app/components/Viewer/ObjectTree/Box.vue @@ -85,9 +85,7 @@ watch( style="filter: brightness(0); display: flex; align-items: center" /> {{ mdiIcon }} - mdi-drag-variant + mdi-drag-variant @@ -107,11 +104,7 @@ function isModel(item) { variant="text" v-tooltip="'Model\'s mesh components'" @click.stop=" - treeviewStore.displayAdditionalTree( - item.id, - item.title, - item.geode_object_type, - ) + treeviewStore.displayAdditionalTree(item.id, item.title, item.geode_object_type) " /> diff --git a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue index 8ca50733..2e39fe7c 100644 --- a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue +++ b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue @@ -1,8 +1,5 @@ diff --git a/app/components/Viewer/ObjectTree/Base/Controls.vue b/app/components/Viewer/ObjectTree/Base/Controls.vue index 41163d67..9505229b 100644 --- a/app/components/Viewer/ObjectTree/Base/Controls.vue +++ b/app/components/Viewer/ObjectTree/Base/Controls.vue @@ -2,14 +2,16 @@ import ActionButton from "@ogw_front/components/ActionButton.vue"; import SearchBar from "@ogw_front/components/SearchBar.vue"; -const { search, sortType, filterOptions, availableFilterOptions } = defineProps({ - search: { type: String, required: true }, - sortType: { type: String, required: true }, - filterOptions: { type: Object, required: true }, - availableFilterOptions: { type: Array, required: true }, -}); +const { search, sortType, filterOptions, availableFilterOptions } = defineProps( + { + search: { type: String, required: true }, + sortType: { type: String, required: true }, + filterOptions: { type: Object, required: true }, + availableFilterOptions: { type: Array, required: true }, + }, +); -const emit = defineEmits(["update:search", "toggle-sort"]); +const emit = defineEmits(["update:search", "toggle-sort", "collapse-all"]); - + + diff --git a/app/components/Viewer/ObjectTree/Base/StickyHeader.vue b/app/components/Viewer/ObjectTree/Base/StickyHeader.vue new file mode 100644 index 00000000..0ba379d5 --- /dev/null +++ b/app/components/Viewer/ObjectTree/Base/StickyHeader.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/app/components/Viewer/ObjectTree/Base/TreeRow.vue b/app/components/Viewer/ObjectTree/Base/TreeRow.vue new file mode 100644 index 00000000..4731293b --- /dev/null +++ b/app/components/Viewer/ObjectTree/Base/TreeRow.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/app/components/Viewer/ObjectTree/Box.vue b/app/components/Viewer/ObjectTree/Box.vue index 15016d13..97963755 100644 --- a/app/components/Viewer/ObjectTree/Box.vue +++ b/app/components/Viewer/ObjectTree/Box.vue @@ -112,11 +112,14 @@ watch( /> - +
@@ -127,6 +130,7 @@ watch( diff --git a/app/components/Viewer/ObjectTree/Layout.vue b/app/components/Viewer/ObjectTree/Layout.vue index 6c3ba63f..3f8d4bbe 100644 --- a/app/components/Viewer/ObjectTree/Layout.vue +++ b/app/components/Viewer/ObjectTree/Layout.vue @@ -143,20 +143,20 @@ function onVerticalResizeStart(event, index) { const deltaPercent = (deltaY / containerHeight) * PERCENT_100; const minHeightPercent = (HEIGHT_MIN / containerHeight) * PERCENT_100; - let newH1 = startHeight1 + deltaPercent; - let newH2 = startHeight2 - deltaPercent; - - if (newH1 < minHeightPercent) { - newH1 = minHeightPercent; - newH2 = startHeight1 + startHeight2 - minHeightPercent; - } else if (newH2 < minHeightPercent) { - newH2 = minHeightPercent; - newH1 = startHeight1 + startHeight2 - minHeightPercent; + let newHeight1 = startHeight1 + deltaPercent; + let newHeight2 = startHeight2 - deltaPercent; + + if (newHeight1 < minHeightPercent) { + newHeight1 = minHeightPercent; + newHeight2 = startHeight1 + startHeight2 - minHeightPercent; + } else if (newHeight2 < minHeightPercent) { + newHeight2 = minHeightPercent; + newHeight1 = startHeight1 + startHeight2 - minHeightPercent; } const newHeights = [...rowHeights.value]; - newHeights[index] = newH1; - newHeights[index + 1] = newH2; + newHeights[index] = newHeight1; + newHeights[index + 1] = newHeight2; treeviewStore.setRowHeights(newHeights); document.body.style.userSelect = "none"; @@ -272,7 +272,7 @@ function onVerticalResizeStart(event, index) { display: flex; flex-direction: column; height: 100%; - overflow-y: auto; + overflow-y: hidden; overflow-x: hidden; flex-shrink: 0; } @@ -286,6 +286,8 @@ function onVerticalResizeStart(event, index) { } .view-wrapper { + display: flex; + flex-direction: column; overflow: hidden; padding: 2px; transition: transform 0.2s; diff --git a/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue b/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue index 882a7203..10edd56f 100644 --- a/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue +++ b/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue @@ -79,6 +79,7 @@ function isModel(item) { :filter-options="filterOptions" :available-filter-options="availableFilterOptions" @toggle-sort="toggleSort" + @collapse-all="opened = []" /> diff --git a/app/components/Viewer/ObjectTree/Box.vue b/app/components/Viewer/ObjectTree/Box.vue index 845e811e..90ac9557 100644 --- a/app/components/Viewer/ObjectTree/Box.vue +++ b/app/components/Viewer/ObjectTree/Box.vue @@ -1,4 +1,6 @@ - + - + {{ item.raw[itemProps.title] || item.id }} diff --git a/app/components/Viewer/ObjectTree/Box.vue b/app/components/Viewer/ObjectTree/Box.vue index aa7507cd..20698c2c 100644 --- a/app/components/Viewer/ObjectTree/Box.vue +++ b/app/components/Viewer/ObjectTree/Box.vue @@ -132,9 +132,7 @@ watch( style="filter: brightness(0); display: flex; align-items: center" /> {{ mdiIcon }} - mdi-drag-variant + mdi-drag-variant +
{ @@ -89,7 +89,7 @@ function handleHoverEnter(item) { } return []; }, - is_model ? "model" : "mesh" + is_model ? "model" : "mesh", ); } diff --git a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue index 0a1dcfc5..9a07a097 100644 --- a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue +++ b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue @@ -84,9 +84,9 @@ function showContextMenu(event, item) { function handleHoverEnter(item) { const actualItem = item.raw || item; - + // Sécurité : on ne highlight que si c'est un composant (qui a une category) ou si on veut highlight tout le type - // Mais ici, pour éviter les exceptions, on ne highlight que les composants individuels + // Mais ici, pour éviter les exceptions, on ne highlight que les composants individuels // ou on s'assure que viewer_id existe. if (!actualItem.category && (!actualItem.children || actualItem.children.length === 0)) { return; diff --git a/app/composables/use_hover_highlight.js b/app/composables/use_hover_highlight.js index 74a32b5d..7f2e056b 100644 --- a/app/composables/use_hover_highlight.js +++ b/app/composables/use_hover_highlight.js @@ -17,7 +17,7 @@ export function useHoverhighlight() { timer = setTimeout(async () => { currentId = id; currentType = type; - + let block_ids = []; if (typeof block_ids_provider === "function") { block_ids = await block_ids_provider(); diff --git a/app/composables/use_tree_scroll.js b/app/composables/use_tree_scroll.js index 95c03547..eb1f1640 100644 --- a/app/composables/use_tree_scroll.js +++ b/app/composables/use_tree_scroll.js @@ -30,10 +30,7 @@ export function useTreeScroll(scrollTopGetter, emit, displayItems, actualItemPro const itemHeight = actualItemProps.value.height || DEFAULT_ITEM_HEIGHT; const firstVisibleIndex = Math.floor(internalScrollTop.value / itemHeight); - if ( - firstVisibleIndex < 0 || - firstVisibleIndex >= displayItems.value.length - ) { + if (firstVisibleIndex < 0 || firstVisibleIndex >= displayItems.value.length) { return undefined; } @@ -50,12 +47,7 @@ export function useTreeScroll(scrollTopGetter, emit, displayItems, actualItemPro if (item && !item.isLeaf && item.depth < firstVisibleDepth) { return item; } - if ( - item && - item.depth === 0 && - !item.isLeaf && - current < firstVisibleIndex - ) { + if (item && item.depth === 0 && !item.isLeaf && current < firstVisibleIndex) { return item; } current -= 1; diff --git a/app/composables/use_virtual_tree.js b/app/composables/use_virtual_tree.js index 38628345..86092c31 100644 --- a/app/composables/use_virtual_tree.js +++ b/app/composables/use_virtual_tree.js @@ -47,8 +47,7 @@ export function useVirtualTree(props, emit) { if (actualSelection.value.strategy === "classic") { const childrenIds = getAllChildrenIds(item); return ( - childrenIds.length > 0 && - childrenIds.every((childId) => selectedSet.value.has(childId)) + childrenIds.length > 0 && childrenIds.every((childId) => selectedSet.value.has(childId)) ); } return false; @@ -63,13 +62,8 @@ export function useVirtualTree(props, emit) { return false; } - const selectedChildren = childrenIds.filter((childId) => - selectedSet.value.has(childId), - ); - return ( - selectedChildren.length > 0 && - selectedChildren.length < childrenIds.length - ); + const selectedChildren = childrenIds.filter((childId) => selectedSet.value.has(childId)); + return selectedChildren.length > 0 && selectedChildren.length < childrenIds.length; } function toggleSelect(item) { From 97b623e40c1b9bc27231829e5ef39b0b2e6d4a3d Mon Sep 17 00:00:00 2001 From: SpliiT Date: Thu, 23 Apr 2026 17:34:00 +0200 Subject: [PATCH 10/25] background colo adaptative --- app/components/Viewer/ObjectTree/Box.vue | 32 ++++++++------ .../ObjectTree/Views/ModelComponents.vue | 43 ++++++++++++------- app/stores/hybrid_viewer.js | 22 +++++++--- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/app/components/Viewer/ObjectTree/Box.vue b/app/components/Viewer/ObjectTree/Box.vue index aa7507cd..d617ce4b 100644 --- a/app/components/Viewer/ObjectTree/Box.vue +++ b/app/components/Viewer/ObjectTree/Box.vue @@ -16,13 +16,24 @@ const scrollContainer = ref(undefined); const treeviewBox = useTemplateRef("treeview-box"); const hybridViewerStore = useHybridViewerStore(); +const LUMINANCE_THRESHOLD = 0.7; +const ADAPTIVE_EXPONENT = 1.4; + +const MIN_BLUR = 1; +const MAX_BLUR = 20; + +const MIN_OPACITY = 0.05; +const MAX_OPACITY = 0.5; + +const MIN_BOOST = 1; +const MAX_BOOST = 1.7; + const { x, y, width, height } = useElementBounding(treeviewBox); -const brightness = ref(0.7); +const brightness = ref(LUMINANCE_THRESHOLD); let isApplyingScroll = false; let resizeObserver = undefined; -// Capteur de luminosité "Smart" watch( [x, y, width, height, () => hybridViewerStore.latestImage], () => { @@ -36,18 +47,13 @@ watch( { immediate: true }, ); -// Calcul des paramètres visuels adaptatifs const adaptiveStyles = computed(() => { - // Mapping : si le fond est le gris clair par défaut (0.7), darkFactor = 0. - // si le fond est noir (0.0), darkFactor = 1. - const normalized = Math.min(1, brightness.value / 0.7); - const darkFactor = Math.pow(1 - normalized, 2); + const normalized = Math.min(1, brightness.value / LUMINANCE_THRESHOLD); + const darkFactor = (1 - normalized) ** ADAPTIVE_EXPONENT; - // Focus Crystal : flou et opacité quasi-nuls sur fond clair (transparent), - // mais remontent fort sur le noir pour garantir la lisibilité du texte. - const blur = 1 + darkFactor * 19; // de 1px à 20px - const opacity = 0.02 + darkFactor * 0.6; // de 2% à 62% - const brightnessBoost = 1 + darkFactor * 0.5; // boost la lumière derrière de 1.0 à 1.5 + const blur = MIN_BLUR + darkFactor * (MAX_BLUR - MIN_BLUR); + const opacity = MIN_OPACITY + darkFactor * (MAX_OPACITY - MIN_OPACITY); + const brightnessBoost = MIN_BOOST + darkFactor * (MAX_BOOST - MIN_BOOST); return { "--adaptive-blur": `${blur}px`, @@ -194,7 +200,6 @@ watch( content: ""; position: absolute; inset: 0; - /* L'opacité et le flou sont pilotés dynamiquement par le JS */ background: rgba(255, 255, 255, var(--adaptive-opacity)); backdrop-filter: blur(var(--adaptive-blur)) brightness(var(--adaptive-brightness)); @@ -202,6 +207,7 @@ watch( brightness(var(--adaptive-brightness)); z-index: 0; pointer-events: none; + border-radius: inherit; transition: background-color 0.3s ease, backdrop-filter 0.3s ease; diff --git a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue index 0a1dcfc5..0933c2ee 100644 --- a/app/components/Viewer/ObjectTree/Views/ModelComponents.vue +++ b/app/components/Viewer/ObjectTree/Views/ModelComponents.vue @@ -1,11 +1,14 @@