@geode/opengeodeweb-front 10.35.0-rc.2 → 10.35.0-rc.4

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 lang="ts">
2
2
  import OverlappingObjectsPicker from "@ogw_front/components/Viewer/OverlappingObjectsPicker.vue";
3
+ import ToolActiveChip from "@ogw_front/components/Viewer/ToolActiveChip.vue";
3
4
  import ViewerContextMenu from "@ogw_front/components/Viewer/ContextMenu/ContextMenu.vue";
4
5
  import ViewerObjectTreeLayout from "@ogw_front/components/Viewer/ObjectTree/Layout.vue";
5
6
  import { getCurrentInstance } from "vue";
@@ -23,6 +24,11 @@ const menuStore = useMenuStore();
23
24
  const viewerStore = useViewerStore();
24
25
  const hybridViewerStore = useHybridViewerStore();
25
26
 
27
+ const hoverHighlightLabel = computed(
28
+ () =>
29
+ `Highlight active (${hybridViewerStore.hover_highlight_field_type === "CELL" ? "Cells" : "Points"})`,
30
+ );
31
+
26
32
  function stopHoverHighlight(): void {
27
33
  hybridViewerStore.is_hover_highlight = false;
28
34
  hybridViewerStore.clearHoverHighlight();
@@ -36,6 +42,9 @@ onKeyStroke("Escape", (event) => {
36
42
  } else if (hybridViewerStore.is_picking) {
37
43
  hybridViewerStore.is_picking = false;
38
44
  consumed = true;
45
+ } else if (hybridViewerStore.is_zoom_box_active) {
46
+ hybridViewerStore.is_zoom_box_active = false;
47
+ consumed = true;
39
48
  } else if (hybridViewerStore.is_ruler_active) {
40
49
  hybridViewerStore.deactivateRuler();
41
50
  consumed = true;
@@ -101,101 +110,40 @@ defineExpose({ get_viewer_id });
101
110
  @update:display-intermediate="handleIntermediateMenuUpdate"
102
111
  />
103
112
 
104
- <v-fade-transition>
105
- <div
113
+ <v-fade-transition group>
114
+ <ToolActiveChip
106
115
  v-if="viewerStore.picking_mode"
107
- class="picking-message-container d-flex justify-center w-100 pa-4"
108
- >
109
- <v-chip
110
- color="secondary"
111
- elevation="8"
112
- size="large"
113
- variant="flat"
114
- prepend-icon="mdi-crosshairs-gps"
115
- class="pick-pulse"
116
- data-testid="pickingActiveChip"
117
- style="pointer-events: auto"
118
- @click="viewerStore.toggle_picking_mode(false)"
119
- >
120
- Picking active — click in the viewer &middot; Esc to stop
121
- <v-divider vertical class="mx-2 my-1" opacity="0.3" />
122
- <v-icon icon="mdi-close" size="small" />
123
- </v-chip>
124
- </div>
125
- </v-fade-transition>
126
-
127
- <v-fade-transition>
128
- <div
116
+ key="picking"
117
+ data-testid="pickingActiveChip"
118
+ label="Picking active — click in the viewer"
119
+ color="secondary"
120
+ icon="mdi-crosshairs-gps"
121
+ @close="viewerStore.toggle_picking_mode(false)"
122
+ />
123
+ <ToolActiveChip
129
124
  v-if="hybridViewerStore.is_hover_highlight"
130
- class="picking-message-container d-flex justify-center w-100 pa-4"
131
- >
132
- <v-chip
133
- data-testid="hoverHighlightChip"
134
- color="primary"
135
- elevation="8"
136
- size="large"
137
- variant="flat"
138
- class="pick-pulse"
139
- style="pointer-events: auto"
140
- @click="stopHoverHighlight"
141
- >
142
- Highlight active ({{
143
- hybridViewerStore.hover_highlight_field_type === "CELL" ? "Cells" : "Points"
144
- }}) &middot; Esc to stop
145
- <v-divider vertical class="mx-2 my-1" opacity="0.3" />
146
- <v-icon icon="mdi-close" size="small" />
147
- </v-chip>
148
- </div>
149
- </v-fade-transition>
150
-
151
- <v-fade-transition>
152
- <div
125
+ key="hoverHighlight"
126
+ data-testid="hoverHighlightChip"
127
+ :label="hoverHighlightLabel"
128
+ color="primary"
129
+ @close="stopHoverHighlight"
130
+ />
131
+ <ToolActiveChip
153
132
  v-if="hybridViewerStore.is_ruler_active"
154
- class="picking-message-container d-flex justify-center w-100 pa-4"
155
- >
156
- <v-chip
157
- data-testid="rulerActiveChip"
158
- color="secondary"
159
- elevation="8"
160
- size="large"
161
- variant="flat"
162
- class="pick-pulse"
163
- style="pointer-events: auto"
164
- prepend-icon="mdi-ruler"
165
- @click="hybridViewerStore.clearRuler()"
166
- >
167
- Ruler &mdash; click to set point
168
- {{ hybridViewerStore.ruler_awaiting_point }}
169
- &middot; Esc to stop
170
- <v-divider vertical class="mx-2 my-1" opacity="0.3" />
171
- <v-icon icon="mdi-close" size="small" />
172
- </v-chip>
173
- </div>
133
+ key="ruler"
134
+ data-testid="rulerActiveChip"
135
+ :label="`Ruler — click to set point ${hybridViewerStore.ruler_awaiting_point}`"
136
+ color="secondary"
137
+ icon="mdi-ruler"
138
+ @close="hybridViewerStore.clearRuler()"
139
+ />
140
+ <ToolActiveChip
141
+ v-if="hybridViewerStore.is_zoom_box_active"
142
+ key="zoomBox"
143
+ data-testid="zoomBoxActiveChip"
144
+ label="Zoom to box"
145
+ color="primary"
146
+ @close="hybridViewerStore.is_zoom_box_active = false"
147
+ />
174
148
  </v-fade-transition>
175
149
  </template>
176
-
177
- <style scoped>
178
- .picking-message-container {
179
- position: absolute;
180
- top: 20px;
181
- left: 0;
182
- pointer-events: none;
183
- z-index: 3;
184
- }
185
-
186
- @keyframes pulse-ring {
187
- 0% {
188
- box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0.7);
189
- }
190
- 70% {
191
- box-shadow: 0 0 0 10px rgba(var(--v-theme-secondary), 0);
192
- }
193
- 100% {
194
- box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0);
195
- }
196
- }
197
-
198
- .pick-pulse {
199
- animation: pulse-ring 1.5s ease-out infinite;
200
- }
201
- </style>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { type ZoomBox, viewportShapedBox } from "@ogw_internal/stores/hybrid_viewer/zoom_box";
3
+ import type { Position } from "@vueuse/core";
4
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
5
+
6
+ const MIN_ZOOM_BOX_PIXELS = 5;
7
+
8
+ const hybridViewerStore = useHybridViewerStore();
9
+ const overlay = useTemplateRef<HTMLDivElement>("overlay");
10
+ const { left, top, width, height } = useElementBounding(overlay);
11
+ const viewport = computed(() => ({ width: width.value, height: height.value }));
12
+
13
+ function draggedBox(start: Position, end: Position): ZoomBox {
14
+ return viewportShapedBox(
15
+ { x: start.x - left.value, y: start.y - top.value },
16
+ { x: end.x - left.value, y: end.y - top.value },
17
+ viewport.value,
18
+ );
19
+ }
20
+
21
+ const { isSwiping, posStart, posEnd } = usePointerSwipe(overlay, {
22
+ threshold: MIN_ZOOM_BOX_PIXELS,
23
+ disableTextSelect: true,
24
+ onSwipeEnd: async () => {
25
+ try {
26
+ await hybridViewerStore.zoomToBox(draggedBox(posStart, posEnd), viewport.value);
27
+ } catch {
28
+ // The viewer store already reported the failed pick request to the user.
29
+ }
30
+ },
31
+ });
32
+
33
+ const rectangle_style = computed(() => {
34
+ const { start, end } = draggedBox(posStart, posEnd);
35
+ return {
36
+ left: `${Math.min(start.x, end.x)}px`,
37
+ top: `${Math.min(start.y, end.y)}px`,
38
+ width: `${Math.abs(end.x - start.x)}px`,
39
+ height: `${Math.abs(end.y - start.y)}px`,
40
+ };
41
+ });
42
+ </script>
43
+
44
+ <template>
45
+ <div ref="overlay" data-testid="zoomBoxOverlay" class="zoom-box-overlay" @contextmenu.prevent>
46
+ <div v-if="isSwiping" class="zoom-box-rectangle" :style="rectangle_style" />
47
+ </div>
48
+ </template>
49
+
50
+ <style scoped>
51
+ .zoom-box-overlay {
52
+ position: absolute;
53
+ inset: 0;
54
+ z-index: 1;
55
+ cursor: crosshair;
56
+ }
57
+
58
+ .zoom-box-rectangle {
59
+ position: absolute;
60
+ border: 2px dashed rgb(var(--v-theme-primary));
61
+ background-color: rgba(var(--v-theme-primary), 0.15);
62
+ pointer-events: none;
63
+ }
64
+ </style>
@@ -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,6 +1,10 @@
1
+ import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
1
2
  import { Status } from "@ogw_front/utils/status";
3
+ import { api_fetch } from "@ogw_internal/utils/api_fetch";
4
+ import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
2
5
  import { setAppBaseUrl } from "@ogw_shared/scripts";
3
6
  import { useAPIStore } from "@ogw_front/stores/api";
7
+ import { useFeedbackStore } from "@ogw_front/stores/feedback";
4
8
 
5
9
  const run_cloud_schema = {
6
10
  $id: "/cloud/run",
@@ -20,13 +24,24 @@ export const useCloudStore = defineStore("cloud", {
20
24
  status: Status.NOT_CONNECTED,
21
25
  }),
22
26
  actions: {
27
+ // Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
28
+ // The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
29
+ async start(email: string) {
30
+ const existing_host = getCloudUrlParam();
31
+ if (existing_host === undefined) {
32
+ await this.launch(email);
33
+ return;
34
+ }
35
+ try {
36
+ await this.connect_existing(existing_host);
37
+ } finally {
38
+ clearCloudUrlParam();
39
+ }
40
+ },
23
41
  async launch(email: string) {
24
42
  this.status = Status.CONNECTING;
25
43
  const { PROJECT, BRANCH } = useRuntimeConfig().public;
26
44
  const params = { email, project: PROJECT, branch: BRANCH };
27
- const { useAppStore } = await import("./app");
28
- const { useFeedbackStore } = await import("./feedback");
29
- const appStore = useAppStore();
30
45
  const feedbackStore = useFeedbackStore();
31
46
  const APIStore = useAPIStore();
32
47
  const result = await APIStore.request(
@@ -45,18 +60,7 @@ export const useCloudStore = defineStore("cloud", {
45
60
  ) {
46
61
  return;
47
62
  }
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
- });
63
+ await this.on_connected(response.url);
60
64
  },
61
65
  response_error_function: () => {
62
66
  feedbackStore.$patch({ server_error: true });
@@ -66,6 +70,46 @@ export const useCloudStore = defineStore("cloud", {
66
70
  );
67
71
  return result;
68
72
  },
73
+ async connect_existing(host: string) {
74
+ this.status = Status.CONNECTING;
75
+ const { useBackStore } = await import("./back");
76
+ const backStore = useBackStore();
77
+ // Probe the candidate host directly so domain_name is only updated once the service answers.
78
+ try {
79
+ await api_fetch(
80
+ {
81
+ $id: backStore.$id,
82
+ base_url: backStore.base_url_for(host),
83
+ start_request: () => {
84
+ backStore.start_request();
85
+ },
86
+ stop_request: () => {
87
+ backStore.stop_request();
88
+ },
89
+ },
90
+ { schema: back_schemas.opengeodeweb_back.ping },
91
+ );
92
+ } catch (error) {
93
+ useFeedbackStore().$patch({ server_error: true });
94
+ this.status = Status.NOT_CONNECTED;
95
+ throw error;
96
+ }
97
+ await this.on_connected(host);
98
+ },
99
+ async on_connected(url: string) {
100
+ const { useAppStore } = await import("./app");
101
+ const { useInfraStore } = await import("./infra");
102
+ const appStore = useAppStore();
103
+ useFeedbackStore().$patch({ server_error: false });
104
+ this.status = Status.CONNECTED;
105
+ useInfraStore().$patch({
106
+ domain_name: url,
107
+ });
108
+ appStore.$patch({
109
+ projectFolderPath: "/project",
110
+ });
111
+ await setAppBaseUrl(appStore.base_url);
112
+ },
69
113
  connect() {
70
114
  this.status = Status.CONNECTED;
71
115
  },
@@ -15,6 +15,7 @@ import { useHybridViewerFilters } from "@ogw_internal/stores/hybrid_viewer/filte
15
15
  import { useHybridViewerHighlight } from "@ogw_internal/stores/hybrid_viewer/highlight";
16
16
  import { useHybridViewerRuler } from "@ogw_internal/stores/hybrid_viewer/ruler";
17
17
  import { useHybridViewerScene } from "@ogw_internal/stores/hybrid_viewer/scene";
18
+ import { useHybridViewerZoomBox } from "@ogw_internal/stores/hybrid_viewer/zoom_box";
18
19
  import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
19
20
 
20
21
  import { Status } from "@ogw_front/utils/status";
@@ -34,6 +35,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
34
35
  const rulerStore = useHybridViewerRuler();
35
36
  const cameraStore = useHybridViewerCamera();
36
37
  const viewportStore = useHybridViewerViewport();
38
+ const zoomBoxStore = useHybridViewerZoomBox();
37
39
 
38
40
  const is_cursor_crosshair = computed(() => is_picking.value || rulerStore.is_ruler_active.value);
39
41
 
@@ -132,5 +134,6 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
132
134
  ...highlightStore,
133
135
  ...rulerStore,
134
136
  ...cameraStore,
137
+ ...zoomBoxStore,
135
138
  };
136
139
  });
@@ -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();
@@ -1,6 +1,7 @@
1
1
  // Third party imports
2
2
  // oxlint-disable-next-line no-unassigned-import
3
3
  import "@kitware/vtk.js/Rendering/OpenGL/Profiles/Geometry";
4
+ import type { Vector3 } from "@kitware/vtk.js/types";
4
5
  import { connectImageStream } from "@kitware/vtk.js/Rendering/Misc/RemoteView";
5
6
  import { initWebSocketClient } from "@ogw_internal/utils/ws_client";
6
7
  import opengeodeweb_front_schemas from "@geode/opengeodeweb-front/opengeodeweb_front_schemas.json" with { type: "json" };
@@ -106,27 +107,16 @@ export const useViewerStore = defineStore(
106
107
  );
107
108
  return result;
108
109
  }
