@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.
- package/app/components/HybridRenderingView.vue +2 -0
- package/app/components/Launcher.vue +5 -2
- package/app/components/RemoteRenderingView.vue +0 -4
- package/app/components/ThresholdFilter.vue +291 -0
- package/app/components/ViewToolbar.vue +55 -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/back.ts +10 -6
- package/app/stores/cloud.ts +59 -15
- package/app/stores/hybrid_viewer.ts +3 -0
- package/app/stores/infra.ts +7 -1
- package/app/stores/viewer.ts +11 -20
- package/app/utils/attributes.ts +26 -1
- package/app/utils/cloud.ts +35 -0
- package/internal/stores/hybrid_viewer/camera.ts +2 -0
- package/internal/stores/hybrid_viewer/filters.ts +24 -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
|
@@ -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>
|
package/app/stores/back.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
package/app/stores/cloud.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/app/stores/infra.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|
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,
|
package/app/utils/attributes.ts
CHANGED
|
@@ -10,7 +10,7 @@ interface AttributeRange {
|
|
|
10
10
|
max: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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;
|