@geode/opengeodeweb-front 10.20.1 → 10.21.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import GlobalObjects from "@ogw_front/components/Viewer/ObjectTree/Views/GlobalObjects.vue";
3
+ import ModelCollections from "@ogw_front/components/Viewer/ObjectTree/Views/ModelCollections.vue";
3
4
  import ModelComponents from "@ogw_front/components/Viewer/ObjectTree/Views/ModelComponents.vue";
4
5
  import ViewerObjectTreeBox from "@ogw_front/components/Viewer/ObjectTree/Box.vue";
5
6
  import { geode_objects } from "@ogw_front/assets/geode_objects";
@@ -263,15 +264,17 @@ function onVerticalResizeStart(event, index) {
263
264
  :icon="geode_objects[view.geode_object_type]?.image"
264
265
  :scroll-top="view.scrollTop"
265
266
  closable
266
- :border-radius="index === additionalViews.length - 1 ? '0 16px 16px 0' : '0'"
267
+ :border-radius="`0 ${index === 0 ? '16px' : '0'} ${index === additionalViews.length - 1 ? '16px' : '0'} 0`"
267
268
  :border-left="false"
268
269
  @close="treeviewStore.closeView(view.id)"
269
270
  @dragstart="onDragStart(index + 1)"
270
271
  @update:scroll-top="treeviewStore.setScrollTop(view.id, $event)"
271
272
  >
272
- <ModelComponents
273
+ <component
274
+ :is="view.viewType === 'model_collections' ? ModelCollections : ModelComponents"
273
275
  data-testid="modelComponentsObjectTree"
274
- :id="view.id"
276
+ :id="view.modelId || view.id"
277
+ :view-id="view.id"
275
278
  @show-menu="emit('show-menu', $event)"
276
279
  />
277
280
  </ViewerObjectTreeBox>
@@ -75,6 +75,31 @@ function isModel(item) {
75
75
  );
76
76
  }
77
77
 
