@geode/opengeodeweb-front 10.35.0-rc.3 → 10.35.0-rc.5

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.
@@ -3,6 +3,7 @@ import Loading from "@ogw_front/components/Loading.vue";
3
3
  import Recaptcha from "@ogw_front/components/Recaptcha.vue";
4
4
  import { Status } from "@ogw_front/utils/status";
5
5
  import { appMode } from "@ogw_shared/app_mode";
6
+ import { getCloudUrlParam } from "@ogw_front/utils/cloud";
6
7
  import { useInfraStore } from "@ogw_front/stores/infra";
7
8
 
8
9
  interface Props {
@@ -15,8 +16,10 @@ interface Props {
15
16
  const { appName, email = undefined, isUserAuthenticated = false, logo = "" } = defineProps<Props>();
16
17
 
17
18
  const infraStore = useInfraStore();
18
- if (infraStore.app_mode !== appMode.CLOUD) {
19
- infraStore.create_backend();
19
+ // In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
20
+ // Points at an already running service: that one is connected straight away, without authentication.
21
+ if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
22
+ infraStore.create_backend(email);
20
23
  }
21
24
 
22
25
  function cloudCreateBackend(): Promise<void> {
@@ -0,0 +1,291 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ MESH_ELEMENT_KINDS,
4
+ MODEL_COMPONENT_KINDS,
5
+ getAttributeRange,
6
+ } from "@ogw_front/utils/attributes";
7
+ import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
8
+ import ToolPanel from "@ogw_front/components/ToolPanel.vue";
9
+ import ViewerOptionsAttributeRangeSelector from "@ogw_front/components/Viewer/Options/AttributeRangeSelector.vue";
10
+ import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
11
+ import { useBackStore } from "@ogw_front/stores/back";
12
+ import { useDataStore } from "@ogw_front/stores/data";
13
+ import { useDebounceFn } from "@vueuse/core";
14
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
15
+
16
+ const DEBOUNCE_DELAY = 100;
17
+
18
+ interface Props {
19
+ escapeFunction?: () => void;
20
+ }
21
+
22
+ const { escapeFunction = undefined } = defineProps<Props>();
23
+
24
+ interface AttributeSource {
25
+ title: string;
26
+ schema: JsonRpcSchema;
27
+ params: Record<string, unknown>;
28
+ location: "point" | "cell";
29
+ }
30
+
31
+ interface AttributeInfo {
32
+ attribute_name: string;
33
+ nb_items: number;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ const show = defineModel<boolean>("show", { default: false });
38
+ const backStore = useBackStore();
39
+ const dataStore = useDataStore();
40
+ const hybridViewerStore = useHybridViewerStore();
41
+ const selectedDatasetId = ref<string>();
42
+ const sources = ref<AttributeSource[]>([]);
43
+ const selectedSourceIndex = ref<number>();
44
+ const attributes = ref<AttributeInfo[]>([]);
45
+ const attributeName = ref<string>();
46
+ const attributeItem = ref<number>(0);
47
+ const minimum = ref<number>();
48
+ const maximum = ref<number>();
49
+
50
+ const allItems = dataStore.refAllItems();
51
+ const availableDatasets = computed<{ title: string; value: string }[]>(() =>
52
+ allItems.value.map((item) => ({
53
+ title: item.name || item.id,
54
+ value: item.id,
55
+ })),
56
+ );
57
+ const availableSources = computed<{ title: string; value: number }[]>(() =>
58
+ sources.value.map((source, index) => ({ title: source.title, value: index })),
59
+ );
60
+ const currentAttribute = computed<AttributeInfo | undefined>(() =>
61
+ attributes.value.find((attribute) => attribute.attribute_name === attributeName.value),
62
+ );
63
+ const itemOptions = computed<{ title: string; value: number }[]>(() =>
64
+ Array.from({ length: currentAttribute.value?.nb_items ?? 0 }, (_, index) => ({
65
+ title: `Item ${index + 1}`,
66
+ value: index,
67
+ })),
68
+ );
69
+
70
+ function attributeSchema(kind: string, isModel: boolean): JsonRpcSchema {
71
+ const prefix = isModel ? "model_component_" : "";
72
+ return back_schemas.opengeodeweb_back[
73
+ `${prefix}${kind}_attribute_names` as keyof typeof back_schemas.opengeodeweb_back
74
+ ] as JsonRpcSchema;
75
+ }
76
+
77
+ function attributeSource(
78
+ kind: string,
79
+ title: string,
80
+ params: Record<string, unknown>,
81
+ isModel: boolean,
82
+ ): AttributeSource {
83
+ return {
84
+ title: `${title}${kind} attribute`,
85
+ schema: attributeSchema(kind, isModel),
86
+ params,
87
+ location: kind === "vertex" ? "point" : "cell",
88
+ };
89
+ }
90
+
91
+ async function fetchSources(id: string): Promise<AttributeSource[]> {
92
+ const item = await dataStore.item(id);
93
+ if (item.viewer_type !== "model") {
94
+ return ["vertex", MESH_ELEMENT_KINDS[item.geode_object_type]]
95
+ .filter((kind): kind is string => kind !== undefined)
96
+ .map((kind) => attributeSource(kind, "", { id }, false));
97
+ }
98
+ const modelSources = await Promise.all(
99
+ Object.entries(MODEL_COMPONENT_KINDS).map(async ([type, kinds]) => {
100
+ const component_ids = await dataStore.getMeshComponentGeodeIds(id, type);
101
+ if (component_ids.length === 0) {
102
+ return [];
103
+ }
104
+ return kinds.map((kind) => attributeSource(kind, `${type} `, { id, component_ids }, true));
105
+ }),
106
+ );
107
+ return modelSources.flat();
108
+ }
109
+
110
+ async function applyThreshold(): Promise<void> {
111
+ const source = sources.value[selectedSourceIndex.value ?? -1];
112
+ if (
113
+ !selectedDatasetId.value ||
114
+ !source ||
115
+ !attributeName.value ||
116
+ minimum.value === undefined ||
117
+ maximum.value === undefined ||
118
+ minimum.value > maximum.value
119
+ ) {
120
+ return;
121
+ }
122
+ await hybridViewerStore.setThreshold([selectedDatasetId.value], {
123
+ name: attributeName.value,
124
+ location: source.location,
125
+ item: attributeItem.value,
126
+ minimum: minimum.value,
127
+ maximum: maximum.value,
128
+ });
129
+ }
130
+
131
+ const debouncedApply = useDebounceFn(() => applyThreshold(), DEBOUNCE_DELAY);
132
+
133
+ function resetRange(): void {
134
+ const { min, max } = getAttributeRange(
135
+ currentAttribute.value as Parameters<typeof getAttributeRange>[0],
136
+ attributeItem.value,
137
+ );
138
+ minimum.value = min;
139
+ maximum.value = max;
140
+ }
141
+
142
+ async function removeThreshold(): Promise<void> {
143
+ attributeName.value = undefined;
144
+ if (selectedDatasetId.value) {
145
+ await hybridViewerStore.setThreshold([selectedDatasetId.value]);
146
+ }
147
+ }
148
+
149
+ watch(selectedDatasetId, async (id, previousId) => {
150
+ if (previousId) {
151
+ await hybridViewerStore.setThreshold([previousId]);
152
+ }
153
+ selectedSourceIndex.value = undefined;
154
+ sources.value = id ? await fetchSources(id) : [];
155
+ });
156
+
157
+ watch(selectedSourceIndex, async (index) => {
158
+ attributeName.value = undefined;
159
+ attributes.value = [];
160
+ const source = sources.value[index ?? -1];
161
+ if (!source) {
162
+ return;
163
+ }
164
+ const response = (await backStore.request({
165
+ schema: source.schema,
166
+ params: source.params,
167
+ })) as { attributes: AttributeInfo[] };
168
+ attributes.value = response.attributes;
169
+ });
170
+
171
+ watch(attributeName, () => {
172
+ attributeItem.value = 0;
173
+ resetRange();
174
+ });
175
+
176
+ watch(attributeItem, () => {
177
+ resetRange();
178
+ });
179
+
180
+ watch([minimum, maximum], () => {
181
+ debouncedApply();
182
+ });
183
+
184
+ watch(show, (visible) => {
185
+ if (visible) {
186
+ applyThreshold();
187
+ }
188
+ });
189
+ </script>
190
+
191
+ <template>
192
+ <ToolPanel
193
+ v-model="show"
194
+ data-testid="thresholdFilterPanel"
195
+ title="Threshold Filter"
196
+ :width="340"
197
+ :click-outside="false"
198
+ :escapeFunction="escapeFunction"
199
+ >
200
+ <v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
201
+ <v-select
202
+ v-model="selectedDatasetId"
203
+ data-testid="thresholdDatasetSelect"
204
+ :items="availableDatasets"
205
+ label="Select a dataset"
206
+ variant="outlined"
207
+ density="compact"
208
+ hide-details
209
+ class="mb-3 text-caption"
210
+ />
211
+
212
+ <v-select
213
+ v-if="selectedDatasetId"
214
+ v-model="selectedSourceIndex"
215
+ data-testid="thresholdAttributeTypeSelect"
216
+ :items="availableSources"
217
+ label="Select an attribute type"
218
+ variant="outlined"
219
+ density="compact"
220
+ hide-details
221
+ class="mb-3 text-caption"
222
+ />
223
+
224
+ <v-select
225
+ v-if="selectedSourceIndex !== undefined"
226
+ v-model="attributeName"
227
+ data-testid="thresholdAttributeSelect"
228
+ :items="attributes.map((attribute) => attribute.attribute_name)"
229
+ label="Select an attribute"
230
+ variant="outlined"
231
+ density="compact"
232
+ hide-details
233
+ class="mb-3 text-caption"
234
+ />
235
+
236
+ <v-select
237
+ v-if="currentAttribute && currentAttribute.nb_items > 1"
238
+ v-model="attributeItem"
239
+ data-testid="thresholdItemSelect"
240
+ :items="itemOptions"
241
+ label="Select an item"
242
+ variant="outlined"
243
+ density="compact"
244
+ hide-details
245
+ class="mb-3 text-caption"
246
+ />
247
+
248
+ <template v-if="attributeName">
249
+ <v-divider class="my-3" />
250
+ <span class="text-caption font-weight-bold">Range</span>
251
+ <ViewerOptionsAttributeRangeSelector
252
+ v-model:minimum="minimum"
253
+ v-model:maximum="maximum"
254
+ class="mt-2"
255
+ @reset="resetRange"
256
+ />
257
+ </template>
258
+ </v-card-text>
259
+
260
+ <template #actions>
261
+ <v-card-actions class="justify-space-between px-3 pb-3 pt-0">
262
+ <v-btn
263
+ data-testid="removeThresholdButton"
264
+ variant="text"
265
+ size="small"
266
+ color="error"
267
+ class="text-caption text-none"
268
+ @click="removeThreshold"
269
+ >
270
+ Remove Threshold
271
+ </v-btn>
272
+ <v-btn
273
+ data-testid="resetThresholdButton"
274
+ variant="tonal"
275
+ size="small"
276
+ color="secondary"
277
+ class="text-caption text-none"
278
+ @click="resetRange"
279
+ >
280
+ Reset
281
+ </v-btn>
282
+ </v-card-actions>
283
+ </template>
284
+ </ToolPanel>
285
+ </template>
286
+
287
+ <style scoped>
288
+ .max-panel-height {
289
+ max-height: 520px;
290
+ }
291
+ </style>
@@ -7,6 +7,7 @@ import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue";
7
7
  import Ruler from "@ogw_front/components/Ruler.vue";
8
8
  import Screenshot from "@ogw_front/components/Screenshot.vue";
9
9
  import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue";
10
+ import ThresholdFilter from "@ogw_front/components/ThresholdFilter.vue";
10
11
  import ZScaling from "@ogw_front/components/ZScaling.vue";
11
12
  import { onKeyStroke } from "@vueuse/core";
12
13
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
@@ -23,6 +24,7 @@ const showCameraOrientation = ref<boolean>(false);
23
24
  const showZScaling = ref<boolean>(false);
24
25
  const showClippingPlanes = ref<boolean>(false);
25
26
  const showShrinkFilter = ref<boolean>(false);
27
+ const showThresholdFilter = ref<boolean>(false);
26
28
  const showRuler = ref<boolean>(false);
27
29
  const gridScale = ref<boolean>(false);
28
30
  const zScale = ref<number>(hybridViewerStore.zScale);
@@ -73,6 +75,7 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
73
75
  showZScaling,
74
76
  showClippingPlanes,
75
77
  showShrinkFilter,
78
+ showThresholdFilter,
76
79
  showRuler,
77
80
  is_picking,
78
81
  is_zoom_box_active,
@@ -231,6 +234,15 @@ const camera_options = computed<CameraOptionAction[]>(() => [
231
234
  toggleTool(showShrinkFilter);
232
235
  },
233
236
  },
237
+ {
238
+ testId: "thresholdFilterButton",
239
+ tooltip: "Threshold Filter",
240
+ icon: "mdi-filter-variant",
241
+ color: showThresholdFilter.value ? "primary" : undefined,
242
+ action: (): void => {
243
+ toggleTool(showThresholdFilter);
244
+ },
245
+ },
234
246
  {
235
247
  testId: "rulerButton",
236
248
  tooltip: "Ruler",
@@ -320,6 +332,10 @@ const camera_options = computed<CameraOptionAction[]>(() => [
320
332
  v-model:show="showShrinkFilter"
321
333
  :escapeFunction="() => (showShrinkFilter = false)"
322
334
  />
335
+ <ThresholdFilter
336
+ v-model:show="showThresholdFilter"
337
+ :escapeFunction="() => (showThresholdFilter = false)"
338
+ />
323
339
  <Ruler v-model:show="showRuler" :escapeFunction="() => (showRuler = false)" />
324
340
  </template>
325
341
 
@@ -29,12 +29,16 @@ export const useBackStore = defineStore("back", {
29
29
  return getRestApiPort(this.default_local_port);
30
30
  },
31
31
  base_url(): string {
32
- const infraStore = useInfraStore();
33
- let back_url = `${this.protocol}://${infraStore.domain_name}:${this.port}`;
34
- if (isCloudMode()) {
35
- back_url += `/geode`;
36
- }
37
- return back_url;
32
+ return this.base_url_for(useInfraStore().domain_name);
33
+ },
34
+ base_url_for(): (domain_name: string) => string {
35
+ return (domain_name: string) => {
36
+ let back_url = `${this.protocol}://${domain_name}:${this.port}`;
37
+ if (isCloudMode()) {
38
+ back_url += `/geode`;
39
+ }
40
+ return back_url;
41
+ };
38
42
  },
39
43
  is_busy(): boolean {
40
44
  return this.request_counter > 0;
@@ -1,35 +1,41 @@
1
+ import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
2
+ import type { RunCloudResponse } from "@geode/cloud-api/types";
1
3
  import { Status } from "@ogw_front/utils/status";
4
+ import { api_fetch } from "@ogw_internal/utils/api_fetch";
5
+ import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
6
+ import cloud_api_schemas from "@geode/cloud-api/cloud_api_schemas.json";
2
7
  import { setAppBaseUrl } from "@ogw_shared/scripts";
3
8
  import { useAPIStore } from "@ogw_front/stores/api";
9
+ import { useFeedbackStore } from "@ogw_front/stores/feedback";
4
10
 
5
- const run_cloud_schema = {
6
- $id: "/cloud/run",
7
- methods: ["POST"],
8
- type: "object",
9
- properties: {
10
- email: { type: "string" },
11
- project: { type: "string" },
12
- branch: { type: "string" },
13
- },
14
- required: ["email", "project", "branch"],
15
- additionalProperties: false,
16
- };
11
+ const run_cloud_schema = cloud_api_schemas.cloud_api.cloud.run;
17
12
 
18
13
  export const useCloudStore = defineStore("cloud", {
19
14
  state: () => ({
20
15
  status: Status.NOT_CONNECTED,
21
16
  }),
22
17
  actions: {
18
+ // Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
19
+ // The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
20
+ async start(email: string) {
21
+ const existing_host = getCloudUrlParam();
22
+ if (existing_host === undefined) {
23
+ await this.launch(email);
24
+ return;
25
+ }
26
+ try {
27
+ await this.connect_existing(existing_host);
28
+ } finally {
29
+ clearCloudUrlParam();
30
+ }
31
+ },
23
32
  async launch(email: string) {
24
33
  this.status = Status.CONNECTING;
25
34
  const { PROJECT, BRANCH } = useRuntimeConfig().public;
26
35
  const params = { email, project: PROJECT, branch: BRANCH };
27
- const { useAppStore } = await import("./app");
28
- const { useFeedbackStore } = await import("./feedback");
29
- const appStore = useAppStore();
30
36
  const feedbackStore = useFeedbackStore();
31
37
  const APIStore = useAPIStore();
32
- const result = await APIStore.request(
38
+ const result = await APIStore.request<RunCloudResponse>(
33
39
  { schema: run_cloud_schema, params },
34
40
  {
35
41
  request_error_function: () => {
@@ -45,18 +51,7 @@ export const useCloudStore = defineStore("cloud", {
45
51
  ) {
46
52
  return;
47
53
  }
48
- const { url } = response;
49
- feedbackStore.$patch({ server_error: false });
50
- this.status = Status.CONNECTED;
51
- const { useInfraStore } = await import("./infra");
52
- const infraStore = useInfraStore();
53
- infraStore.$patch({
54
- domain_name: url,
55
- });
56
- setAppBaseUrl(appStore.base_url).catch(() => undefined);
57
- appStore.$patch({
58
- projectFolderPath: "/project",
59
- });
54
+ await this.on_connected(response.url);
60
55
  },
61
56
  response_error_function: () => {
62
57
  feedbackStore.$patch({ server_error: true });
@@ -66,6 +61,46 @@ export const useCloudStore = defineStore("cloud", {
66
61
  );
67
62
  return result;
68
63
  },
64
+ async connect_existing(host: string) {
65
+ this.status = Status.CONNECTING;
66
+ const { useBackStore } = await import("./back");
67
+ const backStore = useBackStore();
68
+ // Probe the candidate host directly so domain_name is only updated once the service answers.
69
+ try {
70
+ await api_fetch(
71
+ {
72
+ $id: backStore.$id,
73
+ base_url: backStore.base_url_for(host),
74
+ start_request: () => {
75
+ backStore.start_request();
76
+ },
77
+ stop_request: () => {
78
+ backStore.stop_request();
79
+ },
80
+ },
81
+ { schema: back_schemas.opengeodeweb_back.ping },
82
+ );
83
+ } catch (error) {
84
+ useFeedbackStore().$patch({ server_error: true });
85
+ this.status = Status.NOT_CONNECTED;
86
+ throw error;
87
+ }
88
+ await this.on_connected(host);
89
+ },
90
+ async on_connected(url: string) {
91
+ const { useAppStore } = await import("./app");
92
+ const { useInfraStore } = await import("./infra");
93
+ const appStore = useAppStore();
94
+ useFeedbackStore().$patch({ server_error: false });
95
+ this.status = Status.CONNECTED;
96
+ useInfraStore().$patch({
97
+ domain_name: url,
98
+ });
99
+ appStore.$patch({
100
+ projectFolderPath: "/project",
101
+ });
102
+ await setAppBaseUrl(appStore.base_url);
103
+ },
69
104
  connect() {
70
105
  this.status = Status.CONNECTED;
71
106
  },
@@ -55,7 +55,13 @@ export const useInfraStore = defineStore("infra", {
55
55
  this.status = Status.CREATING;
56
56
  if (this.app_mode === appMode.CLOUD) {
57
57
  const cloudStore = useCloudStore();
58
- await cloudStore.launch(email ?? "");
58
+ try {
59
+ await cloudStore.start(email ?? "");
60
+ } catch (error) {
61
+ // Back to the "Load the app" button so the user is not stuck on the loading screen.
62
+ this.status = Status.NOT_CREATED;
63
+ throw error;
64
+ }
59
65
  } else {
60
66
  const { useAppStore } = await import("@ogw_front/stores/app");
61
67
  const appStore = useAppStore();
@@ -10,7 +10,7 @@ interface AttributeRange {
10
10
  max: number;
11
11
  }
12
12
 
13
- export function getAttributeRange(
13
+ function getAttributeRange(
14
14
  currentAttribute: AttributeRangeSource | undefined | null,
15
15
  compIndex = 0,
16
16
  ): AttributeRange {
@@ -38,3 +38,28 @@ export function getAttributeRange(
38
38
 
39
39
  return { min, max };
40
40
  }
41
+
42
+ const MESH_ELEMENT_KINDS: Record<string, string> = {
43
+ EdgedCurve2D: "edge",
44
+ EdgedCurve3D: "edge",
45
+ LightRegularGrid2D: "cell",
46
+ LightRegularGrid3D: "cell",
47
+ RegularGrid2D: "cell",
48
+ RegularGrid3D: "cell",
49
+ PolygonalSurface2D: "polygon",
50
+ PolygonalSurface3D: "polygon",
51
+ TriangulatedSurface2D: "polygon",
52
+ TriangulatedSurface3D: "polygon",
53
+ HybridSolid3D: "polyhedron",
54
+ PolyhedralSolid3D: "polyhedron",
55
+ TetrahedralSolid3D: "polyhedron",
56
+ };
57
+
58
+ const MODEL_COMPONENT_KINDS: Record<string, string[]> = {
59
+ Corner: ["vertex"],
60
+ Line: ["vertex", "edge"],
61
+ Surface: ["vertex", "polygon"],
62
+ Block: ["vertex", "polyhedron"],
63
+ };
64
+
65
+ export { MESH_ELEMENT_KINDS, MODEL_COMPONENT_KINDS, getAttributeRange };
@@ -0,0 +1,35 @@
1
+ const CLOUD_URL_PARAM = "cloud_url";
2
+ const CLOUD_RUN_HOST_SUFFIX = ".run.app";
3
+
4
+ // Hostname of an already running Cloud Run service passed as `?cloud_url=`, restricted to Cloud Run hosts so a crafted link cannot point the app at an arbitrary server.
5
+ function getCloudUrlParam(): string | undefined {
6
+ const value = new URLSearchParams(globalThis.location.search).get(CLOUD_URL_PARAM);
7
+ if (value === null || value === "") {
8
+ return undefined;
9
+ }
10
+ const url_string = value.includes("://") ? value : `https://${value}`;
11
+ let hostname = "";
12
+ try {
13
+ ({ hostname } = new URL(url_string));
14
+ } catch {
15
+ console.warn(`[Cloud] Invalid ${CLOUD_URL_PARAM}:`, value);
16
+ return undefined;
17
+ }
18
+ if (!hostname.endsWith(CLOUD_RUN_HOST_SUFFIX)) {
19
+ console.warn(`[Cloud] Ignoring ${CLOUD_URL_PARAM}, not a Cloud Run host:`, hostname);
20
+ return undefined;
21
+ }
22
+ return hostname;
23
+ }
24
+
25
+ // Drops `?cloud_url=` from the address bar, keeping the router's history state, so a later "Load the app" launches a new service.
26
+ function clearCloudUrlParam(): void {
27
+ const url = new URL(globalThis.location.href);
28
+ if (!url.searchParams.has(CLOUD_URL_PARAM)) {
29
+ return;
30
+ }
31
+ url.searchParams.delete(CLOUD_URL_PARAM);
32
+ globalThis.history.replaceState(globalThis.history.state, "", url);
33
+ }
34
+
35
+ export { clearCloudUrlParam, getCloudUrlParam };
@@ -30,13 +30,37 @@ async function setShrink(ids: string[], shrink_factor: number): Promise<void> {
30
30
  });
31
31
  await remoteRender();
32
32
  }
33
+ interface ThresholdAttribute {
34
+ name: string;
35
+ location: "point" | "cell";
36
+ item: number;
37
+ minimum: number;
38
+ maximum: number;
39
+ }
40
+ async function setThreshold(ids: string[], attribute?: ThresholdAttribute): Promise<void> {
41
+ const viewerStore = useViewerStore();
42
+ const { remoteRender } = useHybridViewerCore();
43
+ const schema = viewer_schemas.opengeodeweb_viewer.viewer.threshold;
44
+ const params = {
45
+ ids,
46
+ attribute,
47
+ };
48
+ await viewerStore.request({
49
+ schema,
50
+ params,
51
+ });
52
+ await remoteRender();
53
+ }
33
54
  function useHybridViewerFilters(): {
34
55
  setClippingPlanes: (ids: string[], planes: unknown) => Promise<void>;
35
56
  setShrink: (ids: string[], shrink_factor: number) => Promise<void>;
57
+ setThreshold: (ids: string[], attribute?: ThresholdAttribute) => Promise<void>;
36
58
  } {
37
59
  return {
38
60
  setClippingPlanes,
39
61
  setShrink,
62
+ setThreshold,
40
63
  };
41
64
  }
42
65
  export { useHybridViewerFilters };
66
+ export type { ThresholdAttribute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.35.0-rc.3",
3
+ "version": "10.35.0-rc.5",
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": {
@@ -48,20 +48,22 @@
48
48
  },
49
49
  "scripts": {
50
50
  "lint": "oxlint --fix .",
51
+ "prepare:types": "cross-env MODE=BROWSER nuxi prepare",
51
52
  "typecheck": "nuxt typecheck && tsc -p tsconfig.tests.json --noEmit",
52
53
  "build:back": "npx opengeodeweb-microservice-pyinstaller ../OpenGeodeWeb-Back",
53
54
  "build:viewer": "npx opengeodeweb-microservice-pyinstaller ../OpenGeodeWeb-Viewer",
54
55
  "build:microservices": "concurrently \"npm run build:back\" \"npm run build:viewer\"",
55
56
  "json": "npx opengeodeweb-microservice-generate --startDir server/api --key route --separator / --prefix api",
56
57
  "test": "npm run test:unit",
57
- "tests": "nuxt prepare && vitest --config ./tests/vitest.config.ts",
58
+ "tests": "npm run prepare:types && vitest --config ./tests/vitest.config.ts",
58
59
  "test:unit": "npm run tests -- --project unit",
59
60
  "test:integration": "npm run tests -- --project integration",
60
61
  "geode_objects": "tsx scripts/generate_geode_objects.ts && prettier ./assets/geode_objects.ts --write",
61
- "build": "nuxt prepare && rolldown -c rolldown.config.ts",
62
+ "build": "npm run prepare:types && rolldown -c rolldown.config.ts",
62
63
  "prepublishOnly": "npm run build"
63
64
  },
64
65
  "dependencies": {
66
+ "@geode/cloud-api": "next",
65
67
  "@geode/opengeodeweb-back": "next",
66
68
  "@geode/opengeodeweb-viewer": "next",
67
69
  "@kitware/vtk.js": "33.3.0",
@@ -106,6 +108,7 @@
106
108
  "@types/lodash": "4.17.25",
107
109
  "@vitejs/plugin-vue": "6.0.8",
108
110
  "@vue/test-utils": "2.4.11",
111
+ "cross-env": "10.0.0",
109
112
  "happy-dom": "20.0.11",
110
113
  "msw": "2.11.1",
111
114
  "oxlint": "1.82.0",