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

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.
@@ -2,6 +2,7 @@
2
2
  import ColormapQuickPicker from "@ogw_front/components/Viewer/Options/ColormapQuickPicker.vue";
3
3
  import HybridViewerTooltip from "@ogw_front/components/HybridViewerTooltip.vue";
4
4
  import ViewToolbar from "@ogw_front/components/ViewToolbar.vue";
5
+ import ZoomBoxOverlay from "@ogw_front/components/ZoomBoxOverlay.vue";
5
6
 
6
7
  import { useDataStore } from "@ogw_front/stores/data";
7
8
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
@@ -102,6 +103,7 @@ async function handleClick(event: PointerEvent): Promise<void> {
102
103
  :y="quickColormap.y"
103
104
  :data-id="quickColormap.data_id"
104
105
  />
106
+ <ZoomBoxOverlay v-if="hybridViewerStore.is_zoom_box_active" />
105
107
  <ViewToolbar />
106
108
  <slot name="ui"></slot>
107
109
  <HybridViewerTooltip :container-width="elementWidth" :container-height="elementHeight" />
@@ -9,7 +9,6 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
9
9
 
10
10
  import ColormapQuickPicker from "@ogw_front/components/Viewer/Options/ColormapQuickPicker.vue";
11
11
  import ViewToolbar from "@ogw_front/components/ViewToolbar.vue";
12
- import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
13
12
  import { newInstance as vtkRemoteView } from "@kitware/vtk.js/Rendering/Misc/RemoteView";
14
13
 
15
14
  interface Props {
@@ -32,9 +31,6 @@ async function get_x_y(event: PointerEvent): Promise<void> {
32
31
  const { offsetX, offsetY, clientX, clientY } = event;
33
32
  if (viewerStore.picking_mode === true) {
34
33
  viewerStore.set_picked_point(offsetX, offsetY);
35
- const schema = viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
36
- const params = { x: offsetX, y: offsetY };
37
- viewerStore.request({ schema, params });
38
34
  } else {
39
35
  await pickColormap(offsetX, offsetY, clientX, clientY);
40
36
  }
@@ -15,6 +15,8 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
15
15
 
16
16
  const hybridViewerStore = useHybridViewerStore();
17
17
  const viewerStore = useViewerStore();
18
+ const { is_picking, is_zoom_box_active } = storeToRefs(hybridViewerStore);
19
+ const { picking_mode } = storeToRefs(viewerStore);
18
20
  const showScreenshot = ref<boolean>(false);
19
21
  const showCameraManager = ref<boolean>(false);
20
22
  const showCameraOrientation = ref<boolean>(false);
@@ -58,6 +60,11 @@ onKeyStroke("Escape", () => {
58
60
  }
59
61
  });
60
62
 
63
+ function stopHoverHighlight(): void {
64
+ hybridViewerStore.is_hover_highlight = false;
65
+ hybridViewerStore.clearHoverHighlight();
66
+ }
67
+
61
68
  function closeAllToolsExcept(toolRef: Ref<boolean>): void {
62
69
  const tools = [
63
70
  showCameraOrientation,
@@ -67,12 +74,18 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
67
74
  showClippingPlanes,
68
75
  showShrinkFilter,
69
76
  showRuler,
77
+ is_picking,
78
+ is_zoom_box_active,
79
+ picking_mode,
70
80
  ];
71
81
  for (const tool of tools) {
72
82
  if (tool !== toolRef) {
73
83
  tool.value = false;
74
84
  }
75
85
  }
86
+ if (toolRef !== showRuler) {
87
+ void nextTick(stopHoverHighlight);
88
+ }
76
89
  }
77
90
 
78
91
  function toggleTool(toolRef: Ref<boolean>): void {
@@ -80,6 +93,19 @@ function toggleTool(toolRef: Ref<boolean>): void {
80
93
  toolRef.value = !toolRef.value;
81
94
  }
82
95
 
96
+ function toggleHoverHighlight(fieldType: string): void {
97
+ const activate =
98
+ !hybridViewerStore.is_hover_highlight ||
99
+ hybridViewerStore.hover_highlight_field_type !== fieldType;
100
+ closeAllToolsExcept(showRuler);
101
+ if (activate) {
102
+ hybridViewerStore.is_hover_highlight = true;
103
+ hybridViewerStore.hover_highlight_field_type = fieldType;
104
+ } else {
105
+ stopHoverHighlight();
106
+ }
107
+ }
108
+
83
109
  const camera_options = computed<CameraOptionAction[]>(() => [
84
110
  {
85
111
  testId: "resetCameraButton",
@@ -95,7 +121,16 @@ const camera_options = computed<CameraOptionAction[]>(() => [
95
121
  icon: "mdi-crosshairs-question",
96
122
  color: hybridViewerStore.is_picking ? "primary" : undefined,
97
123
  action: (): void => {
98
- hybridViewerStore.is_picking = !hybridViewerStore.is_picking;
124
+ toggleTool(is_picking);
125
+ },
126
+ },
127
+ {
128
+ testId: "zoomToBoxButton",
129
+ tooltip: "Zoom to box",
130
+ icon: "mdi-magnify-scan",
131
+ color: hybridViewerStore.is_zoom_box_active ? "primary" : undefined,
132
+ action: (): void => {
133
+ toggleTool(is_zoom_box_active);
99
134
  },
100
135
  },
101
136
  {
@@ -103,28 +138,14 @@ const camera_options = computed<CameraOptionAction[]>(() => [
103
138
  tooltip: "Highlight on hover",
104
139
  icon: "mdi-cursor-default-click",
105
140
  color: hybridViewerStore.is_hover_highlight ? "primary" : undefined,
106
- action: hybridViewerStore.is_hover_highlight
107
- ? (): void => {
108
- hybridViewerStore.is_hover_highlight = false;
109
- hybridViewerStore.clearHoverHighlight();
110
- }
111
- : undefined,
141
+ action: hybridViewerStore.is_hover_highlight ? stopHoverHighlight : undefined,
112
142
  menu: [
113
143
  {
114
144
  title: "Cells",
115
145
  testId: "highlightOnHoverCellsButton",
116
146
  icon: "mdi-select-all",
117
147
  action: (): void => {
118
- if (
119
- hybridViewerStore.is_hover_highlight &&
120
- hybridViewerStore.hover_highlight_field_type === "CELL"
121
- ) {
122
- hybridViewerStore.is_hover_highlight = false;
123
- hybridViewerStore.clearHoverHighlight();
124
- } else {
125
- hybridViewerStore.is_hover_highlight = true;
126
- hybridViewerStore.hover_highlight_field_type = "CELL";
127
- }
148
+ toggleHoverHighlight("CELL");
128
149
  },
129
150
  },
130
151
  {
@@ -132,16 +153,7 @@ const camera_options = computed<CameraOptionAction[]>(() => [
132
153
  testId: "highlightOnHoverPointsButton",
133
154
  icon: "mdi-select-drag",
134
155
  action: (): void => {
135
- if (
136
- hybridViewerStore.is_hover_highlight &&
137
- hybridViewerStore.hover_highlight_field_type === "POINT"
138
- ) {
139
- hybridViewerStore.is_hover_highlight = false;
140
- hybridViewerStore.clearHoverHighlight();
141
- } else {
142
- hybridViewerStore.is_hover_highlight = true;
143
- hybridViewerStore.hover_highlight_field_type = "POINT";
144
- }
156
+ toggleHoverHighlight("POINT");
145
157
  },
146
158
  },
147
159
  ],
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ label: string;
4
+ color: string;
5
+ icon: string;
6
+ }
7
+
8
+ const { label, color, icon } = defineProps<Props>();
9
+
10
+ const emit = defineEmits<{
11
+ close: [];
12
+ }>();
13
+
14
+ defineOptions({ inheritAttrs: false });
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="tool-active-container d-flex justify-center w-100 pa-4"
20
+ :style="{ '--tool-active-color': `var(--v-theme-${color})` }"
21
+ >
22
+ <v-chip
23
+ v-bind="$attrs"
24
+ :color="color"
25
+ :prepend-icon="icon"
26
+ elevation="8"
27
+ size="large"
28
+ variant="flat"
29
+ class="tool-active-pulse"
30
+ style="pointer-events: auto"
31
+ @click="emit('close')"
32
+ >
33
+ {{ label }} &middot; Esc to stop
34
+ <v-divider vertical class="mx-2 my-1" opacity="0.3" />
35
+ <v-icon icon="mdi-close" size="small" />
36
+ </v-chip>
37
+ </div>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .tool-active-container {
42
+ position: absolute;
43
+ top: 20px;
44
+ left: 0;
45
+ pointer-events: none;
46
+ z-index: 3;
47
+ }
48
+
49
+ @keyframes pulse-ring {
50
+ 0% {
51
+ box-shadow: 0 0 0 0 rgba(var(--tool-active-color), 0.7);
52
+ }
53
+ 70% {
54
+ box-shadow: 0 0 0 10px rgba(var(--tool-active-color), 0);
55
+ }
56
+ 100% {
57
+ box-shadow: 0 0 0 0 rgba(var(--tool-active-color), 0);
58
+ }
59
+ }
60
+
61
+ .tool-active-pulse {
62
+ animation: pulse-ring 1.5s ease-out infinite;
63
+ }
64
+ </style>
@@ -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>
@@ -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
  });
@@ -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,
@@ -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;
@@ -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;
@@ -1,11 +1,10 @@
1
- import type { Vector3, ViewStreamLike } from "./vtk_types";
2
1
  import { centerCameraOnPosition, useHybridViewerCamera } from "./camera";
3
2
  import { Status } from "@ogw_front/utils/status";
3
+ import type { ViewStreamLike } from "./vtk_types";
4
4
  import { WHEEL_TIME_OUT_MS } from "./constants";
5
5
  import { useHybridViewerCore } from "./core";
6
6
  import { useHybridViewerHighlight } from "./highlight";
7
7
  import { useViewerStore } from "@ogw_front/stores/viewer";
8
- import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
9
8
 
10
9
  type ContainerRef = Ref<{ $el: HTMLElement } | undefined>;
11
10
 
@@ -44,37 +43,26 @@ async function resize(width: number, height: number): Promise<void> {
44
43
  await remoteRender();
45
44
  }
46
45
 
47
- function performClickPicking(event: MouseEvent, containerElement: HTMLElement): void {
46
+ async function performClickPicking(
47
+ event: MouseEvent,
48
+ containerElement: HTMLElement,
49
+ ): Promise<void> {
48
50
  const { genericRenderWindow } = useHybridViewerCore();
49
51
  const { syncRemoteCamera } = useHybridViewerCamera();
50
52
  const viewerStore = useViewerStore();
51
53
  const rect = containerElement.getBoundingClientRect();
52
- const schema = viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
53
- const params = {
54
- x: Math.round(event.clientX - rect.left),
55
- y: Math.round(rect.height - (event.clientY - rect.top)),
56
- };
57
- void viewerStore.request(
58
- {
59
- schema,
60
- params,
61
- },
62
- {
63
- response_function: (response: unknown) => {
64
- // oxlint-disable-next-line no-unsafe-type-assertion -- response shape is defined by the get_point_position schema.
65
- const { x, y, z } = response as { x: number; y: number; z: number };
66
- const pickedPos: Vector3 = [x, y, z];
67
- if (!genericRenderWindow.value || !pickedPos.some((val) => val !== 0)) {
68
- return;
69
- }
70
- const renderer = genericRenderWindow.value.getRenderer();
71
- const camera = renderer.getActiveCamera();
72
- centerCameraOnPosition(camera, pickedPos);
73
- genericRenderWindow.value.getRenderWindow().render();
74
- syncRemoteCamera();
75
- },
76
- },
54
+ const pickedPos = await viewerStore.pick_world_position(
55
+ event.clientX - rect.left,
56
+ rect.height - (event.clientY - rect.top),
77
57
  );
58
+ if (!genericRenderWindow.value || !pickedPos.some((val) => val !== 0)) {
59
+ return;
60
+ }
61
+ const renderer = genericRenderWindow.value.getRenderer();
62
+ const camera = renderer.getActiveCamera();
63
+ centerCameraOnPosition(camera, pickedPos);
64
+ genericRenderWindow.value.getRenderWindow().render();
65
+ syncRemoteCamera();
78
66
  }
79
67
 
80
68
  function setContainer(container: ContainerRef | undefined): void {
@@ -114,7 +102,7 @@ function setContainer(container: ContainerRef | undefined): void {
114
102
  return;
115
103
  }
116
104
  if (event.button === 0 && is_picking.value) {
117
- performClickPicking(event, containerElement);
105
+ void performClickPicking(event, containerElement);
118
106
  is_picking.value = false;
119
107
  return;
120
108
  }
@@ -0,0 +1,74 @@
1
+ import type { CameraOptions, Vector3 } from "./vtk_types";
2
+ import {
3
+ applyCameraOptions,
4
+ centerCameraOnPosition,
5
+ getCameraOptions,
6
+ useHybridViewerCamera,
7
+ } from "./camera";
8
+ import { requireRenderWindow, useHybridViewerCore } from "./core";
9
+ import type { Position } from "@vueuse/core";
10
+ import { useViewerStore } from "@ogw_front/stores/viewer";
11
+ import { newInstance as vtkCamera } from "@kitware/vtk.js/Rendering/Core/Camera";
12
+
13
+ interface ZoomBox {
14
+ start: Position;
15
+ end: Position;
16
+ }
17
+
18
+ interface Viewport {
19
+ width: number;
20
+ height: number;
21
+ }
22
+
23
+ function viewportShapedBox(start: Position, end: Position, { width, height }: Viewport): ZoomBox {
24
+ const deltaX = end.x - start.x;
25
+ const deltaY = end.y - start.y;
26
+ const aspect = Math.max(width, 1) / Math.max(height, 1);
27
+ const boxWidth = Math.max(Math.abs(deltaX), Math.abs(deltaY) * aspect);
28
+ return {
29
+ start,
30
+ end: {
31
+ x: start.x + (Math.sign(deltaX) || 1) * boxWidth,
32
+ y: start.y + (Math.sign(deltaY) || 1) * (boxWidth / aspect),
33
+ },
34
+ };
35
+ }
36
+
37
+ function computeZoomToBoxCamera(
38
+ camera: CameraOptions,
39
+ box: ZoomBox,
40
+ { width }: Viewport,
41
+ target: Vector3,
42
+ ): CameraOptions {
43
+ const zoomCamera = vtkCamera();
44
+ applyCameraOptions(zoomCamera, camera);
45
+ centerCameraOnPosition(zoomCamera, target);
46
+ // The box is viewport-shaped (see viewportShapedBox), so its width alone gives the zoom factor.
47
+ zoomCamera.dolly(width / Math.abs(box.end.x - box.start.x));
48
+ return getCameraOptions(zoomCamera);
49
+ }
50
+
51
+ const useHybridViewerZoomBox = createSharedComposable(() => {
52
+ const is_zoom_box_active = ref(false);
53
+
54
+ // Leaves the mode first so a second box cannot be drawn while the pick is pending. Request errors are reported by the viewer store.
55
+ async function zoomToBox(box: ZoomBox, viewport: Viewport): Promise<void> {
56
+ is_zoom_box_active.value = false;
57
+ const target = await useViewerStore().pick_world_position(
58
+ (box.start.x + box.end.x) / 2,
59
+ viewport.height - (box.start.y + box.end.y) / 2,
60
+ );
61
+ const { genericRenderWindow } = useHybridViewerCore();
62
+ const { setCamera } = useHybridViewerCamera();
63
+ const camera = requireRenderWindow(genericRenderWindow).getRenderer().getActiveCamera();
64
+ setCamera(computeZoomToBoxCamera(getCameraOptions(camera), box, viewport, target));
65
+ }
66
+
67
+ return {
68
+ is_zoom_box_active,
69
+ zoomToBox,
70
+ };
71
+ });
72
+
73
+ export { computeZoomToBoxCamera, useHybridViewerZoomBox, viewportShapedBox };
74
+ export type { ZoomBox };
package/nuxt.config.ts CHANGED
@@ -152,6 +152,7 @@ export default defineNuxtConfig({
152
152
  "@kitware/vtk.js/IO/XML/XMLPolyDataReader",
153
153
  "@kitware/vtk.js/Rendering/Core/Actor",
154
154
  "@kitware/vtk.js/Rendering/Core/AnnotatedCubeActor",
155
+ "@kitware/vtk.js/Rendering/Core/Camera",
155
156
  "@kitware/vtk.js/Rendering/Core/ColorTransferFunction",
156
157
  "@kitware/vtk.js/Rendering/Core/Mapper",
157
158
  "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.35.0-rc.2",
3
+ "version": "10.35.0-rc.3",
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": {