78
+ const hasCollectionsMap = reactive({});
79
+
80
+ watch(
81
+ () => treeviewStore.items,
82
+ async (newItems) => {
83
+ const models = newItems
84
+ .flatMap((group) => group.children || [])
85
+ .filter((item) => isModel(item));
86
+ const fetchPromises = models.map(async (model) => {
87
+ if (hasCollectionsMap[model.id] === undefined) {
88
+ hasCollectionsMap[model.id] = false;
89
+ try {
90
+ const hasCollections = await dataStore.hasCollectionComponents(model.id);
91
+ hasCollectionsMap[model.id] = hasCollections;
92
+ } catch (error) {
93
+ console.error("Failed to check collections", error);
94
+ }
95
+ }
96
+ });
97
+
98
+ await Promise.all(fetchPromises);
99
+ },
100
+ { immediate: true, deep: true },
101
+ );
102
+
78
103
  function handleHoverEnter({ item, immediate = false }) {
79
104
  const actualItem = item.raw || item;
80
105
 
@@ -169,7 +194,28 @@ function expandAll() {
169
194
  variant="text"
170
195
  v-tooltip="'Model\'s mesh components'"
171
196
  @click.stop="
172
- treeviewStore.displayAdditionalTree(item.id, item.title, item.geode_object_type)
197
+ treeviewStore.displayAdditionalTree(
198
+ item.id,
199
+ item.title,
200
+ item.geode_object_type,
201
+ 'model_components',
202
+ )
203
+ "
204
+ />
205
+ <v-btn
206
+ v-if="isModel(item) && hasCollectionsMap[item.id]"
207
+ icon="mdi-format-list-group"
208
+ size="medium"
209
+ class="ml-2"
210
+ variant="text"
211
+ v-tooltip="'Model\'s collections'"
212
+ @click.stop="
213
+ treeviewStore.displayAdditionalTree(
214
+ item.id,
215
+ item.title,
216
+ item.geode_object_type,
217
+ 'model_collections',
218
+ )
173
219
  "
174
220
  />
175
221
  </template>
@@ -0,0 +1,222 @@
1
+ <script setup>
2
+ import { sortAndFormatItems, useTreeFilter } from "@ogw_front/composables/tree_filter";
3
+ import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonTreeView.vue";
4
+ import FetchingData from "@ogw_front/components/FetchingData.vue";
5
+ import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
6
+ import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
7
+ import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
8
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
+ import { useModelCollections } from "@ogw_front/composables/model_collections";
10
+ import { useTreeviewStore } from "@ogw_front/stores/treeview";
11
+
12
+ const { id, viewId } = defineProps({
13
+ id: { type: String, required: true },
14
+ viewId: { type: String, required: false },
15
+ });
16
+ const actualViewId = viewId || id;
17
+ const { onHoverEnter, onHoverLeave } = useHoverhighlight();
18
+ const hybridViewerStore = useHybridViewerStore();
19
+ const emit = defineEmits(["show-menu"]);
20
+
21
+ const treeviewStore = useTreeviewStore();
22
+ const {
23
+ items: rawItems,
24
+ collectionsCache: componentsCache,
25
+ localCategories,
26
+ selection: visibleComponents,
27
+ updateVisibility,
28
+ } = useModelCollections(id);
29
+
30
+ const currentView = computed(() =>
31
+ treeviewStore.opened_views.find((view) => view.id === actualViewId),
32
+ );
33
+
34
+ const opened = computed({
35
+ get: () => currentView.value?.opened || [],
36
+ set: (val) => treeviewStore.setOpened(actualViewId, val),
37
+ });
38
+
39
+ const {
40
+ search,
41
+ sortType,
42
+ filterOptions,
43
+ processedItems: filteredCategories,
44
+ availableFilterOptions,
45
+ toggleSort,
46
+ customFilter,
47
+ applySearchFilter,
48
+ } = useTreeFilter(localCategories);
49
+
50
+ function onUpdateSelection(newSelection) {
51
+ const finalSelection = applySearchFilter(newSelection, visibleComponents.value);
52
+ updateVisibility(finalSelection);
53
+ }
54
+
55
+ const visibleSelection = computed(() => applySearchFilter(visibleComponents.value, []));
56
+
57
+ const itemsForTreeView = computed(() => {
58
+ if (search.value && componentsCache.value) {
59
+ const query = search.value.toLowerCase();
60
+ const result = [];
61
+ for (const type of Object.keys(componentsCache.value)) {
62
+ const matches = componentsCache.value[type].filter(
63
+ (component) =>
64
+ component.title.toLowerCase().includes(query) ||
65
+ component.id.toLowerCase().includes(query),
66
+ );
67
+ if (matches.length > 0) {
68
+ result.push({
69
+ id: type,
70
+ title: `${type}s (${matches.length})`,
71
+ children: sortAndFormatItems(matches, sortType.value),
72
+ });
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ const result = [];
79
+ for (const category of filteredCategories.value) {
80
+ result.push({
81
+ ...category,
82
+ children: sortAndFormatItems(componentsCache.value?.[category.id], sortType.value),
83
+ });
84
+ }
85
+ return result;
86
+ });
87
+
88
+ function showContextMenu(event, item) {
89
+ const actualItem = item.raw || item;
90
+ emit("show-menu", {
91
+ event,
92
+ itemId: actualItem.category ? actualItem.id : id,
93
+ context_type: actualItem.category ? "model_component" : "model_component_type",
94
+ modelId: id,
95
+ modelComponentType: actualItem.category ? undefined : actualItem.id,
96
+ });
97
+ }
98
+
99
+ function handleHoverEnter({ item, immediate = false }) {
100
+ const actualItem = item.raw || item;
101
+
102
+ if (!actualItem.category && (!actualItem.children || actualItem.children.length === 0)) {
103
+ return;
104
+ }
105
+
106
+ const viewerIdsToHover = extractIds(actualItem);
107
+
108
+ onHoverEnter(id, () => viewerIdsToHover, "model", immediate);
109
+ }
110
+
111
+ function handleHoverLeave() {
112
+ onHoverLeave(id);
113
+ }
114
+
115
+ function expandAll() {
116
+ const allIds = [];
117
+ function traverse(itemsList) {
118
+ for (const item of itemsList) {
119
+ if (item.children && item.children.length > 0) {
120
+ allIds.push(item.id);
121
+ traverse(item.children);
122
+ }
123
+ }
124
+ }
125
+ traverse(itemsForTreeView.value);
126
+ opened.value = allIds;
127
+ }
128
+
129
+ function extractIds(node) {
130
+ if (node.children && node.children.length > 0) {
131
+ return node.children.flatMap((child) => extractIds(child));
132
+ }
133
+ if (node.viewer_id !== undefined && node.viewer_id !== null) {
134
+ return [node.viewer_id];
135
+ }
136
+ return [];
137
+ }
138
+
139
+ function getLeafViewerIds(item) {
140
+ const actualItem = item.raw || item;
141
+ return extractIds(actualItem);
142
+ }
143
+ </script>
144
+
145
+ <template>
146
+ <div class="tree-view-container">
147
+ <ObjectTreeControls
148
+ v-model:search="search"
149
+ :sort-type="sortType"
150
+ :filter-options="filterOptions"
151
+ :available-filter-options="availableFilterOptions"
152
+ :is-collapsed="opened.length === 0"
153
+ @toggle-sort="toggleSort"
154
+ @collapse-all="opened = []"
155
+ @expand-all="expandAll"
156
+ />
157
+
158
+ <FetchingData v-if="rawItems === undefined" :size="48" :width="4" text="" />
159
+
160
+ <CommonTreeView
161
+ :selected="visibleSelection"
162
+ v-model:opened="opened"
163
+ :items="itemsForTreeView"
164
+ :options="{
165
+ selection: { selectable: true, strategy: 'classic' },
166
+ search,
167
+ customFilter,
168
+ }"
169
+ :scroll-top="currentView?.scrollTop || 0"
170
+ class="transparent-treeview virtual-tree-height"
171
+ @update:selected="onUpdateSelection"
172
+ @click:item="onUpdateSelection([$event.id, ...visibleComponents])"
173
+ @update:scroll-top="treeviewStore.setScrollTop(actualViewId, $event)"
174
+ @hover:enter="handleHoverEnter"
175
+ @hover:leave="handleHoverLeave"
176
+ >
177
+ <template #title="{ item, isLeaf }">
178
+ <ObjectTreeItemLabel
179
+ :item="item"
180
+ :is-leaf="isLeaf"
181
+ show-tooltip
182
+ class="text-body-1"
183
+ @contextmenu.prevent.stop="showContextMenu($event, item)"
184
+ />
185
+ </template>
186
+
187
+ <template #append="{ item }">
188
+ <v-btn
189
+ v-if="item.category || (item.children && item.children.length > 0)"
190
+ icon="mdi-target"
191
+ size="medium"
192
+ variant="text"
193
+ v-tooltip="'Focus camera on object'"
194
+ @click.stop="hybridViewerStore.focusCameraOnObject(id, getLeafViewerIds(item))"
195
+ />
196
+ </template>
197
+ </CommonTreeView>
198
+ </div>
199
+ </template>
200
+
201
+ <style scoped>
202
+ .tree-view-container {
203
+ height: 100%;
204
+ display: flex;
205
+ flex-direction: column;
206
+ overflow: hidden;
207
+ min-height: 0;
208
+ }
209
+
210
+ .virtual-tree-height {
211
+ flex-grow: 1;
212
+ min-height: 0;
213
+ }
214
+
215
+ .transparent-treeview {
216
+ background-color: transparent;
217
+ }
218
+
219
+ :deep(.v-list-item__overlay) {
220
+ display: none !important;
221
+ }
222
+ </style>
@@ -9,7 +9,11 @@ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
9
  import { useModelComponents } from "@ogw_front/composables/model_components";
10
10
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
11
11
 
12
- const { id } = defineProps({ id: { type: String, required: true } });
12
+ const { id, viewId } = defineProps({
13
+ id: { type: String, required: true },
14
+ viewId: { type: String, required: false },
15
+ });
16
+ const actualViewId = viewId || id;
13
17
  const { onHoverEnter, onHoverLeave } = useHoverhighlight();
14
18
  const hybridViewerStore = useHybridViewerStore();
15
19
  const emit = defineEmits(["show-menu"]);
@@ -23,11 +27,13 @@ const {
23
27
  updateVisibility,
24
28
  } = useModelComponents(id);
25
29
 
26
- const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === id));
30
+ const currentView = computed(() =>
31
+ treeviewStore.opened_views.find((view) => view.id === actualViewId),
32
+ );
27
33
 
28
34
  const opened = computed({
29
35
  get: () => currentView.value?.opened || [],
30
- set: (val) => treeviewStore.setOpened(id, val),
36
+ set: (val) => treeviewStore.setOpened(actualViewId, val),
31
37
  });
32
38
 
33
39
  const {
@@ -155,7 +161,7 @@ function expandAll() {
155
161
  class="transparent-treeview virtual-tree-height"
156
162
  @update:selected="onUpdateSelection"
157
163
  @click:item="onUpdateSelection([$event.id, ...visibleComponents])"
158
- @update:scroll-top="treeviewStore.setScrollTop(id, $event)"
164
+ @update:scroll-top="treeviewStore.setScrollTop(actualViewId, $event)"
159
165
  @hover:enter="handleHoverEnter"
160
166
  @hover:leave="handleHoverLeave"
161
167
  >
@@ -0,0 +1,72 @@
1
+ import { compareSelections } from "@ogw_front/utils/treeview";
2
+ import { useDataStore } from "@ogw_front/stores/data";
3
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
4
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
5
+
6
+ export function useModelCollections(viewId) {
7
+ const dataStore = useDataStore();
8
+ const dataStyleStore = useDataStyleStore();
9
+ const hybridViewerStore = useHybridViewerStore();
10
+
11
+ const items = dataStore.refFormatedCollectionComponents(viewId);
12
+ const collectionsCache = ref(undefined);
13
+ const localCategories = ref([]);
14
+
15
+ onMounted(async () => {
16
+ const data = await dataStore.fetchAllCollectionComponents(viewId);
17
+ collectionsCache.value = markRaw(data);
18
+ });
19
+
20
+ watch(
21
+ items,
22
+ async (newItems) => {
23
+ if (!newItems) {
24
+ localCategories.value = [];
25
+ return;
26
+ }
27
+
28
+ const data = await dataStore.fetchAllCollectionComponents(viewId);
29
+ collectionsCache.value = markRaw(data);
30
+
31
+ localCategories.value = newItems.map((newCategory) => {
32
+ const existing = localCategories.value.find((category) => category.id === newCategory.id);
33
+ if (existing) {
34
+ existing.title = newCategory.title || newCategory.id;
35
+ return existing;
36
+ }
37
+ return reactive({
38
+ ...newCategory,
39
+ title: newCategory.title || newCategory.id,
40
+ });
41
+ });
42
+ },
43
+ { immediate: true },
44
+ );
45
+
46
+ const selection = dataStyleStore.visibleMeshComponents(viewId);
47
+
48
+ async function updateVisibility(current) {
49
+ const previous = selection.value;
50
+ const { added, removed } = compareSelections(current, previous);
51
+
52
+ if (added.length === 0 && removed.length === 0) {
53
+ return;
54
+ }
55
+
56
+ if (added.length > 0) {
57
+ await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
58
+ }
59
+ if (removed.length > 0) {
60
+ await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
61
+ }
62
+ hybridViewerStore.remoteRender();
63
+ }
64
+
65
+ return {
66
+ items,
67
+ collectionsCache,
68
+ localCategories,
69
+ selection,
70
+ updateVisibility,
71
+ };
72
+ }
@@ -19,11 +19,15 @@ export function useModelComponents(viewId) {
19
19
 
20
20
  watch(
21
21
  items,
22
- (newItems) => {
22
+ async (newItems) => {
23
23
  if (!newItems) {
24
24
  localCategories.value = [];
25
25
  return;
26
26
  }
27
+
28
+ const data = await dataStore.fetchAllMeshComponents(viewId);
29
+ componentsCache.value = markRaw(data);
30
+
27
31
  localCategories.value = newItems.map((newCategory) => {
28
32
  const existing = localCategories.value.find((category) => category.id === newCategory.id);
29
33
  if (existing) {
@@ -30,13 +30,14 @@ export function useVirtualTree(propsIn, emit) {
30
30
  emit("update:opened", [...newOpened]);
31
31
  }
32
32
 
33
- function getAllChildrenIds(item, ids = []) {
33
+ function getLeafChildrenIds(item, ids = []) {
34
34
  const children = item[actualItemProps.value.children];
35
- if (children) {
35
+ if (children && children.length > 0) {
36
36
  for (const child of children) {
37
- ids.push(child[actualItemProps.value.value]);
38
- getAllChildrenIds(child, ids);
37
+ getLeafChildrenIds(child, ids);
39
38
  }
39
+ } else {
40
+ ids.push(item[actualItemProps.value.value]);
40
41
  }
41
42
  return ids;
42
43
  }
@@ -47,7 +48,7 @@ export function useVirtualTree(propsIn, emit) {
47
48
  return true;
48
49
  }
49
50
  if (actualSelection.value.strategy === "classic") {
50
- const childrenIds = getAllChildrenIds(item);
51
+ const childrenIds = getLeafChildrenIds(item);
51
52
  return (
52
53
  childrenIds.length > 0 && childrenIds.every((childId) => selectedSet.value.has(childId))
53
54
  );
@@ -59,7 +60,7 @@ export function useVirtualTree(propsIn, emit) {
59
60
  if (actualSelection.value.strategy !== "classic") {
60
61
  return false;
61
62
  }
62
- const childrenIds = getAllChildrenIds(item);
63
+ const childrenIds = getLeafChildrenIds(item);
63
64
  if (childrenIds.length === 0) {
64
65
  return false;
65
66
  }
@@ -75,7 +76,7 @@ export function useVirtualTree(propsIn, emit) {
75
76
  const isCurrentlySelected = newSelected.has(id) || isSelected(item);
76
77
 
77
78
  if (actualSelection.value.strategy === "classic") {
78
- const childrenIds = getAllChildrenIds(item);
79
+ const childrenIds = getLeafChildrenIds(item);
79
80
  if (isCurrentlySelected) {
80
81
  newSelected.delete(id);
81
82
  for (const childId of childrenIds) {
@@ -5,6 +5,8 @@ import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schem
5
5
 
6
6
  // Local imports
7
7
  import { database } from "@ogw_internal/database/database.js";
8
+ import { useDataCollections } from "./data_helpers/collections.js";
9
+ import { useDataMesh } from "./data_helpers/mesh.js";
8
10
  import { useViewerStore } from "@ogw_front/stores/viewer";
9
11
 
10
12
  const viewer_generic_schemas = viewer_schemas.opengeodeweb_viewer.generic;
@@ -16,6 +18,27 @@ export const useDataStore = defineStore("data", () => {
16
18
  const model_components_db = database.model_components;
17
19
  const model_components_relation_db = database.model_components_relation;
18
20
 
21
+ const {
22
+ formatedMeshComponents,
23
+ refFormatedMeshComponents,
24
+ getMeshComponentsByType,
25
+ getAllMeshComponents,
26
+ fetchAllMeshComponents,
27
+ getMeshComponentGeodeIds,
28
+ getCornersGeodeIds,
29
+ getLinesGeodeIds,
30
+ getSurfacesGeodeIds,
31
+ getBlocksGeodeIds,
32
+ } = useDataMesh();
33
+
34
+ const {
35
+ hasCollectionComponents,
36
+ getAllCollectionComponents,
37
+ fetchAllCollectionComponents,
38
+ formatedCollectionComponents,
39
+ refFormatedCollectionComponents,
40
+ } = useDataCollections();
41
+
19
42
  async function item(id) {
20
43
  const data_item = await data_db.get(id);
21
44
  if (!data_item) {
@@ -42,84 +65,6 @@ export const useDataStore = defineStore("data", () => {
42
65
  );
43
66
  }
44
67
 
45
- async function formatedMeshComponents(modelId) {
46
- const items = await model_components_db.where("id").equals(modelId).toArray();
47
- const componentTitles = {
48
- Corner: "Corners",
49
- Line: "Lines",
50
- Surface: "Surfaces",
51
- Block: "Blocks",
52
- };
53
-
54
- const componentsByType = {};
55
- for (const component_item of items) {
56
- if (componentTitles[component_item.type]) {
57
- if (!componentsByType[component_item.type]) {
58
- componentsByType[component_item.type] = [];
59
- }
60
- componentsByType[component_item.type].push(component_item);
61
- }
62
- }
63
-
64
- return Object.keys(componentTitles)
65
- .filter((type) => componentsByType[type])
66
- .map((type) => ({
67
- id: type,
68
- title: componentTitles[type],
69
- children: componentsByType[type].map((meshComponent) => ({
70
- id: meshComponent.geode_id,
71
- title: meshComponent.name,
72
- category: meshComponent.type,
73
- viewer_id: Number(meshComponent.viewer_id),
74
- is_active: meshComponent.is_active,
75
- })),
76
- }));
77
- }
78
-
79
- async function getMeshComponentsByType(modelId, type) {
80
- const components = await model_components_db
81
- .where("[id+type]")
82
- .equals([modelId, type])
83
- .toArray();
84
- return components.map((meshComponent) => ({
85
- id: meshComponent.geode_id,
86
- title: meshComponent.name,
87
- category: meshComponent.type,
88
- viewer_id: Number(meshComponent.viewer_id),
89
- is_active: meshComponent.is_active,
90
- }));
91
- }
92
-
93
- async function getAllMeshComponents(modelId) {
94
- const items = await model_components_db.where("id").equals(modelId).toArray();
95
- return items.map((meshComponent) => ({
96
- id: meshComponent.geode_id,
97
- title: meshComponent.name,
98
- category: meshComponent.type,
99
- viewer_id: Number(meshComponent.viewer_id),
100
- is_active: meshComponent.is_active,
101
- }));
102
- }
103
-
104
- async function fetchAllMeshComponents(modelId) {
105
- const components = await getAllMeshComponents(modelId);
106
- const byType = {};
107
- for (const component of components) {
108
- if (!byType[component.category]) {
109
- byType[component.category] = [];
110
- }
111
- byType[component.category].push(component);
112
- }
113
- return byType;
114
- }
115
-
116
- function refFormatedMeshComponents(modelId) {
117
- return useObservable(
118
- liveQuery(() => formatedMeshComponents(modelId)),
119
- { initialValue: undefined },
120
- );
121
- }
122
-
123
68
  async function meshComponentType(modelId, geode_id) {
124
69
  const component = await model_components_db
125
70
  .where("[id+geode_id]")
@@ -227,30 +172,6 @@ export const useDataStore = defineStore("data", () => {
227
172
  await database.model_components_relation.where("id").equals(modelId).delete();
228
173
  }
229
174
 
230
- async function getMeshComponentGeodeIds(modelId, type) {
231
- const components = await model_components_db
232
- .where("[id+type]")
233
- .equals([modelId, type])
234
- .toArray();
235
- return components.map((component) => component.geode_id);
236
- }
237
-
238
- async function getCornersGeodeIds(modelId) {
239
- return await getMeshComponentGeodeIds(modelId, "Corner");
240
- }
241
-
242
- async function getLinesGeodeIds(modelId) {
243
- return await getMeshComponentGeodeIds(modelId, "Line");
244
- }
245
-
246
- async function getSurfacesGeodeIds(modelId) {
247
- return await getMeshComponentGeodeIds(modelId, "Surface");
248
- }
249
-
250
- async function getBlocksGeodeIds(modelId) {
251
- return await getMeshComponentGeodeIds(modelId, "Block");
252
- }
253
-
254
175
  async function getAllModelComponentsViewerIds(modelId) {
255
176
  const components = await model_components_db.where("id").equals(modelId).toArray();
256
177
  return components.map((component) => Number.parseInt(component.viewer_id, 10));
@@ -283,10 +204,6 @@ export const useDataStore = defineStore("data", () => {
283
204
  allItems,
284
205
  refItem,
285
206
  meshComponentType,
286
- formatedMeshComponents,
287
- refFormatedMeshComponents,
288
- getMeshComponentsByType,
289
- getAllMeshComponents,
290
207
  registerObject,
291
208
  deregisterObject,
292
209
  addItem,
@@ -294,18 +211,29 @@ export const useDataStore = defineStore("data", () => {
294
211
  addComponentRelations,
295
212
  deleteItem,
296
213
  updateItem,
297
- getCornersGeodeIds,
298
- getLinesGeodeIds,
299
- getSurfacesGeodeIds,
300
- getBlocksGeodeIds,
301
214
  getAllModelComponentsViewerIds,
302
- getMeshComponentGeodeIds,
303
215
  getMeshComponentsViewerIds,
304
216
  getComponentByViewerId,
305
217
 
306
218
  exportStores,
307
219
  importStores,
308
220
  clear,
221
+
222
+ formatedMeshComponents,
223
+ refFormatedMeshComponents,
224
+ getMeshComponentsByType,
225
+ getAllMeshComponents,
309
226
  fetchAllMeshComponents,
227
+ getMeshComponentGeodeIds,
228
+ getCornersGeodeIds,
229
+ getLinesGeodeIds,
230
+ getSurfacesGeodeIds,
231
+ getBlocksGeodeIds,
232
+
233
+ hasCollectionComponents,
234
+ getAllCollectionComponents,
235
+ fetchAllCollectionComponents,
236
+ formatedCollectionComponents,
237
+ refFormatedCollectionComponents,
310
238
  };
311
239
  });
@@ -0,0 +1,102 @@
1
+ import { database } from "@ogw_internal/database/database.js";
2
+ import { liveQuery } from "dexie";
3
+ import { useDataMesh } from "./mesh.js";
4
+ import { useObservable } from "@vueuse/rxjs";
5
+
6
+ export function useDataCollections() {
7
+ const model_components_db = database.model_components;
8
+ const model_components_relation_db = database.model_components_relation;
9
+ const { getAllMeshComponents } = useDataMesh();
10
+
11
+ async function hasCollectionComponents(modelId) {
12
+ const count = await model_components_db
13
+ .where("id")
14
+ .equals(modelId)
15
+ .and((component) =>
16
+ ["Horizon", "Fault", "FaultBlock", "StratigraphicUnit", "ModelBoundary"].includes(
17
+ component.type,
18
+ ),
19
+ )
20
+ .count();
21
+ return count > 0;
22
+ }
23
+
24
+ async function getAllCollectionComponents(modelId) {
25
+ const items = await model_components_db.where("id").equals(modelId).toArray();
26
+ return items
27
+ .filter((component) =>
28
+ ["Horizon", "Fault", "FaultBlock", "StratigraphicUnit", "ModelBoundary"].includes(
29
+ component.type,
30
+ ),
31
+ )
32
+ .map((component) => ({
33
+ id: component.geode_id,
34
+ title: component.name,
35
+ category: component.type,
36
+ viewer_id: Number(component.viewer_id),
37
+ is_active: component.is_active,
38
+ }));
39
+ }
40
+
41
+ async function fetchAllCollectionComponents(modelId) {
42
+ const components = await getAllCollectionComponents(modelId);
43
+ const relations = await model_components_relation_db.where("id").equals(modelId).toArray();
44
+ const allMeshComponents = await getAllMeshComponents(modelId);
45
+ const meshComponentsById = {};
46
+ for (const meshComponent of allMeshComponents) {
47
+ meshComponentsById[meshComponent.id] = meshComponent;
48
+ }
49
+
50
+ const byType = {};
51
+ for (const component of components) {
52
+ if (!byType[component.category]) {
53
+ byType[component.category] = [];
54
+ }
55
+ const itemRelations = relations.filter(
56
+ (relation) => relation.parent === component.id && relation.type === "collection",
57
+ );
58
+ const children = itemRelations
59
+ .map((relation) => meshComponentsById[relation.child])
60
+ .filter(Boolean);
61
+ byType[component.category].push({
62
+ ...component,
63
+ children,
64
+ });
65
+ }
66
+ return byType;
67
+ }
68
+
69
+ async function formatedCollectionComponents(modelId) {
70
+ const byType = await fetchAllCollectionComponents(modelId);
71
+ const collectionTitles = {
72
+ Horizon: "Horizons",
73
+ Fault: "Faults",
74
+ FaultBlock: "FaultBlocks",
75
+ StratigraphicUnit: "StratigraphicUnits",
76
+ ModelBoundary: "ModelBoundaries",
77
+ };
78
+
79
+ return Object.keys(collectionTitles)
80
+ .filter((type) => byType[type] && byType[type].length > 0)
81
+ .map((type) => ({
82
+ id: type,
83
+ title: collectionTitles[type],
84
+ children: byType[type],
85
+ }));
86
+ }
87
+
88
+ function refFormatedCollectionComponents(modelId) {
89
+ return useObservable(
90
+ liveQuery(() => formatedCollectionComponents(modelId)),
91
+ { initialValue: undefined },
92
+ );
93
+ }
94
+
95
+ return {
96
+ hasCollectionComponents,
97
+ getAllCollectionComponents,
98
+ fetchAllCollectionComponents,
99
+ formatedCollectionComponents,
100
+ refFormatedCollectionComponents,
101
+ };
102
+ }
@@ -0,0 +1,122 @@
1
+ import { database } from "@ogw_internal/database/database.js";
2
+ import { liveQuery } from "dexie";
3
+ import { useObservable } from "@vueuse/rxjs";
4
+
5
+ export function useDataMesh() {
6
+ const model_components_db = database.model_components;
7
+
8
+ async function formatedMeshComponents(modelId) {
9
+ const items = await model_components_db.where("id").equals(modelId).toArray();
10
+ const componentTitles = {
11
+ Corner: "Corners",
12
+ Line: "Lines",
13
+ Surface: "Surfaces",
14
+ Block: "Blocks",
15
+ };
16
+
17
+ const componentsByType = {};
18
+ for (const component_item of items) {
19
+ if (componentTitles[component_item.type]) {
20
+ if (!componentsByType[component_item.type]) {
21
+ componentsByType[component_item.type] = [];
22
+ }
23
+ componentsByType[component_item.type].push(component_item);
24
+ }
25
+ }
26
+
27
+ return Object.keys(componentTitles)
28
+ .filter((type) => componentsByType[type])
29
+ .map((type) => ({
30
+ id: type,
31
+ title: componentTitles[type],
32
+ children: componentsByType[type].map((meshComponent) => ({
33
+ id: meshComponent.geode_id,
34
+ title: meshComponent.name,
35
+ category: meshComponent.type,
36
+ viewer_id: Number(meshComponent.viewer_id),
37
+ is_active: meshComponent.is_active,
38
+ })),
39
+ }));
40
+ }
41
+
42
+ function refFormatedMeshComponents(modelId) {
43
+ return useObservable(
44
+ liveQuery(() => formatedMeshComponents(modelId)),
45
+ { initialValue: undefined },
46
+ );
47
+ }
48
+
49
+ async function getMeshComponentsByType(modelId, type) {
50
+ const components = await model_components_db
51
+ .where("[id+type]")
52
+ .equals([modelId, type])
53
+ .toArray();
54
+ return components.map((meshComponent) => ({
55
+ id: meshComponent.geode_id,
56
+ title: meshComponent.name,
57
+ category: meshComponent.type,
58
+ viewer_id: Number(meshComponent.viewer_id),
59
+ is_active: meshComponent.is_active,
60
+ }));
61
+ }
62
+
63
+ async function getAllMeshComponents(modelId) {
64
+ const items = await model_components_db.where("id").equals(modelId).toArray();
65
+ return items.map((meshComponent) => ({
66
+ id: meshComponent.geode_id,
67
+ title: meshComponent.name,
68
+ category: meshComponent.type,
69
+ viewer_id: Number(meshComponent.viewer_id),
70
+ is_active: meshComponent.is_active,
71
+ }));
72
+ }
73
+
74
+ async function fetchAllMeshComponents(modelId) {
75
+ const components = await getAllMeshComponents(modelId);
76
+ const byType = {};
77
+ for (const component of components) {
78
+ if (!byType[component.category]) {
79
+ byType[component.category] = [];
80
+ }
81
+ byType[component.category].push(component);
82
+ }
83
+ return byType;
84
+ }
85
+
86
+ async function getMeshComponentGeodeIds(modelId, type) {
87
+ const components = await model_components_db
88
+ .where("[id+type]")
89
+ .equals([modelId, type])
90
+ .toArray();
91
+ return components.map((component) => component.geode_id);
92
+ }
93
+
94
+ async function getCornersGeodeIds(modelId) {
95
+ return await getMeshComponentGeodeIds(modelId, "Corner");
96
+ }
97
+
98
+ async function getLinesGeodeIds(modelId) {
99
+ return await getMeshComponentGeodeIds(modelId, "Line");
100
+ }
101
+
102
+ async function getSurfacesGeodeIds(modelId) {
103
+ return await getMeshComponentGeodeIds(modelId, "Surface");
104
+ }
105
+
106
+ async function getBlocksGeodeIds(modelId) {
107
+ return await getMeshComponentGeodeIds(modelId, "Block");
108
+ }
109
+
110
+ return {
111
+ formatedMeshComponents,
112
+ refFormatedMeshComponents,
113
+ getMeshComponentsByType,
114
+ getAllMeshComponents,
115
+ fetchAllMeshComponents,
116
+ getMeshComponentGeodeIds,
117
+ getCornersGeodeIds,
118
+ getLinesGeodeIds,
119
+ getSurfacesGeodeIds,
120
+ getBlocksGeodeIds,
121
+ };
122
+ }
@@ -1,6 +1,6 @@
1
1
  import { defineStore } from "pinia";
2
2
 
3
- import { ref, toRaw, watch } from "vue";
3
+ import { ref, watch } from "vue";
4
4
  import { compareSelections } from "@ogw_front/utils/treeview";
5
5
  import { database } from "@ogw_internal/database/database";
6
6
 
@@ -45,13 +45,20 @@ export const useTreeviewStore = defineStore("treeview", () => {
45
45
  watch(
46
46
  [opened_views, panelWidth, additionalPanelWidth, selection, rowHeights],
47
47
  () => {
48
+ // oxlint-disable-next-line unicorn/prefer-structured-clone
49
+ const clean_opened_views = JSON.parse(JSON.stringify(opened_views.value));
50
+ // oxlint-disable-next-line unicorn/prefer-structured-clone
51
+ const clean_selectionIds = JSON.parse(JSON.stringify(selection.value));
52
+ // oxlint-disable-next-line unicorn/prefer-structured-clone
53
+ const clean_rowHeights = JSON.parse(JSON.stringify(rowHeights.value));
54
+
48
55
  database.treeview_config.put({
49
56
  id: "main",
50
- opened_views: toRaw(opened_views.value),
57
+ opened_views: clean_opened_views,
51
58
  panelWidth: panelWidth.value,
52
59
  additionalPanelWidth: additionalPanelWidth.value,
53
- selectionIds: toRaw(selection.value),
54
- rowHeights: toRaw(rowHeights.value),
60
+ selectionIds: clean_selectionIds,
61
+ rowHeights: clean_rowHeights,
55
62
  });
56
63
  },
57
64
  { deep: true },
@@ -127,15 +134,18 @@ export const useTreeviewStore = defineStore("treeview", () => {
127
134
  }
128
135
  }
129
136
 
130
- function displayAdditionalTree(id, title, geodeObjectType) {
131
- const index = opened_views.value.findIndex((view) => view.id === id);
137
+ function displayAdditionalTree(id, title, geodeObjectType, viewType = "model_components") {
138
+ const viewId = `${id}_${viewType}`;
139
+ const index = opened_views.value.findIndex((view) => view.id === viewId);
132
140
  if (index !== -1) {
133
- return closeView(id);
141
+ return closeView(viewId);
134
142
  }
135
143
  additionalPanelWidth.value = panelWidth.value;
136
144
  opened_views.value.push({
137
145
  type: "component",
138
- id,
146
+ id: viewId,
147
+ modelId: id,
148
+ viewType,
139
149
  title: title || id,
140
150
  geode_object_type: geodeObjectType,
141
151
  scrollTop: 0,
@@ -4,6 +4,7 @@ const NEAR_ZERO_THRESHOLD = 1e-10;
4
4
  const SLERP_LINEAR_THRESHOLD = 0.9995;
5
5
  const LONG_ANIMATION_DURATION = 1000;
6
6
  const SHORT_ANIMATION_DURATION = 500;
7
+ const MID_ANIMATION_RATIO = 0.5;
7
8
 
8
9
  function vecSub(vector, other) {
9
10
  return [vector[0] - other[0], vector[1] - other[1], vector[2] - other[2]];
@@ -21,7 +22,7 @@ function vecNormalize(vector) {
21
22
  return [vector[0] / len, vector[1] / len, vector[2] / len];
22
23
  }
23
24
 
24
- function slerp(from, target, ratio) {
25
+ function slerp(from, target, ratio, mid) {
25
26
  const normFrom = vecNormalize(from);
26
27
  const normTarget = vecNormalize(target);
27
28
  let dotProduct =
@@ -34,6 +35,11 @@ function slerp(from, target, ratio) {
34
35
  normFrom[2] + (normTarget[2] - normFrom[2]) * ratio,
35
36
  ]);
36
37
  }
38
+ if (dotProduct < -SLERP_LINEAR_THRESHOLD && mid) {
39
+ return ratio < MID_ANIMATION_RATIO
40
+ ? slerp(normFrom, mid, ratio * 2)
41
+ : slerp(mid, normTarget, (ratio - MID_ANIMATION_RATIO) * 2);
42
+ }
37
43
  const theta = Math.acos(dotProduct);
38
44
  const sinTheta = Math.sin(theta);
39
45
  const weightFrom = Math.sin((1 - ratio) * theta) / sinTheta;
@@ -71,6 +77,22 @@ function animateCamera(options) {
71
77
  const targetDir = vecSub(targetState.position, targetState.focal_point);
72
78
  const startDist = vecLength(startDir);
73
79
  const targetDist = vecLength(targetDir);
80
+
81
+ const normStart = vecNormalize(startDir);
82
+ const normTarget = vecNormalize(targetDir);
83
+ const startTargetDot =
84
+ normStart[0] * normTarget[0] + normStart[1] * normTarget[1] + normStart[2] * normTarget[2];
85
+
86
+ let antipodalMid = undefined;
87
+ if (startTargetDot < -SLERP_LINEAR_THRESHOLD) {
88
+ const normUp = vecNormalize(startState.view_up);
89
+ antipodalMid = vecNormalize([
90
+ normStart[1] * normUp[2] - normStart[2] * normUp[1],
91
+ normStart[2] * normUp[0] - normStart[0] * normUp[2],
92
+ normStart[0] * normUp[1] - normStart[1] * normUp[0],
93
+ ]);
94
+ }
95
+
74
96
  const startTime = performance.now();
75
97
  function animate(currentTime) {
76
98
  const progress = Math.min((currentTime - startTime) / duration, 1);
@@ -79,7 +101,7 @@ function animateCamera(options) {
79
101
  ? 1 - (1 - progress) ** easeExponent
80
102
  : progress * (2 - progress);
81
103
  const bump = bumpMultiplier * Math.sin(Math.PI * progress);
82
- const dir = slerp(startDir, targetDir, ease);
104
+ const dir = slerp(startDir, targetDir, ease, antipodalMid);
83
105
  const dist = startDist + (targetDist - startDist) * ease + bump;
84
106
  const focalPoint = startState.focal_point.map(
85
107
  (startValue, index) => startValue + (targetState.focal_point[index] - startValue) * ease,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.20.1",
3
+ "version": "10.21.0-rc.1",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -34,8 +34,8 @@
34
34
  "build": ""
35
35
  },
36
36
  "dependencies": {
37
- "@geode/opengeodeweb-back": "latest",
38
- "@geode/opengeodeweb-viewer": "latest",
37
+ "@geode/opengeodeweb-back": "next",
38
+ "@geode/opengeodeweb-viewer": "next",
39
39
  "@google-cloud/run": "3.2.0",
40
40
  "@kitware/vtk.js": "33.3.0",
41
41
  "@mdi/font": "7.4.47",