@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.
- package/app/components/HybridRenderingView.vue +2 -0
- package/app/components/RemoteRenderingView.vue +0 -4
- package/app/components/ViewToolbar.vue +39 -27
- package/app/components/Viewer/ToolActiveChip.vue +64 -0
- package/app/components/Viewer/Ui.vue +41 -93
- package/app/components/ZoomBoxOverlay.vue +64 -0
- package/app/stores/hybrid_viewer.ts +3 -0
- package/app/stores/viewer.ts +11 -20
- package/internal/stores/hybrid_viewer/camera.ts +2 -0
- package/internal/stores/hybrid_viewer/ruler.ts +8 -14
- package/internal/stores/hybrid_viewer/viewport.ts +17 -29
- package/internal/stores/hybrid_viewer/zoom_box.ts +74 -0
- package/nuxt.config.ts +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }} · 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
|
-
<
|
|
113
|
+
<v-fade-transition group>
|
|
114
|
+
<ToolActiveChip
|
|
106
115
|
v-if="viewerStore.picking_mode"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 · 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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}}) · 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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
· 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
|
});
|
package/app/stores/viewer.ts
CHANGED
|
@@ -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
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
45
|
-
hover_highlight_field_type.value =
|
|
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
|
-
|
|
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(
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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