@geode/opengeodeweb-front 10.8.0 → 10.9.0

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.
@@ -0,0 +1,30 @@
1
+ <script setup>
2
+ const DEFAULT_ICON_SIZE = 28;
3
+ const { icon, tooltip, color, size, variant, density, tooltipLocation, iconSize } = defineProps({
4
+ icon: { type: String, required: true },
5
+ tooltip: { type: String, required: true },
6
+ color: { type: String, default: undefined },
7
+ size: { type: [String, Number], default: undefined },
8
+ variant: { type: String },
9
+ density: { type: String, default: "comfortable" },
10
+ tooltipLocation: { type: String, default: "left" },
11
+ iconSize: { type: [String, Number], default: DEFAULT_ICON_SIZE },
12
+ });
13
+
14
+ const emit = defineEmits(["click"]);
15
+ </script>
16
+
17
+ <template>
18
+ <v-btn
19
+ :color="color"
20
+ :size="size"
21
+ :variant="variant"
22
+ :density="density"
23
+ v-tooltip:[tooltipLocation]="tooltip"
24
+ v-bind="$attrs"
25
+ icon
26
+ @click="emit('click', $event)"
27
+ >
28
+ <v-icon :size="iconSize">{{ icon }}</v-icon>
29
+ </v-btn>
30
+ </template>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
3
3
 
4
+ import ActionButton from "@ogw_front/components/ActionButton.vue";
4
5
  import Screenshot from "@ogw_front/components/Screenshot";
5
6
  import ZScaling from "@ogw_front/components/ZScaling";
6
7
 
