@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
|
@@ -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" />
|
|
@@ -3,6 +3,7 @@ import Loading from "@ogw_front/components/Loading.vue";
|
|
|
3
3
|
import Recaptcha from "@ogw_front/components/Recaptcha.vue";
|
|
4
4
|
import { Status } from "@ogw_front/utils/status";
|
|
5
5
|
import { appMode } from "@ogw_shared/app_mode";
|
|
6
|
+
import { getCloudUrlParam } from "@ogw_front/utils/cloud";
|
|
6
7
|
import { useInfraStore } from "@ogw_front/stores/infra";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -15,8 +16,10 @@ interface Props {
|
|
|
15
16
|
const { appName, email = undefined, isUserAuthenticated = false, logo = "" } = defineProps<Props>();
|
|
16
17
|
|
|
17
18
|
const infraStore = useInfraStore();
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
|
|
20
|
+
// Points at an already running service: that one is connected straight away, without authentication.
|
|
21
|
+
if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
|
|
22
|
+
infraStore.create_backend(email);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function cloudCreateBackend(): Promise<void> {
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
MESH_ELEMENT_KINDS,
|
|
4
|
+
MODEL_COMPONENT_KINDS,
|
|
5
|
+
getAttributeRange,
|
|
6
|
+
} from "@ogw_front/utils/attributes";
|
|
7
|
+
import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
|
|
8
|
+
import ToolPanel from "@ogw_front/components/ToolPanel.vue";
|
|
9
|
+
import ViewerOptionsAttributeRangeSelector from "@ogw_front/components/Viewer/Options/AttributeRangeSelector.vue";
|
|
10
|
+
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
|
|
11
|
+
import { useBackStore } from "@ogw_front/stores/back";
|
|
12
|
+
import { useDataStore } from "@ogw_front/stores/data";
|
|
13
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
14
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
15
|
+
|
|
16
|
+
const DEBOUNCE_DELAY = 100;
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
escapeFunction?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { escapeFunction = undefined } = defineProps<Props>();
|
|
23
|
+
|
|
24
|
+
interface AttributeSource {
|
|
25
|
+
title: string;
|
|
26
|
+
schema: JsonRpcSchema;
|
|
27
|
+
params: Record<string, unknown>;
|
|
28
|
+
location: "point" | "cell";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AttributeInfo {
|
|
32
|
+
attribute_name: string;
|
|
33
|
+
nb_items: number;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const show = defineModel<boolean>("show", { default: false });
|
|
38
|
+
const backStore = useBackStore();
|
|
39
|
+
const dataStore = useDataStore();
|
|
40
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
41
|
+
const selectedDatasetId = ref<string>();
|
|
42
|
+
const sources = ref<AttributeSource[]>([]);
|
|
43
|
+
const selectedSourceIndex = ref<number>();
|
|
44
|
+
const attributes = ref<AttributeInfo[]>([]);
|
|
45
|
+
const attributeName = ref<string>();
|
|
46
|
+
const attributeItem = ref<number>(0);
|
|
47
|
+
const minimum = ref<number>();
|
|
48
|
+
const maximum = ref<number>();
|
|
49
|
+
|
|
50
|
+
const allItems = dataStore.refAllItems();
|
|
51
|
+
const availableDatasets = computed<{ title: string; value: string }[]>(() =>
|
|
52
|
+
allItems.value.map((item) => ({
|
|
53
|
+
title: item.name || item.id,
|
|
54
|
+
value: item.id,
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
const availableSources = computed<{ title: string; value: number }[]>(() =>
|
|
58
|
+
sources.value.map((source, index) => ({ title: source.title, value: index })),
|
|
59
|
+
);
|
|
60
|
+
const currentAttribute = computed<AttributeInfo | undefined>(() =>
|
|
61
|
+
attributes.value.find((attribute) => attribute.attribute_name === attributeName.value),
|
|
62
|
+
);
|
|
63
|
+
const itemOptions = computed<{ title: string; value: number }[]>(() =>
|
|
64
|
+
Array.from({ length: currentAttribute.value?.nb_items ?? 0 }, (_, index) => ({
|
|
65
|
+
title: `Item ${index + 1}`,
|
|
66
|
+
value: index,
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
function attributeSchema(kind: string, isModel: boolean): JsonRpcSchema {
|
|
71
|
+
const prefix = isModel ? "model_component_" : "";
|
|
72
|
+
return back_schemas.opengeodeweb_back[
|
|
73
|
+
`${prefix}${kind}_attribute_names` as keyof typeof back_schemas.opengeodeweb_back
|
|
74
|
+
] as JsonRpcSchema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function attributeSource(
|
|
78
|
+
kind: string,
|
|
79
|
+
title: string,
|
|
80
|
+
params: Record<string, unknown>,
|
|
81
|
+
isModel: boolean,
|
|
82
|
+
): AttributeSource {
|
|
83
|
+
return {
|
|
84
|
+
title: `${title}${kind} attribute`,
|
|
85
|
+
schema: attributeSchema(kind, isModel),
|
|
86
|
+
params,
|
|
87
|
+
location: kind === "vertex" ? "point" : "cell",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchSources(id: string): Promise<AttributeSource[]> {
|
|
92
|
+
const item = await dataStore.item(id);
|
|
93
|
+
if (item.viewer_type !== "model") {
|
|
94
|
+
return ["vertex", MESH_ELEMENT_KINDS[item.geode_object_type]]
|
|
95
|
+
.filter((kind): kind is string => kind !== undefined)
|
|
96
|
+
.map((kind) => attributeSource(kind, "", { id }, false));
|
|
97
|
+
}
|
|
98
|
+
const modelSources = await Promise.all(
|
|
99
|
+
Object.entries(MODEL_COMPONENT_KINDS).map(async ([type, kinds]) => {
|
|
100
|
+
const component_ids = await dataStore.getMeshComponentGeodeIds(id, type);
|
|
101
|
+
if (component_ids.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return kinds.map((kind) => attributeSource(kind, `${type} `, { id, component_ids }, true));
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
return modelSources.flat();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function applyThreshold(): Promise<void> {
|
|
111
|
+
const source = sources.value[selectedSourceIndex.value ?? -1];
|
|
112
|
+
if (
|
|
113
|
+
!selectedDatasetId.value ||
|
|
114
|
+
!source ||
|
|
115
|
+
!attributeName.value ||
|
|
116
|
+
minimum.value === undefined ||
|
|
117
|
+
maximum.value === undefined ||
|
|
118
|
+
minimum.value > maximum.value
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await hybridViewerStore.setThreshold([selectedDatasetId.value], {
|
|
123
|
+
name: attributeName.value,
|
|
124
|
+
location: source.location,
|
|
125
|
+
item: attributeItem.value,
|
|
126
|
+
minimum: minimum.value,
|
|
127
|
+
maximum: maximum.value,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const debouncedApply = useDebounceFn(() => applyThreshold(), DEBOUNCE_DELAY);
|
|
132
|
+
|
|
133
|
+
function resetRange(): void {
|
|
134
|
+
const { min, max } = getAttributeRange(
|
|
135
|
+
currentAttribute.value as Parameters<typeof getAttributeRange>[0],
|
|
136
|
+
attributeItem.value,
|
|
137
|
+
);
|
|
138
|
+
minimum.value = min;
|
|
139
|
+
maximum.value = max;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function removeThreshold(): Promise<void> {
|
|
143
|
+
attributeName.value = undefined;
|
|
144
|
+
if (selectedDatasetId.value) {
|
|
145
|
+
await hybridViewerStore.setThreshold([selectedDatasetId.value]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
watch(selectedDatasetId, async (id, previousId) => {
|
|
150
|
+
if (previousId) {
|
|
151
|
+
await hybridViewerStore.setThreshold([previousId]);
|
|
152
|
+
}
|
|
153
|
+
selectedSourceIndex.value = undefined;
|
|
154
|
+
sources.value = id ? await fetchSources(id) : [];
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
watch(selectedSourceIndex, async (index) => {
|
|
158
|
+
attributeName.value = undefined;
|
|
159
|
+
attributes.value = [];
|
|
160
|
+
const source = sources.value[index ?? -1];
|
|
161
|
+
if (!source) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const response = (await backStore.request({
|
|
165
|
+
schema: source.schema,
|
|
166
|
+
params: source.params,
|
|
167
|
+
})) as { attributes: AttributeInfo[] };
|
|
168
|
+
attributes.value = response.attributes;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watch(attributeName, () => {
|
|
172
|
+
attributeItem.value = 0;
|
|
173
|
+
resetRange();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
watch(attributeItem, () => {
|
|
177
|
+
resetRange();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
watch([minimum, maximum], () => {
|
|
181
|
+
debouncedApply();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
watch(show, (visible) => {
|
|
185
|
+
if (visible) {
|
|
186
|
+
applyThreshold();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<template>
|
|
192
|
+
<ToolPanel
|
|
193
|
+
v-model="show"
|
|
194
|
+
data-testid="thresholdFilterPanel"
|
|
195
|
+
title="Threshold Filter"
|
|
196
|
+
:width="340"
|
|
197
|
+
:click-outside="false"
|
|
198
|
+
:escapeFunction="escapeFunction"
|
|
199
|
+
>
|
|
200
|
+
<v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
|
|
201
|
+
<v-select
|
|
202
|
+
v-model="selectedDatasetId"
|
|
203
|
+
data-testid="thresholdDatasetSelect"
|
|
204
|
+
:items="availableDatasets"
|
|
205
|
+
label="Select a dataset"
|
|
206
|
+
variant="outlined"
|
|
207
|
+
density="compact"
|
|
208
|
+
hide-details
|
|
209
|
+
class="mb-3 text-caption"
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
<v-select
|
|
213
|
+
v-if="selectedDatasetId"
|
|
214
|
+
v-model="selectedSourceIndex"
|
|
215
|
+
data-testid="thresholdAttributeTypeSelect"
|
|
216
|
+
:items="availableSources"
|
|
217
|
+
label="Select an attribute type"
|
|
218
|
+
variant="outlined"
|
|
219
|
+
density="compact"
|
|
220
|
+
hide-details
|
|
221
|
+
class="mb-3 text-caption"
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
<v-select
|
|
225
|
+
v-if="selectedSourceIndex !== undefined"
|
|
226
|
+
v-model="attributeName"
|
|
227
|
+
data-testid="thresholdAttributeSelect"
|
|
228
|
+
:items="attributes.map((attribute) => attribute.attribute_name)"
|
|
229
|
+
label="Select an attribute"
|
|
230
|
+
variant="outlined"
|
|
231
|
+
density="compact"
|
|
232
|
+
hide-details
|
|
233
|
+
class="mb-3 text-caption"
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<v-select
|
|
237
|
+
v-if="currentAttribute && currentAttribute.nb_items > 1"
|
|
238
|
+
v-model="attributeItem"
|
|
239
|
+
data-testid="thresholdItemSelect"
|
|
240
|
+
:items="itemOptions"
|
|
241
|
+
label="Select an item"
|
|
242
|
+
variant="outlined"
|
|
243
|
+
density="compact"
|
|
244
|
+
hide-details
|
|
245
|
+
class="mb-3 text-caption"
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
<template v-if="attributeName">
|
|
249
|
+
<v-divider class="my-3" />
|
|
250
|
+
<span class="text-caption font-weight-bold">Range</span>
|
|
251
|
+
<ViewerOptionsAttributeRangeSelector
|
|
252
|
+
v-model:minimum="minimum"
|
|
253
|
+
v-model:maximum="maximum"
|
|
254
|
+
class="mt-2"
|
|
255
|
+
@reset="resetRange"
|
|
256
|
+
/>
|
|
257
|
+
</template>
|
|
258
|
+
</v-card-text>
|
|
259
|
+
|
|
260
|
+
<template #actions>
|
|
261
|
+
<v-card-actions class="justify-space-between px-3 pb-3 pt-0">
|
|
262
|
+
<v-btn
|
|
263
|
+
data-testid="removeThresholdButton"
|
|
264
|
+
variant="text"
|
|
265
|
+
size="small"
|
|
266
|
+
color="error"
|
|
267
|
+
class="text-caption text-none"
|
|
268
|
+
@click="removeThreshold"
|
|
269
|
+
>
|
|
270
|
+
Remove Threshold
|
|
271
|
+
</v-btn>
|
|
272
|
+
<v-btn
|
|
273
|
+
data-testid="resetThresholdButton"
|
|
274
|
+
variant="tonal"
|
|
275
|
+
size="small"
|
|
276
|
+
color="secondary"
|
|
277
|
+
class="text-caption text-none"
|
|
278
|
+
@click="resetRange"
|
|
279
|
+
>
|
|
280
|
+
Reset
|
|
281
|
+
</v-btn>
|
|
282
|
+
</v-card-actions>
|
|
283
|
+
</template>
|
|
284
|
+
</ToolPanel>
|
|
285
|
+
</template>
|
|
286
|
+
|
|
287
|
+
<style scoped>
|
|
288
|
+
.max-panel-height {
|
|
289
|
+
max-height: 520px;
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -7,6 +7,7 @@ import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue";
|
|
|
7
7
|
import Ruler from "@ogw_front/components/Ruler.vue";
|
|
8
8
|
import Screenshot from "@ogw_front/components/Screenshot.vue";
|
|
9
9
|
import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue";
|
|
10
|
+
import ThresholdFilter from "@ogw_front/components/ThresholdFilter.vue";
|
|
10
11
|
import ZScaling from "@ogw_front/components/ZScaling.vue";
|
|
11
12
|
import { onKeyStroke } from "@vueuse/core";
|
|
12
13
|
import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
|
|
@@ -15,12 +16,15 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
|
|
|
15
16
|
|
|
16
17
|
const hybridViewerStore = useHybridViewerStore();
|
|
17
18
|
const viewerStore = useViewerStore();
|
|
19
|
+
const { is_picking, is_zoom_box_active } = storeToRefs(hybridViewerStore);
|
|
20
|
+
const { picking_mode } = storeToRefs(viewerStore);
|
|
18
21
|
const showScreenshot = ref<boolean>(false);
|
|
19
22
|
const showCameraManager = ref<boolean>(false);
|
|
20
23
|
const showCameraOrientation = ref<boolean>(false);
|
|
21
24
|
const showZScaling = ref<boolean>(false);
|
|
22
25
|
const showClippingPlanes = ref<boolean>(false);
|
|
23
26
|
const showShrinkFilter = ref<boolean>(false);
|
|
27
|
+
const showThresholdFilter = ref<boolean>(false);
|
|
24
28
|
const showRuler = ref<boolean>(false);
|
|
25
29
|
const gridScale = ref<boolean>(false);
|
|
26
30
|
const zScale = ref<number>(hybridViewerStore.zScale);
|
|
@@ -58,6 +62,11 @@ onKeyStroke("Escape", () => {
|
|
|
58
62
|
}
|
|
59
63
|
});
|
|
60
64
|
|
|
65
|
+
function stopHoverHighlight(): void {
|
|
66
|
+
hybridViewerStore.is_hover_highlight = false;
|
|
67
|
+
hybridViewerStore.clearHoverHighlight();
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
function closeAllToolsExcept(toolRef: Ref<boolean>): void {
|
|
62
71
|
const tools = [
|
|
63
72
|
showCameraOrientation,
|
|
@@ -66,13 +75,20 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
|
|
|
66
75
|
showZScaling,
|
|
67
76
|
showClippingPlanes,
|
|
68
77
|
showShrinkFilter,
|
|
78
|
+
showThresholdFilter,
|
|
69
79
|
showRuler,
|
|
80
|
+
is_picking,
|
|
81
|
+
is_zoom_box_active,
|
|
82
|
+
picking_mode,
|
|
70
83
|
];
|
|
71
84
|
for (const tool of tools) {
|
|
72
85
|
if (tool !== toolRef) {
|
|
73
86
|
tool.value = false;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
89
|
+
if (toolRef !== showRuler) {
|
|
90
|
+
void nextTick(stopHoverHighlight);
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
function toggleTool(toolRef: Ref<boolean>): void {
|
|
@@ -80,6 +96,19 @@ function toggleTool(toolRef: Ref<boolean>): void {
|
|
|
80
96
|
toolRef.value = !toolRef.value;
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
function toggleHoverHighlight(fieldType: string): void {
|
|
100
|
+
const activate =
|
|
101
|
+
!hybridViewerStore.is_hover_highlight ||
|
|
102
|
+
hybridViewerStore.hover_highlight_field_type !== fieldType;
|
|
103
|
+
closeAllToolsExcept(showRuler);
|
|
104
|
+
if (activate) {
|
|
105
|
+
hybridViewerStore.is_hover_highlight = true;
|
|
106
|
+
hybridViewerStore.hover_highlight_field_type = fieldType;
|
|
107
|
+
} else {
|
|
108
|
+
stopHoverHighlight();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
const camera_options = computed<CameraOptionAction[]>(() => [
|
|
84
113
|
{
|
|
85
114
|
testId: "resetCameraButton",
|
|
@@ -95,7 +124,16 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
95
124
|
icon: "mdi-crosshairs-question",
|
|
96
125
|
color: hybridViewerStore.is_picking ? "primary" : undefined,
|
|
97
126
|
action: (): void => {
|
|
98
|
-
|
|
127
|
+
toggleTool(is_picking);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
testId: "zoomToBoxButton",
|
|
132
|
+
tooltip: "Zoom to box",
|
|
133
|
+
icon: "mdi-magnify-scan",
|
|
134
|
+
color: hybridViewerStore.is_zoom_box_active ? "primary" : undefined,
|
|
135
|
+
action: (): void => {
|
|
136
|
+
toggleTool(is_zoom_box_active);
|
|
99
137
|
},
|
|
100
138
|
},
|
|
101
139
|
{
|
|
@@ -103,28 +141,14 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
103
141
|
tooltip: "Highlight on hover",
|
|
104
142
|
icon: "mdi-cursor-default-click",
|
|
105
143
|
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,
|
|
144
|
+
action: hybridViewerStore.is_hover_highlight ? stopHoverHighlight : undefined,
|
|
112
145
|
menu: [
|
|
113
146
|
{
|
|
114
147
|
title: "Cells",
|
|
115
148
|
testId: "highlightOnHoverCellsButton",
|
|
116
149
|
icon: "mdi-select-all",
|
|
117
150
|
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
|
-
}
|
|
151
|
+
toggleHoverHighlight("CELL");
|
|
128
152
|
},
|
|
129
153
|
},
|
|
130
154
|
{
|
|
@@ -132,16 +156,7 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
132
156
|
testId: "highlightOnHoverPointsButton",
|
|
133
157
|
icon: "mdi-select-drag",
|
|
134
158
|
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
|
-
}
|
|
159
|
+
toggleHoverHighlight("POINT");
|
|
145
160
|
},
|
|
146
161
|
},
|
|
147
162
|
],
|
|
@@ -219,6 +234,15 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
219
234
|
toggleTool(showShrinkFilter);
|
|
220
235
|
},
|
|
221
236
|
},
|
|
237
|
+
{
|
|
238
|
+
testId: "thresholdFilterButton",
|
|
239
|
+
tooltip: "Threshold Filter",
|
|
240
|
+
icon: "mdi-filter-variant",
|
|
241
|
+
color: showThresholdFilter.value ? "primary" : undefined,
|
|
242
|
+
action: (): void => {
|
|
243
|
+
toggleTool(showThresholdFilter);
|
|
244
|
+
},
|
|
245
|
+
},
|
|
222
246
|
{
|
|
223
247
|
testId: "rulerButton",
|
|
224
248
|
tooltip: "Ruler",
|
|
@@ -308,6 +332,10 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
308
332
|
v-model:show="showShrinkFilter"
|
|
309
333
|
:escapeFunction="() => (showShrinkFilter = false)"
|
|
310
334
|
/>
|
|
335
|
+
<ThresholdFilter
|
|
336
|
+
v-model:show="showThresholdFilter"
|
|
337
|
+
:escapeFunction="() => (showThresholdFilter = false)"
|
|
338
|
+
/>
|
|
311
339
|
<Ruler v-model:show="showRuler" :escapeFunction="() => (showRuler = false)" />
|
|
312
340
|
</template>
|
|
313
341
|
|
|
@@ -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>
|