109
- async function set_picked_point(x: number, y: number): Promise<void> {
110
+ async function pick_world_position(x: number, y: number): Promise<Vector3> {
110
111
  const schema = opengeodeweb_viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
111
- const params = {
112
- x: Math.round(x),
113
- y: Math.round(y),
114
- };
115
- const response = await request({
116
- schema,
117
- params,
118
- });
119
- const {
120
- x: world_x,
121
- y: world_y,
122
- z: world_z,
123
- // oxlint-disable-next-line no-unsafe-type-assertion -- response shape is defined by the get_point_position schema.
124
- } = response as { x: number; y: number; z: number };
125
- picked_point.value = {
126
- x: world_x,
127
- y: world_y,
128
- z: world_z,
129
- };
112
+ const response = await request({ schema, params: { x: Math.round(x), y: Math.round(y) } });
113
+ // oxlint-disable-next-line no-unsafe-type-assertion -- response shape is defined by the get_point_position schema.
114
+ const position = response as { x: number; y: number; z: number };
115
+ return [position.x, position.y, position.z];
116
+ }
117
+ async function set_picked_point(x: number, y: number): Promise<void> {
118
+ const [world_x, world_y, world_z] = await pick_world_position(x, y);
119
+ picked_point.value = { x: world_x, y: world_y, z: world_z };
130
120
  }
131
121
  async function ws_connect(): Promise<void> {
132
122
  if (status.value === Status.CONNECTED) {
@@ -219,6 +209,7 @@ export const useViewerStore = defineStore(
219
209
  is_busy,
220
210
  toggle_picking_mode,
221
211
  set_picked_point,
212
+ pick_world_position,
222
213
  ws_connect,
223
214
  start_request,
224
215
  stop_request,
@@ -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 };
@@ -182,11 +182,13 @@ const useHybridViewerCamera = createSharedComposable(() => {
182
182
  bumpMultiplier: 0,
183
183
  easeExponent: EASE_EXPONENT,
184
184
  onUpdate: () => {
185
+ renderer.resetCameraClippingRange();
185
186
  const renderWindow = requireRenderWindow(genericRenderWindow).getRenderWindow();
186
187
  renderWindow.render();
187
188
  },
188
189
  onEnd: () => {
189
190
  applyCameraOptions(camera, targetCameraOptions);
191
+ renderer.resetCameraClippingRange();
190
192
  const renderWindow = requireRenderWindow(genericRenderWindow).getRenderWindow();
191
193
  renderWindow.render();
192
194
  is_moving.value = false;
@@ -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 };
@@ -27,22 +27,24 @@ function useHybridViewerRuler(): {
27
27
  const ruler_point2 = ref<number[] | undefined>(undefined);
28
28
  const ruler_distance = ref<number | undefined>(undefined);
29
29
  const ruler_awaiting_point = ref(1);
30
- const ruler_previous_hover_state = ref<RulerHoverState>({ active: false, fieldType: "CELL" });
30
+ // Hover highlight state overridden by the snap, restored when snapping stops. Without snap the ruler leaves the hover highlight untouched.
31
+ let hover_state_before_snap: RulerHoverState | undefined = undefined;
31
32
 
32
33
  function updateRulerSnapHighlight(): void {
33
34
  const { is_hover_highlight, hover_highlight_field_type, clearHoverHighlight } =
34
35
  useHybridViewerHighlight();
35
36
 
36
37
  if (is_ruler_active.value && ruler_snap.value) {
37
- ruler_previous_hover_state.value = {
38
+ hover_state_before_snap = {
38
39
  active: is_hover_highlight.value,
39
40
  fieldType: hover_highlight_field_type.value,
40
41
  };
41
42
  is_hover_highlight.value = true;
42
43
  hover_highlight_field_type.value = "POINT";
43
- } else {
44
- is_hover_highlight.value = ruler_previous_hover_state.value.active;
45
- hover_highlight_field_type.value = ruler_previous_hover_state.value.fieldType;
44
+ } else if (hover_state_before_snap) {
45
+ is_hover_highlight.value = hover_state_before_snap.active;
46
+ hover_highlight_field_type.value = hover_state_before_snap.fieldType;
47
+ hover_state_before_snap = undefined;
46
48
  if (!is_hover_highlight.value) {
47
49
  clearHoverHighlight();
48
50
  }
@@ -87,15 +89,7 @@ function useHybridViewerRuler(): {
87
89
  };
88
90
  coords = response.attributes?.coordinates;
89
91
  } else {
90
- const schema = viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
91
- const params = { x: Math.round(x), y: Math.round(y) };
92
- // oxlint-disable-next-line no-unsafe-type-assertion -- response shape is defined by the get_point_position schema.
93
- const response = (await viewerStore.request({ schema, params })) as {
94
- x: number;
95
- y: number;
96
- z: number;
97
- };
98
- coords = [response.x, response.y, response.z];
92
+ coords = await viewerStore.pick_world_position(x, y);
99
93
  }
100
94
  if (!coords) {
101
95
  return;