@@ -71,14 +72,11 @@ const camera_options = [
71
72
  <v-container :class="[$style.floatToolbar, 'pa-0']" width="auto">
72
73
  <v-row v-for="camera_option in camera_options" :key="camera_option.icon" dense>
73
74
  <v-col>
74
- <v-btn
75
- density="comfortable"
76
- icon
75
+ <ActionButton
76
+ :icon="camera_option.icon"
77
+ :tooltip="camera_option.tooltip"
77
78
  @click.stop="camera_option.action"
78
- v-tooltip:left="camera_option.tooltip"
79
- >
80
- <v-icon :icon="camera_option.icon" size="32" />
81
- </v-btn>
79
+ />
82
80
  </v-col>
83
81
  </v-row>
84
82
  </v-container>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
3
3
 
4
+ import ActionButton from "@ogw_front/components/ActionButton.vue";
4
5
  import Screenshot from "@ogw_front/components/Screenshot";
5
6
  import { useViewerStore } from "@ogw_front/stores/viewer";
6
7
 
@@ -45,14 +46,12 @@ const camera_options = [
45
46
  <v-container :class="[$style.floatToolbar, 'pa-0']" width="auto">
46
47
  <v-row v-for="camera_option in camera_options" :key="camera_option.icon" dense>
47
48
  <v-col>
48
- <v-btn
49
- density="comfortable"
50
- icon
51
- @click.stop="camera_option.action"
52
- v-tooltip:left="camera_option.tooltip"
53
- >
54
- <v-icon :icon="camera_option.icon" size="32" />
55
- </v-btn>
49
+ <ActionButton
50
+ :tooltip="camera_option.tooltip"
51
+ :icon="camera_option.icon"
52
+ tooltip-location="left"
53
+ @click="camera_option.action"
54
+ />
56
55
  </v-col>
57
56
  </v-row>
58
57
  </v-container>
@@ -11,33 +11,24 @@ function goBackToFileTree() {
11
11
  treeviewStore.displayFileTree();
12
12
  }
13
13
 
14
- const model_id = computed(() => treeviewStore.model_id);
15
-
16
- const metaDatas = computed(() => {
17
- if (!model_id.value) {
18
- return {};
19
- }
20
- return dataStore.refItem(model_id.value).value || {};
21
- });
14
+ const metaDatas = computed(() => dataStore.refItem(treeviewStore.model_id));
22
15
  </script>
23
16
 
24
17
  <template>
25
18
  <v-breadcrumbs class="mb-n10 breadcrumb-container">
26
19
  <div class="d-flex align-center gap-2 ml-2 mt-2 mb-1">
27
- <v-menu v-if="treeviewStore.isAdditionnalTreeDisplayed" offset-y>
28
- <template v-slot:activator="{ props }">
29
- <v-btn icon variant="text" size="medium" v-bind="props" @click="goBackToFileTree">
30
- <v-icon size="large">mdi-file-tree</v-icon>
31
- </v-btn>
32
- <span class="text-h5 font-weight-bold">/</span>
33
- <v-icon size="x-large">
34
- {{ selectedTree && selectedTree.icon ? selectedTree.icon : "mdi-shape-outline" }}
35
- </v-icon>
36
- <span class="text-subtitle-1 font-weight-regular align-center mt-1">
37
- Model Explorer ({{ metaDatas.name }})
38
- </span>
39
- </template>
40
- </v-menu>
20
+ <template v-if="treeviewStore.isAdditionnalTreeDisplayed">
21
+ <v-btn icon variant="text" size="medium" @click.stop="goBackToFileTree">
22
+ <v-icon size="large">mdi-file-tree</v-icon>
23
+ </v-btn>
24
+ <span class="text-h5 font-weight-bold">/</span>
25
+ <v-icon size="large">
26
+ {{ selectedTree && selectedTree.icon ? selectedTree.icon : "mdi-shape-outline" }}
27
+ </v-icon>
28
+ <span class="text-subtitle-1 font-weight-regular align-center mt-1">
29
+ Model Explorer ({{ metaDatas.name }})
30
+ </span>
31
+ </template>
41
32
 
42
33
  <div v-else class="d-flex align-center gap-2">
43
34
  <v-icon size="large">mdi-file-tree</v-icon>
@@ -49,6 +40,8 @@ const metaDatas = computed(() => {
49
40
 
50
41
  <style scoped>
51
42
  .breadcrumb-container {
43
+ position: relative;
44
+ z-index: 10;
52
45
  max-width: 100%;
53
46
  overflow: hidden;
54
47
  white-space: nowrap;
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import ActionButton from "@ogw_front/components/ActionButton.vue";
3
+ import SearchBar from "@ogw_front/components/SearchBar.vue";
2
4
  import { compareSelections } from "@ogw_front/utils/treeview";
3
5
  import { useDataStore } from "@ogw_front/stores/data";
4
6
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
@@ -11,50 +13,132 @@ const hybridViewerStore = useHybridViewerStore();
11
13
  const { id } = defineProps({ id: { type: String, required: true } });
12
14
  const emit = defineEmits(["show-menu"]);
13
15
 
14
- const items = dataStore.refFormatedMeshComponents(id);
15
- const mesh_components_selection = computed(() => dataStyleStore.visibleMeshComponents(id));
16
+ const items = dataStore.refFormatedMeshComponents(toRef(() => id));
17
+ const mesh_components_selection = dataStyleStore.visibleMeshComponents(toRef(() => id));
16
18
 
17
- watch(
18
- mesh_components_selection,
19
- async (current, previous) => {
20
- if (!previous) {
21
- return;
22
- }
19
+ const search = ref("");
20
+ const sortType = ref("name");
21
+ const filterOptions = ref({
22
+ Corner: true,
23
+ Line: true,
24
+ Surface: true,
25
+ Block: true,
26
+ });
23
27
 
24
- const { added, removed } = compareSelections(current, previous);
25
- console.log("TreeComponent selection change:", {
26
- id: props.id,
27
- added,
28
- removed,
29
- });
30
-
31
- if (added.length > 0) {
32
- await dataStyleStore.setModelMeshComponentsVisibility(id, added, true);
33
- }
34
- if (removed.length > 0) {
35
- await dataStyleStore.setModelMeshComponentsVisibility(id, removed, false);
36
- }
37
- hybridViewerStore.remoteRender();
38
- },
39
- { deep: true },
28
+ const processedItems = computed(() =>
29
+ items.value
30
+ .filter((category) => filterOptions.value[category.id])
31
+ .map((category) => {
32
+ const field = sortType.value === "name" ? "title" : "id";
33
+ const children = (category.children || []).toSorted((first, second) =>
34
+ first[field].localeCompare(second[field]),
35
+ );
36
+ return { id: category.id, title: category.title, children };
37
+ }),
40
38
  );
39
+
40
+ const availableFilterOptions = computed(() => items.value.map((category) => category.id));
41
+
42
+ function toggleSort() {
43
+ sortType.value = sortType.value === "name" ? "id" : "name";
44
+ }
45
+
46
+ function customFilter(value, searchQuery, item) {
47
+ if (!searchQuery) {
48
+ return true;
49
+ }
50
+ const query = searchQuery.toLowerCase();
51
+ return (
52
+ item.raw.title.toLowerCase().includes(query) ||
53
+ (item.raw.id && item.raw.id.toLowerCase().includes(query))
54
+ );
55
+ }
56
+
57
+ async function onSelectionChange(current) {
58
+ const previous = mesh_components_selection.value;
59
+ const { added, removed } = compareSelections(current, previous);
60
+
61
+ if (added.length === 0 && removed.length === 0) {
62
+ return;
63
+ }
64
+
65
+ if (added.length > 0) {
66
+ await dataStyleStore.setComponentsVisibility(id, added, true);
67
+ }
68
+ if (removed.length > 0) {
69
+ await dataStyleStore.setComponentsVisibility(id, removed, false);
70
+ }
71
+ hybridViewerStore.remoteRender();
72
+ }
41
73
  </script>
42
74
 
43
75
  <template>
76
+ <v-row dense align="center" class="mr-1 ml-3 mt-2 pa-1">
77
+ <v-col>
78
+ <SearchBar v-model="search" label="Search" color="black" base-color="black" />
79
+ </v-col>
80
+ <v-col cols="auto" class="d-flex align-center">
81
+ <ActionButton
82
+ :tooltip="'Sort by ' + (sortType === 'name' ? 'ID' : 'Name')"
83
+ :icon="
84
+ sortType === 'name' ? 'mdi-sort-alphabetical-ascending' : 'mdi-sort-numeric-ascending'
85
+ "
86
+ tooltipLocation="bottom"
87
+ @click="toggleSort"
88
+ />
89
+ <v-menu :close-on-content-click="false">
90
+ <template #activator="{ props }">
91
+ <ActionButton
92
+ tooltip="Filter options"
93
+ icon="mdi-filter-variant"
94
+ tooltipLocation="bottom"
95
+ class="ml-1"
96
+ v-bind="props"
97
+ />
98
+ </template>
99
+ <v-list class="mt-1">
100
+ <v-list-item v-for="category_id in availableFilterOptions" :key="category_id">
101
+ <v-checkbox
102
+ v-model="filterOptions[category_id]"
103
+ :label="category_id"
104
+ hide-details
105
+ density="compact"
106
+ />
107
+ </v-list-item>
108
+ </v-list>
109
+ </v-menu>
110
+ </v-col>
111
+ </v-row>
44
112
  <v-treeview
45
- v-model:selected="mesh_components_selection"
46
- :items="items"
113
+ :selected="mesh_components_selection"
114
+ :items="processedItems"
115
+ :search="search"
116
+ :custom-filter="customFilter"
47
117
  class="transparent-treeview"
48
118
  item-value="id"
49
- select-strategy="classic"
119
+ select-strategy="independent"
50
120
  selectable
121
+ @update:selected="onSelectionChange"
51
122
  >
52
123
  <template #title="{ item }">
53
124
  <span
54
125
  class="treeview-item"
126
+ :class="{ 'inactive-item': item.is_active === false }"
55
127
  @contextmenu.prevent.stop="emit('show-menu', { event: $event, itemId: item })"
56
- >{{ item.title }}</span
57
128
  >
129
+ {{ item.title }}
130
+ <v-tooltip v-if="item.category" activator="parent" location="right">
131
+ <div class="d-flex flex-column pa-1">
132
+ <span class="text-caption"><strong>ID:</strong> {{ item.id }}</span>
133
+ <span v-if="item.title" class="text-caption"
134
+ ><strong>Name:</strong> {{ item.title }}</span
135
+ >
136
+ <span class="text-caption font-italic border-t-sm d-flex align-center">
137
+ <strong class="mr-1">Status:</strong> {{ item.is_active ? "Active" : "Inactive" }}
138
+ </span>
139
+ </div>
140
+ </v-tooltip>
141
+ </span>
58
142
  </template>
59
143
  </v-treeview>
60
144
  </template>
@@ -67,6 +151,10 @@ watch(
67
151
  max-width: 100%;
68
152
  display: inline-block;
69
153
  }
154
+ .inactive-item {
155
+ opacity: 0.4;
156
+ font-style: italic;
157
+ }
70
158
 
71
159
  .transparent-treeview {
72
160
  background-color: transparent;
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import ActionButton from "@ogw_front/components/ActionButton.vue";
3
+ import SearchBar from "@ogw_front/components/SearchBar.vue";
2
4
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
3
5
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
4
6
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
@@ -11,6 +13,56 @@ const hybridViewerStore = useHybridViewerStore();
11
13
 
12
14
  const emit = defineEmits(["show-menu"]);
13
15
 
16
+ const search = ref("");
17
+ const sortType = ref("name");
18
+ const filterOptions = ref({});
19
+
20
+ const processedItems = computed(() =>
21
+ treeviewStore.items
22
+ .filter((category) => filterOptions.value[category.title] !== false)
23
+ .map((category) => {
24
+ const field = sortType.value === "name" ? "title" : "id";
25
+ const children = (category.children || []).toSorted((first, second) =>
26
+ first[field].localeCompare(second[field], undefined, {
27
+ numeric: true,
28
+ sensitivity: "base",
29
+ }),
30
+ );
31
+ return { id: category.title, title: category.title, children };
32
+ }),
33
+ );
34
+
35
+ const availableFilterOptions = computed(() =>
36
+ treeviewStore.items.map((category) => category.title),
37
+ );
38
+
39
+ watch(
40
+ availableFilterOptions,
41
+ (newOptions) => {
42
+ for (const option of newOptions) {
43
+ if (filterOptions.value[option] === undefined) {
44
+ filterOptions.value[option] = true;
45
+ }
46
+ }
47
+ },
48
+ { immediate: true },
49
+ );
50
+
51
+ function toggleSort() {
52
+ sortType.value = sortType.value === "name" ? "id" : "name";
53
+ }
54
+
55
+ function customFilter(value, searchQuery, item) {
56
+ if (!searchQuery) {
57
+ return true;
58
+ }
59
+ const query = searchQuery.toLowerCase();
60
+ return (
61
+ item.raw.title.toLowerCase().includes(query) ||
62
+ (item.raw.id && item.raw.id.toLowerCase().includes(query))
63
+ );
64
+ }
65
+
14
66
  function isLeafNode(item) {
15
67
  return !item.children || item.children.length === 0;
16
68
  }
@@ -24,8 +76,8 @@ watch(
24
76
  }
25
77
  const { added, removed } = compareSelections(current, previous);
26
78
  const updates = [
27
- ...added.map((item) => dataStyleStore.setVisibility(item.id, true)),
28
- ...removed.map((item) => dataStyleStore.setVisibility(item.id, false)),
79
+ ...added.map((id) => dataStyleStore.setVisibility(id, true)),
80
+ ...removed.map((id) => dataStyleStore.setVisibility(id, false)),
29
81
  ];
30
82
  await Promise.all(updates);
31
83
  hybridViewerStore.remoteRender();
@@ -45,10 +97,47 @@ onMounted(() => {
45
97
  </script>
46
98
 
47
99
  <template>
100
+ <v-row v-if="treeviewStore.items.length > 0" dense align="center" class="mr-1 ml-3 mt-2 pa-1">
101
+ <v-col>
102
+ <SearchBar v-model="search" label="Search" color="black" base-color="black" />
103
+ </v-col>
104
+ <v-col cols="auto" class="d-flex align-center">
105
+ <ActionButton
106
+ :tooltip="'Sort by ' + (sortType === 'name' ? 'ID' : 'Name')"
107
+ :icon="
108
+ sortType === 'name' ? 'mdi-sort-alphabetical-ascending' : 'mdi-sort-numeric-ascending'
109
+ "
110
+ tooltipLocation="bottom"
111
+ @click="toggleSort"
112
+ />
113
+ <v-menu :close-on-content-click="false">
114
+ <template #activator="{ props }">
115
+ <ActionButton
116
+ tooltip="Filter options"
117
+ icon="mdi-filter-variant"
118
+ tooltipLocation="bottom"
119
+ class="ml-1"
120
+ v-bind="props"
121
+ />
122
+ </template>
123
+ <v-list class="mt-1">
124
+ <v-list-item v-for="category_id in availableFilterOptions" :key="category_id">
125
+ <v-checkbox
126
+ v-model="filterOptions[category_id]"
127
+ :label="category_id"
128
+ hide-details
129
+ density="compact"
130
+ />
131
+ </v-list-item>
132
+ </v-list>
133
+ </v-menu>
134
+ </v-col>
135
+ </v-row>
48
136
  <v-treeview
49
137
  v-model:selected="treeviewStore.selection"
50
- :items="treeviewStore.items"
51
- return-object
138
+ :items="processedItems"
139
+ :search="search"
140
+ :custom-filter="customFilter"
52
141
  class="transparent-treeview"
53
142
  item-value="id"
54
143
  select-strategy="classic"
@@ -35,7 +35,7 @@ export const useDataStore = defineStore("data", () => {
35
35
  }
36
36
 
37
37
  async function formatedMeshComponents(modelId) {
38
- const items = await database.model_components.where({ id: modelId }).toArray();
38
+ const items = await database.model_components.where("id").equals(modelId).toArray();
39
39
  const componentTitles = {
40
40
  Corner: "Corners",
41
41
  Line: "Lines",
@@ -62,19 +62,26 @@ export const useDataStore = defineStore("data", () => {
62
62
  id: meshComponent.geode_id,
63
63
  title: meshComponent.name,
64
64
  category: meshComponent.type,
65
+ is_active: meshComponent.is_active,
65
66
  })),
66
67
  }));
67
68
  }
68
69
 
69
70
  function refFormatedMeshComponents(id) {
70
71
  return useObservable(
71
- liveQuery(() => formatedMeshComponents(id)),
72
+ liveQuery(() => {
73
+ const unwrapped_id = unref(id);
74
+ return formatedMeshComponents(unwrapped_id);
75
+ }),
72
76
  { initialValue: [] },
73
77
  );
74
78
  }
75
79
 
76
80
  async function meshComponentType(modelId, geode_id) {
77
- const component = await database.model_components.where({ id: modelId, geode_id }).first();
81
+ const component = await database.model_components
82
+ .where("[id+geode_id]")
83
+ .equals([modelId, geode_id])
84
+ .first();
78
85
  return component?.type;
79
86
  }
80
87
 
@@ -109,6 +116,7 @@ export const useDataStore = defineStore("data", () => {
109
116
  type: component.type,
110
117
  viewer_id: component.viewer_id,
111
118
  name: component.name,
119
+ is_active: component.is_active,
112
120
  });
113
121
  }
114
122
  }
@@ -155,13 +163,14 @@ export const useDataStore = defineStore("data", () => {
155
163
  }
156
164
 
157
165
  async function deleteModelComponents(modelId) {
158
- await database.model_components.where({ id: modelId }).delete();
159
- await database.model_components_relation.where({ id: modelId }).delete();
166
+ await database.model_components.where("id").equals(modelId).delete();
167
+ await database.model_components_relation.where("id").equals(modelId).delete();
160
168
  }
161
169
 
162
170
  async function getMeshComponentGeodeIds(modelId, component_type) {
163
171
  const components = await database.model_components
164
- .where({ id: modelId, type: component_type })
172
+ .where("[id+type]")
173
+ .equals([modelId, component_type])
165
174
  .toArray();
166
175
  return components.map((component) => component.geode_id);
167
176
  }
@@ -221,6 +230,7 @@ export const useDataStore = defineStore("data", () => {
221
230
  getLinesGeodeIds,
222
231
  getSurfacesGeodeIds,
223
232
  getBlocksGeodeIds,
233
+ getMeshComponentGeodeIds,
224
234
  getMeshComponentsViewerIds,
225
235
 
226
236
  exportStores,
@@ -85,6 +85,7 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
85
85
  selectedObjects: dataStyleState.selectedObjects,
86
86
  ...meshStyleStore,
87
87
  ...modelStyleStore,
88
+ setComponentsVisibility: modelStyleStore.setModelComponentsVisibility,
88
89
  addDataStyle,
89
90
  applyDefaultStyle,
90
91
  setVisibility,
@@ -25,7 +25,7 @@ export const useTreeviewStore = defineStore("treeview", () => {
25
25
  sensitivity: "base",
26
26
  }),
27
27
  );
28
- selection.value.push(child);
28
+ selection.value.push(id);
29
29
  return;
30
30
  }
31
31
  }
@@ -36,7 +36,7 @@ export const useTreeviewStore = defineStore("treeview", () => {
36
36
  sensitivity: "base",
37
37
  }),
38
38
  );
39
- selection.value.push(child);
39
+ selection.value.push(id);
40
40
  }
41
41
 
42
42
  function displayAdditionalTree(id) {
@@ -14,6 +14,10 @@ import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schem
14
14
 
15
15
  const model_schemas = viewer_schemas.opengeodeweb_viewer.model;
16
16
 
17
+ const MESH_CONFIG = [{ type: "Corner" }, { type: "Line" }, { type: "Surface" }, { type: "Block" }];
18
+
19
+ const MESH_TYPES = MESH_CONFIG.map((config) => config.type);
20
+
17
21
  export function useModelStyle() {
18
22
  const dataStore = useDataStore();
19
23
  const dataStyleStateStore = useDataStyleStateStore();
@@ -26,19 +30,54 @@ export function useModelStyle() {
26
30
  const hybridViewerStore = useHybridViewerStore();
27
31
  const viewerStore = useViewerStore();
28
32
 
29
- function modelVisibility(id) {
30
- return dataStyleStateStore.getStyle(id).visibility;
33
+ function buildSelection(modelId, components, stylesMap) {
34
+ const componentsByType = Object.fromEntries(MESH_TYPES.map((type) => [type, []]));
35
+ for (const component of components) {
36
+ if (componentsByType[component.type]) {
37
+ componentsByType[component.type].push(component);
38
+ }
39
+ }
40
+
41
+ const groupStyles = dataStyleStateStore.getStyle(modelId);
42
+ const selection = [];
43
+ for (const type of MESH_TYPES) {
44
+ const typeComponents = componentsByType[type];
45
+ if (typeComponents.length === 0) {
46
+ continue;
47
+ }
48
+
49
+ const typeKey = `${type.toLowerCase()}s`;
50
+ const defaultVisibility = groupStyles[typeKey].visibility;
51
+
52
+ let allVisible = true;
53
+ for (const component of typeComponents) {
54
+ const isVisible = stylesMap[component.geode_id]?.visibility ?? defaultVisibility;
55
+ if (isVisible) {
56
+ selection.push(component.geode_id);
57
+ } else {
58
+ allVisible = false;
59
+ }
60
+ }
61
+ if (allVisible) {
62
+ selection.push(type);
63
+ }
64
+ }
65
+ return selection;
66
+ }
67
+
68
+ function modelVisibility(modelId) {
69
+ return dataStyleStateStore.getStyle(modelId).visibility;
31
70
  }
32
71
 
33
- function setModelVisibility(id, visibility) {
72
+ function setModelVisibility(modelId, visibility) {
34
73
  return viewerStore.request(
35
74
  model_schemas.visibility,
36
- { id, visibility },
75
+ { id: modelId, visibility },
37
76
  {
38
77
  response_function: async () => {
39
- await hybridViewerStore.setVisibility(id, visibility);
40
- await dataStyleStateStore.mutateStyle(id, { visibility });
41
- return { id, visibility };
78
+ await hybridViewerStore.setVisibility(modelId, visibility);
79
+ await dataStyleStateStore.mutateStyle(modelId, { visibility });
80
+ return { id: modelId, visibility };
42
81
  },
43
82
  },
44
83
  );
@@ -47,60 +86,34 @@ export function useModelStyle() {
47
86
  function visibleMeshComponents(id_ref) {
48
87
  const selection = ref([]);
49
88
  watch(
50
- () => (isRef(id_ref) ? id_ref.value : id_ref),
51
- (newId, oldId, onCleanup) => {
52
- if (!newId) {
89
+ () => unref(id_ref),
90
+ (modelId, _prev, onCleanup) => {
91
+ if (!modelId) {
92
+ selection.value = [];
53
93
  return;
54
94
  }
55
95
  const observable = liveQuery(async () => {
56
- const components = await database.model_components.where("id").equals(newId).toArray();
57
- if (components.length === 0) {
96
+ const allComponents = await database.model_components
97
+ .where("id")
98
+ .equals(modelId)
99
+ .toArray();
100
+ if (allComponents.length === 0) {
58
101
  return [];
59
102
  }
60
-
61
- const all_styles = await database.model_component_datastyle
103
+ const componentStyles = await database.model_component_datastyle
62
104
  .where("id_model")
63
- .equals(newId)
105
+ .equals(modelId)
64
106
  .toArray();
65
- const styles = {};
66
- for (const style of all_styles) {
67
- styles[style.id_component] = style;
68
- }
69
-
70
- const current_selection = [];
71
- const meshTypes = new Set(["Corner", "Line", "Surface", "Block"]);
72
- const componentsByType = {};
73
- for (const component of components) {
74
- if (meshTypes.has(component.type)) {
75
- if (!componentsByType[component.type]) {
76
- componentsByType[component.type] = [];
77
- }
78
- componentsByType[component.type].push(component.geode_id);
79
- }
80
- }
81
-
82
- for (const [type, geode_ids] of Object.entries(componentsByType)) {
83
- let all_visible = true;
84
- for (const gid of geode_ids) {
85
- const is_visible = styles[gid] === undefined ? true : styles[gid].visibility;
86
- if (is_visible) {
87
- current_selection.push(gid);
88
- } else {
89
- all_visible = false;
90
- }
91
- }
92
- if (all_visible) {
93
- current_selection.push(type);
94
- }
95
- }
96
- return current_selection;
107
+ const stylesMap = Object.fromEntries(
108
+ componentStyles.map((style) => [style.id_component, style]),
109
+ );
110
+ return buildSelection(modelId, allComponents, stylesMap);
97
111
  });
98
112
 
99
113
  const subscription = observable.subscribe({
100
114
  next: (val) => {
101
115
  selection.value = val;
102
116
  },
103
- error: (err) => console.error(err),
104
117
  });
105
118
  onCleanup(() => subscription.unsubscribe());
106
119
  },
@@ -109,41 +122,69 @@ export function useModelStyle() {
109
122
  return selection;
110
123
  }
111
124
 
112
- function applyModelStyle(id) {
113
- const style = dataStyleStateStore.getStyle(id);
125
+ async function setModelComponentsVisibility(modelId, componentIds, visibility) {
126
+ const allComponents = await database.model_components.where("id").equals(modelId).toArray();
127
+ const componentsMap = Object.fromEntries(
128
+ allComponents.map((component) => [component.geode_id, component]),
129
+ );
130
+
131
+ return Promise.all(
132
+ MESH_TYPES.map(async (type) => {
133
+ const typeComponents = allComponents.filter((component) => component.type === type);
134
+ const isSelectedType = componentIds.includes(type);
135
+ const idsToUpdate = isSelectedType
136
+ ? typeComponents.map((component) => component.geode_id)
137
+ : componentIds.filter((id) => componentsMap[id]?.type === type);
138
+
139
+ if (idsToUpdate.length === 0) {
140
+ return;
141
+ }
142
+
143
+ const viewerIds = await dataStore.getMeshComponentsViewerIds(modelId, idsToUpdate);
144
+ if (viewerIds.length > 0) {
145
+ const schema = model_schemas[`${type.toLowerCase()}s`].visibility;
146
+ await viewerStore.request(schema, { id: modelId, block_ids: viewerIds, visibility });
147
+ }
148
+ return dataStyleStateStore.mutateComponentStyles(modelId, idsToUpdate, { visibility });
149
+ }),
150
+ );
151
+ }
152
+
153
+ function applyModelStyle(modelId) {
154
+ const style = dataStyleStateStore.getStyle(modelId);
114
155
  const handlers = {
115
- visibility: () => setModelVisibility(id, style.visibility),
116
- corners: () => modelCornersStyleStore.applyModelCornersStyle(id),
117
- lines: () => modelLinesStyleStore.applyModelLinesStyle(id),
118
- surfaces: () => modelSurfacesStyleStore.applyModelSurfacesStyle(id),
119
- blocks: () => modelBlocksStyleStore.applyModelBlocksStyle(id),
120
- points: () => modelPointsStyleStore.applyModelPointsStyle(id),
121
- edges: () => modelEdgesStyleStore.applyModelEdgesStyle(id),
156
+ visibility: () => setModelVisibility(modelId, style.visibility),
157
+ corners: () => modelCornersStyleStore.applyModelCornersStyle(modelId),
158
+ lines: () => modelLinesStyleStore.applyModelLinesStyle(modelId),
159
+ surfaces: () => modelSurfacesStyleStore.applyModelSurfacesStyle(modelId),
160
+ blocks: () => modelBlocksStyleStore.applyModelBlocksStyle(modelId),
161
+ points: () => modelPointsStyleStore.applyModelPointsStyle(modelId),
162
+ edges: () => modelEdgesStyleStore.applyModelEdgesStyle(modelId),
122
163
  };
123
164
 
124
- const promises = Object.keys(style)
125
- .filter((key) => handlers[key])
126
- .map((key) => handlers[key]());
127
-
128
- return Promise.all(promises);
165
+ return Promise.all(
166
+ Object.keys(style)
167
+ .filter((key) => handlers[key])
168
+ .map((key) => handlers[key]()),
169
+ );
129
170
  }
130
171
 
131
- function setModelMeshComponentsDefaultStyle(id) {
132
- return dataStore.item(id).then((item) => {
172
+ function setModelMeshComponentsDefaultStyle(modelId) {
173
+ return dataStore.item(modelId).then((item) => {
133
174
  if (!item) {
134
175
  return [];
135
176
  }
136
177
  const { mesh_components } = item;
137
178
  const handlers = {
138
- Corner: () => modelCornersStyleStore.setModelCornersDefaultStyle(id),
139
- Line: () => modelLinesStyleStore.setModelLinesDefaultStyle(id),
140
- Surface: () => modelSurfacesStyleStore.setModelSurfacesDefaultStyle(id),
141
- Block: () => modelBlocksStyleStore.setModelBlocksDefaultStyle(id),
179
+ Corner: () => modelCornersStyleStore.setModelCornersDefaultStyle(modelId),
180
+ Line: () => modelLinesStyleStore.setModelLinesDefaultStyle(modelId),
181
+ Surface: () => modelSurfacesStyleStore.setModelSurfacesDefaultStyle(modelId),
182
+ Block: () => modelBlocksStyleStore.setModelBlocksDefaultStyle(modelId),
142
183
  };
143
184
  return Promise.all(
144
185
  Object.keys(mesh_components)
145
- .filter((k) => handlers[k])
146
- .map((k) => handlers[k]()),
186
+ .filter((key) => handlers[key])
187
+ .map((key) => handlers[key]()),
147
188
  );
148
189
  });
149
190
  }
@@ -152,6 +193,7 @@ export function useModelStyle() {
152
193
  modelVisibility,
153
194
  visibleMeshComponents,
154
195
  setModelVisibility,
196
+ setModelComponentsVisibility,
155
197
  applyModelStyle,
156
198
  setModelMeshComponentsDefaultStyle,
157
199
  ...useModelBlocksStyle(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.8.0",
3
+ "version": "10.9.0",
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": {