@geode/opengeodeweb-front 10.30.0-rc.2 → 10.30.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/ClippingPlaneCard.vue +112 -0
- package/app/components/ClippingPlanes.vue +238 -0
- package/app/components/ToolPanel.vue +3 -2
- package/app/components/ViewToolbar.vue +12 -0
- package/app/composables/clipping_planes_widget.js +262 -0
- package/app/stores/hybrid_viewer.js +8 -0
- package/app/utils/clipping_planes.js +110 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { getPlaneCssColor } from "@ogw_front/utils/clipping_planes";
|
|
3
|
+
|
|
4
|
+
const { plane, index } = defineProps({
|
|
5
|
+
plane: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
index: {
|
|
10
|
+
type: Number,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits(["remove", "flipNormal"]);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<v-card
|
|
20
|
+
data-testid="planeCard"
|
|
21
|
+
variant="outlined"
|
|
22
|
+
class="pa-2 mb-3 rounded-lg border-opacity-50"
|
|
23
|
+
:style="{ borderColor: getPlaneCssColor(index) }"
|
|
24
|
+
>
|
|
25
|
+
<v-row align="center" justify="space-between" no-gutters class="mb-1">
|
|
26
|
+
<v-col cols="auto" class="d-flex align-center">
|
|
27
|
+
<v-chip
|
|
28
|
+
size="x-small"
|
|
29
|
+
variant="flat"
|
|
30
|
+
:style="{
|
|
31
|
+
backgroundColor: getPlaneCssColor(index),
|
|
32
|
+
color: 'white',
|
|
33
|
+
}"
|
|
34
|
+
class="font-weight-bold"
|
|
35
|
+
>
|
|
36
|
+
Plane #{{ index + 1 }}
|
|
37
|
+
</v-chip>
|
|
38
|
+
</v-col>
|
|
39
|
+
<v-col cols="auto">
|
|
40
|
+
<v-btn
|
|
41
|
+
data-testid="removePlaneButton"
|
|
42
|
+
icon="mdi-trash-can-outline"
|
|
43
|
+
size="x-small"
|
|
44
|
+
variant="text"
|
|
45
|
+
color="error"
|
|
46
|
+
@click="emit('remove')"
|
|
47
|
+
/>
|
|
48
|
+
</v-col>
|
|
49
|
+
</v-row>
|
|
50
|
+
|
|
51
|
+
<v-row no-gutters class="mb-1">
|
|
52
|
+
<v-col class="text-caption text-medium-emphasis">Origin [X, Y, Z]</v-col>
|
|
53
|
+
</v-row>
|
|
54
|
+
<v-row v-if="plane.origin" dense class="mb-2">
|
|
55
|
+
<v-col v-for="axis in 3" :key="'orig-' + axis" cols="4">
|
|
56
|
+
<v-text-field
|
|
57
|
+
v-model.number="plane.origin[axis - 1]"
|
|
58
|
+
data-testid="planeOriginInput"
|
|
59
|
+
type="number"
|
|
60
|
+
variant="outlined"
|
|
61
|
+
density="compact"
|
|
62
|
+
hide-details
|
|
63
|
+
step="any"
|
|
64
|
+
class="text-caption"
|
|
65
|
+
/>
|
|
66
|
+
</v-col>
|
|
67
|
+
</v-row>
|
|
68
|
+
<v-row v-else dense class="mb-2">
|
|
69
|
+
<v-col v-for="axis in 3" :key="'orig-placeholder-' + axis" cols="4">
|
|
70
|
+
<v-text-field
|
|
71
|
+
placeholder="—"
|
|
72
|
+
variant="outlined"
|
|
73
|
+
density="compact"
|
|
74
|
+
hide-details
|
|
75
|
+
disabled
|
|
76
|
+
class="text-caption"
|
|
77
|
+
/>
|
|
78
|
+
</v-col>
|
|
79
|
+
</v-row>
|
|
80
|
+
|
|
81
|
+
<v-row align="center" justify="space-between" no-gutters class="mb-1">
|
|
82
|
+
<v-col class="text-caption text-medium-emphasis">Normal [X, Y, Z]</v-col>
|
|
83
|
+
<v-col cols="auto">
|
|
84
|
+
<v-btn
|
|
85
|
+
data-testid="invertNormalButton"
|
|
86
|
+
size="x-small"
|
|
87
|
+
variant="text"
|
|
88
|
+
color="primary"
|
|
89
|
+
prepend-icon="mdi-swap-horizontal"
|
|
90
|
+
class="text-none text-caption px-1"
|
|
91
|
+
@click="emit('flipNormal')"
|
|
92
|
+
>
|
|
93
|
+
Invert Normal
|
|
94
|
+
</v-btn>
|
|
95
|
+
</v-col>
|
|
96
|
+
</v-row>
|
|
97
|
+
<v-row dense>
|
|
98
|
+
<v-col v-for="axis in 3" :key="'norm-' + axis" cols="4">
|
|
99
|
+
<v-text-field
|
|
100
|
+
v-model.number="plane.normal[axis - 1]"
|
|
101
|
+
data-testid="planeNormalInput"
|
|
102
|
+
type="number"
|
|
103
|
+
variant="outlined"
|
|
104
|
+
density="compact"
|
|
105
|
+
hide-details
|
|
106
|
+
step="any"
|
|
107
|
+
class="text-caption"
|
|
108
|
+
/>
|
|
109
|
+
</v-col>
|
|
110
|
+
</v-row>
|
|
111
|
+
</v-card>
|
|
112
|
+
</template>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { DEBOUNCE_DELAY, DEFAULT_NORMALS } from "@ogw_front/utils/clipping_planes";
|
|
3
|
+
import ClippingPlaneCard from "@ogw_front/components/ClippingPlaneCard";
|
|
4
|
+
import ToolPanel from "@ogw_front/components/ToolPanel";
|
|
5
|
+
import { useClippingPlanesWidget } from "@ogw_front/composables/clipping_planes_widget";
|
|
6
|
+
import { useDataStore } from "@ogw_front/stores/data";
|
|
7
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
8
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
9
|
+
|
|
10
|
+
const show = defineModel("show", { type: Boolean, default: false });
|
|
11
|
+
const dataStore = useDataStore();
|
|
12
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
13
|
+
const targetAllVisible = ref(true);
|
|
14
|
+
const selectedDatasetIds = ref([]);
|
|
15
|
+
const planes = ref([{ origin: undefined, normal: [1, 0, 0] }]);
|
|
16
|
+
const allItems = dataStore.refAllItems();
|
|
17
|
+
const availableDatasets = computed(() =>
|
|
18
|
+
allItems.value.map((item) => ({ title: item.name || item.id, value: item.id })),
|
|
19
|
+
);
|
|
20
|
+
const widgetContainer = useTemplateRef("widgetContainer");
|
|
21
|
+
const debouncedApply = useDebounceFn(() => applyClippingPlanes(), DEBOUNCE_DELAY);
|
|
22
|
+
const {
|
|
23
|
+
getSceneCenter,
|
|
24
|
+
syncWidgets,
|
|
25
|
+
syncLocalCamera,
|
|
26
|
+
cleanupLocalWidget,
|
|
27
|
+
initLocalWidget,
|
|
28
|
+
updateWidgetPlacement,
|
|
29
|
+
isFromWidget,
|
|
30
|
+
setFromWidget,
|
|
31
|
+
} = useClippingPlanesWidget({
|
|
32
|
+
planes,
|
|
33
|
+
targetAllVisible,
|
|
34
|
+
selectedDatasetIds,
|
|
35
|
+
allItems,
|
|
36
|
+
hybridViewerStore,
|
|
37
|
+
debouncedApply,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function addPlane() {
|
|
41
|
+
const normal = DEFAULT_NORMALS[planes.value.length % DEFAULT_NORMALS.length];
|
|
42
|
+
planes.value.push({ origin: getSceneCenter(), normal });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function removePlane(index) {
|
|
46
|
+
planes.value.splice(index, 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function flipNormal(plane) {
|
|
50
|
+
plane.normal = plane.normal.map((component) => -component);
|
|
51
|
+
syncWidgets();
|
|
52
|
+
applyClippingPlanes();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function applyClippingPlanes() {
|
|
56
|
+
const allIds = allItems.value.map((item) => item.id);
|
|
57
|
+
if (allIds.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const center = getSceneCenter();
|
|
61
|
+
const targetIds = targetAllVisible.value ? allIds : selectedDatasetIds.value;
|
|
62
|
+
const untargetedIds = allIds.filter((id) => !targetIds.includes(id));
|
|
63
|
+
const planesData = planes.value.map((plane) => ({
|
|
64
|
+
origin: (plane.origin || center).map(Number),
|
|
65
|
+
normal: plane.normal.map(Number),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
if (targetIds.length > 0) {
|
|
69
|
+
await hybridViewerStore.setClippingPlanes(targetIds, planesData);
|
|
70
|
+
}
|
|
71
|
+
if (untargetedIds.length > 0) {
|
|
72
|
+
await hybridViewerStore.setClippingPlanes(untargetedIds, []);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function resetClippingPlanes() {
|
|
77
|
+
setFromWidget(true);
|
|
78
|
+
planes.value = [{ origin: undefined, normal: [1, 0, 0] }];
|
|
79
|
+
updateWidgetPlacement({ isReset: true });
|
|
80
|
+
setFromWidget(false);
|
|
81
|
+
await applyClippingPlanes();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function removeClippingPlanes() {
|
|
85
|
+
const allIds = allItems.value.map((item) => item.id);
|
|
86
|
+
await hybridViewerStore.setClippingPlanes(allIds, []);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
watch(widgetContainer, (container) => {
|
|
90
|
+
if (container) {
|
|
91
|
+
initLocalWidget(container.$el || container);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
watch(
|
|
96
|
+
planes,
|
|
97
|
+
() => {
|
|
98
|
+
if (isFromWidget()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
syncWidgets();
|
|
102
|
+
debouncedApply();
|
|
103
|
+
},
|
|
104
|
+
{ deep: true },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
watch(show, (visible) => {
|
|
108
|
+
if (visible) {
|
|
109
|
+
updateWidgetPlacement({ isReset: true });
|
|
110
|
+
applyClippingPlanes();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
watch(
|
|
115
|
+
[targetAllVisible, selectedDatasetIds],
|
|
116
|
+
() => {
|
|
117
|
+
if (show.value) {
|
|
118
|
+
updateWidgetPlacement({ isReset: true });
|
|
119
|
+
applyClippingPlanes();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{ deep: true },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
watch(allItems, () => {
|
|
126
|
+
if (show.value) {
|
|
127
|
+
updateWidgetPlacement({ isReset: true });
|
|
128
|
+
applyClippingPlanes();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
watch(
|
|
133
|
+
() => Object.values(hybridViewerStore.hybridDb).filter((entry) => entry && entry.actor).length,
|
|
134
|
+
(actorCount) => {
|
|
135
|
+
if (show.value && actorCount > 0) {
|
|
136
|
+
updateWidgetPlacement({ isReset: true });
|
|
137
|
+
applyClippingPlanes();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
watch(() => hybridViewerStore.camera_options, syncLocalCamera, { deep: true });
|
|
143
|
+
|
|
144
|
+
onBeforeUnmount(cleanupLocalWidget);
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<ToolPanel v-model="show" title="Clipping Planes" :width="360" :click-outside="false">
|
|
149
|
+
<v-card-text class="pa-3 max-panel-height overflow-y-auto">
|
|
150
|
+
<v-sheet
|
|
151
|
+
ref="widgetContainer"
|
|
152
|
+
height="180"
|
|
153
|
+
color="transparent"
|
|
154
|
+
class="rounded-lg mb-3 overflow-hidden"
|
|
155
|
+
/>
|
|
156
|
+
<v-switch
|
|
157
|
+
v-model="targetAllVisible"
|
|
158
|
+
data-testid="targetAllVisibleSwitch"
|
|
159
|
+
label="Apply to all visible datasets"
|
|
160
|
+
color="primary"
|
|
161
|
+
density="compact"
|
|
162
|
+
hide-details
|
|
163
|
+
class="mb-2 text-caption"
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<v-select
|
|
167
|
+
v-if="!targetAllVisible"
|
|
168
|
+
v-model="selectedDatasetIds"
|
|
169
|
+
data-testid="selectedDatasetsSelect"
|
|
170
|
+
:items="availableDatasets"
|
|
171
|
+
label="Select datasets"
|
|
172
|
+
multiple
|
|
173
|
+
chips
|
|
174
|
+
closable-chips
|
|
175
|
+
variant="outlined"
|
|
176
|
+
density="compact"
|
|
177
|
+
hide-details
|
|
178
|
+
class="mb-3 text-caption"
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<v-divider class="my-2" />
|
|
182
|
+
|
|
183
|
+
<v-row align="center" justify="space-between" no-gutters class="mb-2">
|
|
184
|
+
<v-col class="text-caption font-weight-bold">Planes ({{ planes.length }})</v-col>
|
|
185
|
+
<v-col cols="auto">
|
|
186
|
+
<v-btn
|
|
187
|
+
data-testid="addPlaneButton"
|
|
188
|
+
size="x-small"
|
|
189
|
+
variant="tonal"
|
|
190
|
+
color="primary"
|
|
191
|
+
icon="mdi-plus"
|
|
192
|
+
@click="addPlane"
|
|
193
|
+
/>
|
|
194
|
+
</v-col>
|
|
195
|
+
</v-row>
|
|
196
|
+
|
|
197
|
+
<ClippingPlaneCard
|
|
198
|
+
v-for="(plane, idx) in planes"
|
|
199
|
+
:key="idx"
|
|
200
|
+
:plane="plane"
|
|
201
|
+
:index="idx"
|
|
202
|
+
@remove="removePlane(idx)"
|
|
203
|
+
@flip-normal="flipNormal(plane)"
|
|
204
|
+
/>
|
|
205
|
+
</v-card-text>
|
|
206
|
+
|
|
207
|
+
<template #actions>
|
|
208
|
+
<v-card-actions class="justify-space-between px-3 pb-3 pt-0">
|
|
209
|
+
<v-btn
|
|
210
|
+
data-testid="removeClippingPlanesButton"
|
|
211
|
+
variant="text"
|
|
212
|
+
size="small"
|
|
213
|
+
color="error"
|
|
214
|
+
class="text-caption text-none"
|
|
215
|
+
@click="removeClippingPlanes"
|
|
216
|
+
>
|
|
217
|
+
Remove Clipping
|
|
218
|
+
</v-btn>
|
|
219
|
+
<v-btn
|
|
220
|
+
data-testid="resetClippingPlanesButton"
|
|
221
|
+
variant="tonal"
|
|
222
|
+
size="small"
|
|
223
|
+
color="secondary"
|
|
224
|
+
class="text-caption text-none"
|
|
225
|
+
@click="resetClippingPlanes"
|
|
226
|
+
>
|
|
227
|
+
Reset
|
|
228
|
+
</v-btn>
|
|
229
|
+
</v-card-actions>
|
|
230
|
+
</template>
|
|
231
|
+
</ToolPanel>
|
|
232
|
+
</template>
|
|
233
|
+
|
|
234
|
+
<style scoped>
|
|
235
|
+
.max-panel-height {
|
|
236
|
+
max-height: 520px;
|
|
237
|
+
}
|
|
238
|
+
</style>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import GlassCard from "@ogw_front/components/GlassCard";
|
|
3
3
|
|
|
4
|
-
const { title, width, closeLabel, actionLabel } = defineProps({
|
|
4
|
+
const { title, width, closeLabel, actionLabel, clickOutside } = defineProps({
|
|
5
5
|
title: { type: String, default: "" },
|
|
6
6
|
width: { type: Number, default: 260 },
|
|
7
7
|
closeLabel: { type: String, default: "Close" },
|
|
8
8
|
actionLabel: { type: String, default: undefined },
|
|
9
|
+
clickOutside: { type: Boolean, default: true },
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
const model = defineModel({ type: Boolean, default: false });
|
|
@@ -19,7 +20,7 @@ function close() {
|
|
|
19
20
|
<template>
|
|
20
21
|
<GlassCard
|
|
21
22
|
v-if="model"
|
|
22
|
-
v-click-outside="close"
|
|
23
|
+
v-click-outside="{ handler: close, closeConditional: () => clickOutside }"
|
|
23
24
|
:title="title"
|
|
24
25
|
:width="width"
|
|
25
26
|
:ripple="false"
|
|
@@ -3,6 +3,7 @@ import ActionButton from "@ogw_front/components/ActionButton";
|
|
|
3
3
|
import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.svg";
|
|
4
4
|
import CameraManager from "@ogw_front/components/CameraManager";
|
|
5
5
|
import CameraOrientation from "@ogw_front/components/CameraOrientation";
|
|
6
|
+
import ClippingPlanes from "@ogw_front/components/ClippingPlanes";
|
|
6
7
|
import Screenshot from "@ogw_front/components/Screenshot";
|
|
7
8
|
import ZScaling from "@ogw_front/components/ZScaling";
|
|
8
9
|
import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
|
|
@@ -15,6 +16,7 @@ const take_screenshot = ref(false);
|
|
|
15
16
|
const show_camera_manager = ref(false);
|
|
16
17
|
const showCameraOrientation = ref(false);
|
|
17
18
|
const showZScaling = ref(false);
|
|
19
|
+
const showClippingPlanes = ref(false);
|
|
18
20
|
const grid_scale = ref(false);
|
|
19
21
|
const zScale = ref(hybridViewerStore.zScale);
|
|
20
22
|
|
|
@@ -151,6 +153,15 @@ const camera_options = computed(() => [
|
|
|
151
153
|
showZScaling.value = !showZScaling.value;
|
|
152
154
|
},
|
|
153
155
|
},
|
|
156
|
+
{
|
|
157
|
+
testId: "clippingPlanesButton",
|
|
158
|
+
tooltip: "Clipping Planes",
|
|
159
|
+
icon: "mdi-content-cut",
|
|
160
|
+
color: showClippingPlanes.value ? "primary" : undefined,
|
|
161
|
+
action: () => {
|
|
162
|
+
showClippingPlanes.value = !showClippingPlanes.value;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
154
165
|
]);
|
|
155
166
|
</script>
|
|
156
167
|
|
|
@@ -223,6 +234,7 @@ const camera_options = computed(() => [
|
|
|
223
234
|
:width="260"
|
|
224
235
|
@apply="handleZScalingClose"
|
|
225
236
|
/>
|
|
237
|
+
<ClippingPlanes v-model:show="showClippingPlanes" />
|
|
226
238
|
</template>
|
|
227
239
|
|
|
228
240
|
<style module>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AXIS_SCALE,
|
|
3
|
+
CHANGE_THRESHOLD,
|
|
4
|
+
PLANE_COLORS,
|
|
5
|
+
SIZE_RATIO,
|
|
6
|
+
computeSceneBoundsInfo,
|
|
7
|
+
getPlaneStyle,
|
|
8
|
+
hasPlaneChanged,
|
|
9
|
+
} from "@ogw_front/utils/clipping_planes";
|
|
10
|
+
import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
|
|
11
|
+
import { newInstance as vtkImplicitPlaneWidget } from "@kitware/vtk.js/Widgets/Widgets3D/ImplicitPlaneWidget";
|
|
12
|
+
import { newInstance as vtkWidgetManager } from "@kitware/vtk.js/Widgets/Core/WidgetManager";
|
|
13
|
+
|
|
14
|
+
// oxlint-disable-next-line max-params max-lines-per-function
|
|
15
|
+
function useClippingPlanesWidget({
|
|
16
|
+
planes,
|
|
17
|
+
targetAllVisible,
|
|
18
|
+
selectedDatasetIds,
|
|
19
|
+
allItems,
|
|
20
|
+
hybridViewerStore,
|
|
21
|
+
debouncedApply,
|
|
22
|
+
}) {
|
|
23
|
+
let localRenderWindow = undefined;
|
|
24
|
+
let widgetManager = undefined;
|
|
25
|
+
let widgetEntries = [];
|
|
26
|
+
let fromWidget = false;
|
|
27
|
+
let maxDistance = 0;
|
|
28
|
+
let isLimitingCameraZoom = false;
|
|
29
|
+
|
|
30
|
+
function resolveActiveActors() {
|
|
31
|
+
const targetIds = targetAllVisible.value
|
|
32
|
+
? allItems.value.map((item) => item.id)
|
|
33
|
+
: selectedDatasetIds.value;
|
|
34
|
+
const targeted = targetIds
|
|
35
|
+
.map((id) => {
|
|
36
|
+
const item = hybridViewerStore.hybridDb[id];
|
|
37
|
+
return item ? item.actor : undefined;
|
|
38
|
+
})
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
if (targeted.length > 0) {
|
|
41
|
+
return targeted;
|
|
42
|
+
}
|
|
43
|
+
return Object.values(hybridViewerStore.hybridDb)
|
|
44
|
+
.map((entry) => entry && entry.actor)
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSceneBoundsInfo() {
|
|
49
|
+
return computeSceneBoundsInfo(resolveActiveActors());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSceneCenter() {
|
|
53
|
+
return getSceneBoundsInfo().center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createWidgetEntry(planeWidget, widgetHandle, planeIndex) {
|
|
57
|
+
const widgetState = planeWidget.getWidgetState();
|
|
58
|
+
const plane = planes.value[planeIndex];
|
|
59
|
+
if (plane.origin) {
|
|
60
|
+
widgetState.setOrigin(plane.origin);
|
|
61
|
+
}
|
|
62
|
+
widgetState.setNormal(plane.normal);
|
|
63
|
+
const subscription = widgetState.onModified(() => {
|
|
64
|
+
if (fromWidget) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const origin = widgetState.getOrigin().map((val) => Number(val.toFixed(4)));
|
|
68
|
+
const normal = widgetState.getNormal().map((val) => Number(val.toFixed(4)));
|
|
69
|
+
if (
|
|
70
|
+
!hasPlaneChanged(
|
|
71
|
+
origin,
|
|
72
|
+
normal,
|
|
73
|
+
planes.value[planeIndex].origin,
|
|
74
|
+
planes.value[planeIndex].normal,
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
fromWidget = true;
|
|
80
|
+
planes.value[planeIndex].origin = origin;
|
|
81
|
+
planes.value[planeIndex].normal = normal;
|
|
82
|
+
nextTick(() => {
|
|
83
|
+
fromWidget = false;
|
|
84
|
+
});
|
|
85
|
+
debouncedApply();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { planeWidget, widgetHandle, subscription };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function syncWidgets() {
|
|
92
|
+
if (!widgetManager || !localRenderWindow) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
while (widgetEntries.length > planes.value.length) {
|
|
96
|
+
const entry = widgetEntries.pop();
|
|
97
|
+
entry.subscription.unsubscribe();
|
|
98
|
+
widgetManager.removeWidget(entry.planeWidget);
|
|
99
|
+
entry.planeWidget.delete();
|
|
100
|
+
}
|
|
101
|
+
const { cubicBounds } = getSceneBoundsInfo();
|
|
102
|
+
for (const [idx, plane] of planes.value.entries()) {
|
|
103
|
+
const rgb = PLANE_COLORS[idx % PLANE_COLORS.length];
|
|
104
|
+
if (!widgetEntries[idx]) {
|
|
105
|
+
const planeWidget = vtkImplicitPlaneWidget();
|
|
106
|
+
const widgetHandle = widgetManager.addWidget(planeWidget);
|
|
107
|
+
widgetHandle.setAxisScale(AXIS_SCALE);
|
|
108
|
+
widgetHandle.setHandleSizeRatio(SIZE_RATIO);
|
|
109
|
+
widgetEntries.push(createWidgetEntry(planeWidget, widgetHandle, idx));
|
|
110
|
+
}
|
|
111
|
+
const entry = widgetEntries[idx];
|
|
112
|
+
entry.widgetHandle.setRepresentationStyle(getPlaneStyle(rgb));
|
|
113
|
+
fromWidget = true;
|
|
114
|
+
entry.widgetHandle.placeWidget(cubicBounds);
|
|
115
|
+
fromWidget = false;
|
|
116
|
+
entry.widgetHandle.setAxisScale(AXIS_SCALE);
|
|
117
|
+
entry.widgetHandle.setHandleSizeRatio(SIZE_RATIO);
|
|
118
|
+
const widgetState = entry.planeWidget.getWidgetState();
|
|
119
|
+
if (plane.origin) {
|
|
120
|
+
widgetState.setOrigin(plane.origin);
|
|
121
|
+
}
|
|
122
|
+
widgetState.setNormal(plane.normal);
|
|
123
|
+
}
|
|
124
|
+
localRenderWindow.getRenderWindow().render();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function limitCameraZoomOut(camera) {
|
|
128
|
+
if (maxDistance <= 0 || isLimitingCameraZoom) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const currentDist = camera.getDistance();
|
|
133
|
+
if (currentDist <= maxDistance + CHANGE_THRESHOLD) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isLimitingCameraZoom = true;
|
|
138
|
+
const focal = camera.getFocalPoint();
|
|
139
|
+
const pos = camera.getPosition();
|
|
140
|
+
const ratio = maxDistance / currentDist;
|
|
141
|
+
|
|
142
|
+
camera.setPosition(
|
|
143
|
+
focal[0] + (pos[0] - focal[0]) * ratio,
|
|
144
|
+
focal[1] + (pos[1] - focal[1]) * ratio,
|
|
145
|
+
focal[2] + (pos[2] - focal[2]) * ratio,
|
|
146
|
+
);
|
|
147
|
+
localRenderWindow.getRenderWindow().render();
|
|
148
|
+
isLimitingCameraZoom = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function syncLocalCamera() {
|
|
152
|
+
if (!localRenderWindow) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const renderer = localRenderWindow.getRenderer();
|
|
156
|
+
const camera = renderer.getActiveCamera();
|
|
157
|
+
const mainCam = hybridViewerStore.camera_options;
|
|
158
|
+
const { center, cubicBounds } = getSceneBoundsInfo();
|
|
159
|
+
renderer.resetCamera(cubicBounds);
|
|
160
|
+
if (mainCam && mainCam.focal_point && mainCam.position) {
|
|
161
|
+
const dir = [
|
|
162
|
+
mainCam.position[0] - mainCam.focal_point[0],
|
|
163
|
+
mainCam.position[1] - mainCam.focal_point[1],
|
|
164
|
+
mainCam.position[2] - mainCam.focal_point[2],
|
|
165
|
+
];
|
|
166
|
+
const dirLen = Math.hypot(...dir);
|
|
167
|
+
if (dirLen > 0) {
|
|
168
|
+
const distance = camera.getDistance();
|
|
169
|
+
const normDir = dir.map((component) => component / dirLen);
|
|
170
|
+
camera.setFocalPoint(...center);
|
|
171
|
+
camera.setPosition(
|
|
172
|
+
center[0] + normDir[0] * distance,
|
|
173
|
+
center[1] + normDir[1] * distance,
|
|
174
|
+
center[2] + normDir[2] * distance,
|
|
175
|
+
);
|
|
176
|
+
if (mainCam.view_up) {
|
|
177
|
+
camera.setViewUp(...mainCam.view_up);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
limitCameraZoomOut(camera);
|
|
183
|
+
localRenderWindow.getRenderWindow().render();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cleanupLocalWidget() {
|
|
187
|
+
maxDistance = 0;
|
|
188
|
+
isLimitingCameraZoom = false;
|
|
189
|
+
for (const entry of widgetEntries) {
|
|
190
|
+
entry.subscription.unsubscribe();
|
|
191
|
+
if (widgetManager) {
|
|
192
|
+
widgetManager.removeWidget(entry.planeWidget);
|
|
193
|
+
}
|
|
194
|
+
entry.planeWidget.delete();
|
|
195
|
+
}
|
|
196
|
+
widgetEntries = [];
|
|
197
|
+
if (localRenderWindow) {
|
|
198
|
+
localRenderWindow.delete();
|
|
199
|
+
localRenderWindow = undefined;
|
|
200
|
+
widgetManager = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function initLocalWidget(container) {
|
|
205
|
+
cleanupLocalWidget();
|
|
206
|
+
container.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true });
|
|
207
|
+
localRenderWindow = vtkGenericRenderWindow({
|
|
208
|
+
background: [0, 0, 0, 0],
|
|
209
|
+
listenWindowResize: false,
|
|
210
|
+
});
|
|
211
|
+
localRenderWindow.setContainer(container);
|
|
212
|
+
const camera = localRenderWindow.getRenderer().getActiveCamera();
|
|
213
|
+
camera.onModified(() => limitCameraZoomOut(camera));
|
|
214
|
+
const canvas = localRenderWindow.getApiSpecificRenderWindow().getCanvas();
|
|
215
|
+
Object.assign(canvas.style, { width: "100%", height: "100%", background: "transparent" });
|
|
216
|
+
localRenderWindow.resize();
|
|
217
|
+
widgetManager = vtkWidgetManager();
|
|
218
|
+
widgetManager.setRenderer(localRenderWindow.getRenderer());
|
|
219
|
+
updateWidgetPlacement();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function updateWidgetPlacement({ isReset = false } = {}) {
|
|
223
|
+
if (!widgetManager || !localRenderWindow) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (isReset) {
|
|
227
|
+
maxDistance = 0;
|
|
228
|
+
}
|
|
229
|
+
const center = getSceneCenter();
|
|
230
|
+
for (const plane of planes.value) {
|
|
231
|
+
if (!plane.origin || isReset) {
|
|
232
|
+
plane.origin = [...center];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
syncWidgets();
|
|
236
|
+
syncLocalCamera();
|
|
237
|
+
if (maxDistance <= 0) {
|
|
238
|
+
maxDistance = localRenderWindow.getRenderer().getActiveCamera().getDistance();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isFromWidget() {
|
|
243
|
+
return fromWidget;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setFromWidget(value) {
|
|
247
|
+
fromWidget = value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
getSceneCenter,
|
|
252
|
+
syncWidgets,
|
|
253
|
+
syncLocalCamera,
|
|
254
|
+
cleanupLocalWidget,
|
|
255
|
+
initLocalWidget,
|
|
256
|
+
updateWidgetPlacement,
|
|
257
|
+
isFromWidget,
|
|
258
|
+
setFromWidget,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { useClippingPlanesWidget };
|
|
@@ -137,6 +137,13 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
async function setClippingPlanes(ids, planes) {
|
|
141
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes;
|
|
142
|
+
const params = { ids, planes };
|
|
143
|
+
await viewerStore.request({ schema, params });
|
|
144
|
+
await remoteRender();
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
function resetCamera() {
|
|
141
148
|
genericRenderWindow.value.getRenderer().resetCamera();
|
|
142
149
|
genericRenderWindow.value.getRenderWindow().render();
|
|
@@ -307,6 +314,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
307
314
|
removeItem,
|
|
308
315
|
setVisibility,
|
|
309
316
|
setZScaling,
|
|
317
|
+
setClippingPlanes,
|
|
310
318
|
syncRemoteCamera,
|
|
311
319
|
setCamera,
|
|
312
320
|
initHybridViewer,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const AXIS_SCALE = 0.45;
|
|
2
|
+
const SIZE_RATIO = 0.1;
|
|
3
|
+
const DEBOUNCE_DELAY = 200;
|
|
4
|
+
const CHANGE_THRESHOLD = 1e-4;
|
|
5
|
+
// oxlint-disable no-magic-numbers
|
|
6
|
+
const COLOR_BLUE = [0.12, 0.53, 0.9];
|
|
7
|
+
const COLOR_GREEN = [0.26, 0.63, 0.28];
|
|
8
|
+
const COLOR_ORANGE = [0.98, 0.55, 0];
|
|
9
|
+
const COLOR_PURPLE = [0.55, 0.14, 0.67];
|
|
10
|
+
const COLOR_RED = [0.9, 0.22, 0.21];
|
|
11
|
+
// oxlint-enable no-magic-numbers
|
|
12
|
+
const PLANE_COLORS = [COLOR_BLUE, COLOR_GREEN, COLOR_ORANGE, COLOR_PURPLE, COLOR_RED];
|
|
13
|
+
const NORMAL_X = [1, 0, 0];
|
|
14
|
+
const NORMAL_Y = [0, 1, 0];
|
|
15
|
+
const NORMAL_Z = [0, 0, 1];
|
|
16
|
+
const DEFAULT_NORMALS = [NORMAL_X, NORMAL_Y, NORMAL_Z];
|
|
17
|
+
const RGB_MAX_VALUE = 255;
|
|
18
|
+
|
|
19
|
+
function getPlaneCssColor(index) {
|
|
20
|
+
const rgb = PLANE_COLORS[index % PLANE_COLORS.length];
|
|
21
|
+
return `rgb(${rgb.map((channel) => Math.round(channel * RGB_MAX_VALUE)).join(",")})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasPlaneChanged(origin, normal, currentOrigin, currentNormal) {
|
|
25
|
+
if (!currentOrigin) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
origin.some((val, idx) => Math.abs(val - currentOrigin[idx]) > CHANGE_THRESHOLD) ||
|
|
30
|
+
normal.some((val, idx) => Math.abs(val - currentNormal[idx]) > CHANGE_THRESHOLD)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getPlaneStyle(rgb) {
|
|
35
|
+
return {
|
|
36
|
+
active: {
|
|
37
|
+
plane: { opacity: 1, color: rgb },
|
|
38
|
+
normal: { opacity: 1, color: rgb },
|
|
39
|
+
origin: { opacity: 1, color: rgb },
|
|
40
|
+
},
|
|
41
|
+
inactive: {
|
|
42
|
+
plane: { opacity: 0.5, color: rgb },
|
|
43
|
+
normal: { opacity: 1, color: rgb },
|
|
44
|
+
origin: { opacity: 1, color: rgb },
|
|
45
|
+
},
|
|
46
|
+
static: {
|
|
47
|
+
display2D: { representation: 0 },
|
|
48
|
+
outline: { color: [1, 1, 1], opacity: 1, representation: 1, interpolation: 0 },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function computeSceneBounds(actors) {
|
|
54
|
+
let bounds = [Infinity, -Infinity, Infinity, -Infinity, Infinity, -Infinity];
|
|
55
|
+
for (const actor of actors) {
|
|
56
|
+
const boundsOfActor = actor.getBounds();
|
|
57
|
+
bounds = [
|
|
58
|
+
Math.min(bounds[0], boundsOfActor[0]),
|
|
59
|
+
Math.max(bounds[1], boundsOfActor[1]),
|
|
60
|
+
Math.min(bounds[2], boundsOfActor[2]),
|
|
61
|
+
Math.max(bounds[3], boundsOfActor[3]),
|
|
62
|
+
Math.min(bounds[4], boundsOfActor[4]),
|
|
63
|
+
Math.max(bounds[5], boundsOfActor[5]),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
return bounds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function computeSceneBoundsInfo(actors) {
|
|
70
|
+
if (!actors || actors.length === 0) {
|
|
71
|
+
return { center: [0, 0, 0], cubicBounds: [-1, 1, -1, 1, -1, 1] };
|
|
72
|
+
}
|
|
73
|
+
const [xmin, xmax, ymin, ymax, zmin, zmax] = computeSceneBounds(actors);
|
|
74
|
+
const center = [
|
|
75
|
+
Number(((xmin + xmax) / 2).toFixed(4)),
|
|
76
|
+
Number(((ymin + ymax) / 2).toFixed(4)),
|
|
77
|
+
Number(((zmin + zmax) / 2).toFixed(4)),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const maxActorExtent = Math.max(
|
|
81
|
+
...actors.map((actor) => {
|
|
82
|
+
const bounds = actor.getBounds();
|
|
83
|
+
return Math.max(bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]);
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
const halfExtent = maxActorExtent / 2;
|
|
87
|
+
const cubicBounds = [
|
|
88
|
+
center[0] - halfExtent,
|
|
89
|
+
center[0] + halfExtent,
|
|
90
|
+
center[1] - halfExtent,
|
|
91
|
+
center[1] + halfExtent,
|
|
92
|
+
center[2] - halfExtent,
|
|
93
|
+
center[2] + halfExtent,
|
|
94
|
+
];
|
|
95
|
+
return { center, cubicBounds };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
AXIS_SCALE,
|
|
100
|
+
SIZE_RATIO,
|
|
101
|
+
DEBOUNCE_DELAY,
|
|
102
|
+
CHANGE_THRESHOLD,
|
|
103
|
+
PLANE_COLORS,
|
|
104
|
+
DEFAULT_NORMALS,
|
|
105
|
+
getPlaneCssColor,
|
|
106
|
+
hasPlaneChanged,
|
|
107
|
+
getPlaneStyle,
|
|
108
|
+
computeSceneBounds,
|
|
109
|
+
computeSceneBoundsInfo,
|
|
110
|
+
};
|
package/package.json
CHANGED