@geode/opengeodeweb-front 10.41.1-rc.1 → 10.42.0-rc.2
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/FileUploader.vue +1 -1
- package/app/components/Screenshot.vue +148 -25
- package/app/components/Viewer/Options/TextureItem.vue +4 -3
- package/app/utils/default_styles/meshes.ts +6 -8
- package/internal/stores/data_style/mesh/cells/index.ts +6 -4
- package/internal/stores/data_style/mesh/polygons/index.ts +6 -4
- package/internal/stores/hybrid_viewer/scene.ts +23 -2
- package/package.json +1 -1
- package/server/api/microservice/app/set_is_app_ready.post.ts +0 -4
|
@@ -176,7 +176,7 @@ watch(
|
|
|
176
176
|
@folders-ignored="warnFoldersIgnored"
|
|
177
177
|
/>
|
|
178
178
|
|
|
179
|
-
<v-card-text v-if="internal_files.length" class="mt-6 pa-0">
|
|
179
|
+
<v-card-text v-if="!mini && internal_files.length" class="mt-6 pa-0">
|
|
180
180
|
<v-sheet class="d-flex align-center mb-4" color="transparent">
|
|
181
181
|
<v-icon icon="mdi-file-check" class="mr-3" color="primary" size="24" />
|
|
182
182
|
<span class="text-subtitle-1 font-weight-bold text-white"> Selected files </span>
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
import viewer_schemas, {
|
|
3
3
|
type ViewerTakeScreenshotParams,
|
|
4
4
|
} from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";
|
|
5
|
+
import { BACKGROUND_GREY_VALUE } from "@ogw_internal/stores/hybrid_viewer/constants";
|
|
6
|
+
import type { RGBAColor } from "@ogw_front/utils/default_styles/constants";
|
|
5
7
|
import ToolPanel from "@ogw_front/components/ToolPanel.vue";
|
|
8
|
+
import ViewerOptionsColorPicker from "@ogw_front/components/Viewer/Options/ColorPicker.vue";
|
|
6
9
|
import fileDownload from "js-file-download";
|
|
7
10
|
import { useClipboardItems } from "@vueuse/core";
|
|
8
11
|
import { useFeedbackStore } from "@ogw_front/stores/feedback";
|
|
12
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
9
13
|
import { useViewerStore } from "@ogw_front/stores/viewer";
|
|
10
14
|
|
|
11
15
|
const DEFAULT_PANEL_WIDTH = 260;
|
|
@@ -24,14 +28,54 @@ const output_extensions =
|
|
|
24
28
|
const filename = ref<string>("");
|
|
25
29
|
const output_extension = ref<ViewerTakeScreenshotParams["output_extension"]>("png");
|
|
26
30
|
const include_background = ref<boolean>(true);
|
|
31
|
+
const include_ui = ref<boolean>(false);
|
|
27
32
|
const screenshot_type = ref<string>("file");
|
|
33
|
+
const DEFAULT_BACKGROUND_COLOR: RGBAColor = {
|
|
34
|
+
red: BACKGROUND_GREY_VALUE,
|
|
35
|
+
green: BACKGROUND_GREY_VALUE,
|
|
36
|
+
blue: BACKGROUND_GREY_VALUE,
|
|
37
|
+
alpha: 1,
|
|
38
|
+
};
|
|
39
|
+
const background_color = ref<RGBAColor>({ ...DEFAULT_BACKGROUND_COLOR });
|
|
40
|
+
let is_capturing = false;
|
|
28
41
|
|
|
29
42
|
const { copy } = useClipboardItems();
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
const
|
|
44
|
+
function applyBackgroundColor(color: RGBAColor): Promise<void> {
|
|
45
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
46
|
+
// oxlint-disable-next-line id-length -- the viewer schema requires { r, g, b }
|
|
47
|
+
return hybridViewerStore.setBackgroundColor({ r: color.red, g: color.green, b: color.blue });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function restoreBackgroundColor(): Promise<void> {
|
|
51
|
+
background_color.value = { ...DEFAULT_BACKGROUND_COLOR };
|
|
52
|
+
await applyBackgroundColor(DEFAULT_BACKGROUND_COLOR);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function deliverScreenshot(blob: Blob | Uint8Array, current_filename: string): Promise<void> {
|
|
33
56
|
const feedbackStore = useFeedbackStore();
|
|
34
|
-
|
|
57
|
+
if (screenshot_type.value === "file") {
|
|
58
|
+
fileDownload(blob, `${current_filename}.${output_extension.value}`);
|
|
59
|
+
feedbackStore.add_success("Screenshot downloaded");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const pngBlob = new Blob([blob], { type: "image/png" });
|
|
64
|
+
await copy([new ClipboardItem({ "image/png": pngBlob })]);
|
|
65
|
+
feedbackStore.add_success("Screenshot copied to clipboard");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
feedbackStore.add_error(
|
|
69
|
+
0,
|
|
70
|
+
"",
|
|
71
|
+
"Clipboard Error",
|
|
72
|
+
`Failed to copy screenshot to clipboard: ${message}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function takeSceneScreenshot(current_filename: string): Promise<void> {
|
|
78
|
+
const viewerStore = useViewerStore();
|
|
35
79
|
const schema = viewer_schemas.opengeodeweb_viewer.viewer.take_screenshot;
|
|
36
80
|
const params = {
|
|
37
81
|
filename: current_filename,
|
|
@@ -46,32 +90,92 @@ async function takeScreenshot(): Promise<void> {
|
|
|
46
90
|
{
|
|
47
91
|
// `blob` is the image file content, received as binary (Uint8Array) through wslink
|
|
48
92
|
response_function: async ({ blob }) => {
|
|
49
|
-
|
|
50
|
-
fileDownload(blob, `${current_filename}.${output_extension.value}`);
|
|
51
|
-
feedbackStore.add_success("Screenshot downloaded");
|
|
52
|
-
} else {
|
|
53
|
-
try {
|
|
54
|
-
const pngBlob = new Blob([blob], { type: "image/png" });
|
|
55
|
-
await copy([new ClipboardItem({ "image/png": pngBlob })]);
|
|
56
|
-
feedbackStore.add_success("Screenshot copied to clipboard");
|
|
57
|
-
} catch (error) {
|
|
58
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
-
feedbackStore.add_error(
|
|
60
|
-
0,
|
|
61
|
-
"",
|
|
62
|
-
"Clipboard Error",
|
|
63
|
-
`Failed to copy screenshot to clipboard: ${message}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
93
|
+
await deliverScreenshot(blob, current_filename);
|
|
67
94
|
},
|
|
68
95
|
},
|
|
69
96
|
);
|
|
70
|
-
show.value = false;
|
|
71
97
|
}
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
// `preferCurrentTab` is not yet part of the TypeScript DOM lib
|
|
100
|
+
interface TabCaptureOptions extends DisplayMediaStreamOptions {
|
|
101
|
+
preferCurrentTab: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Captures the whole browser tab (scene and web UI) through the native screen capture API
|
|
105
|
+
async function captureTab(mime_type: string): Promise<Blob> {
|
|
106
|
+
const options: TabCaptureOptions = { video: true, audio: false, preferCurrentTab: true };
|
|
107
|
+
const stream = await navigator.mediaDevices.getDisplayMedia(options);
|
|
108
|
+
try {
|
|
109
|
+
const video = document.createElement("video");
|
|
110
|
+
video.srcObject = stream;
|
|
111
|
+
video.muted = true;
|
|
112
|
+
await video.play();
|
|
113
|
+
const canvas = document.createElement("canvas");
|
|
114
|
+
canvas.width = video.videoWidth;
|
|
115
|
+
canvas.height = video.videoHeight;
|
|
116
|
+
canvas.getContext("2d")?.drawImage(video, 0, 0);
|
|
117
|
+
// oxlint-disable-next-line promise/avoid-new
|
|
118
|
+
return await new Promise((resolve, reject) => {
|
|
119
|
+
canvas.toBlob((blob) => {
|
|
120
|
+
if (blob) {
|
|
121
|
+
resolve(blob);
|
|
122
|
+
} else {
|
|
123
|
+
reject(new Error("Failed to encode the captured frame"));
|
|
124
|
+
}
|
|
125
|
+
}, mime_type);
|
|
126
|
+
});
|
|
127
|
+
} finally {
|
|
128
|
+
for (const track of stream.getTracks()) {
|
|
129
|
+
track.stop();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function takeUiScreenshot(current_filename: string): Promise<void> {
|
|
135
|
+
const feedbackStore = useFeedbackStore();
|
|
136
|
+
const mime_type = output_extension.value === "jpg" ? "image/jpeg" : "image/png";
|
|
137
|
+
try {
|
|
138
|
+
const blob = await captureTab(mime_type);
|
|
139
|
+
await deliverScreenshot(blob, current_filename);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
feedbackStore.add_error(0, "", "Screenshot Error", `Failed to capture the page: ${message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function takeScreenshot(): Promise<void> {
|
|
147
|
+
const current_filename = screenshot_type.value === "file" ? filename.value : "screenshot";
|
|
148
|
+
is_capturing = true;
|
|
149
|
+
try {
|
|
150
|
+
if (include_ui.value) {
|
|
151
|
+
// Close the panel first so it does not appear on the captured page
|
|
152
|
+
show.value = false;
|
|
153
|
+
await nextTick();
|
|
154
|
+
await takeUiScreenshot(current_filename);
|
|
155
|
+
} else {
|
|
156
|
+
await takeSceneScreenshot(current_filename);
|
|
157
|
+
show.value = false;
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
await restoreBackgroundColor();
|
|
161
|
+
is_capturing = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
watch(background_color, async (color) => {
|
|
166
|
+
if (show.value) {
|
|
167
|
+
await applyBackgroundColor(color);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watch(show, async (value) => {
|
|
172
|
+
if (!value && !is_capturing) {
|
|
173
|
+
await restoreBackgroundColor();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
watch([output_extension, include_ui], ([extension, with_ui]) => {
|
|
178
|
+
if (extension !== "png" || with_ui) {
|
|
75
179
|
include_background.value = true;
|
|
76
180
|
}
|
|
77
181
|
});
|
|
@@ -161,11 +265,22 @@ function handleClose(): void {
|
|
|
161
265
|
</v-row>
|
|
162
266
|
|
|
163
267
|
<v-row dense>
|
|
268
|
+
<v-col cols="12" class="py-1">
|
|
269
|
+
<v-switch
|
|
270
|
+
data-testid="screenshotIncludeUiSwitch"
|
|
271
|
+
v-model="include_ui"
|
|
272
|
+
label="Include UI"
|
|
273
|
+
density="compact"
|
|
274
|
+
hide-details
|
|
275
|
+
inset
|
|
276
|
+
class="text-caption"
|
|
277
|
+
></v-switch>
|
|
278
|
+
</v-col>
|
|
164
279
|
<v-col cols="12" class="py-1">
|
|
165
280
|
<v-switch
|
|
166
281
|
data-testid="screenshotIncludeBackgroundSwitch"
|
|
167
282
|
v-model="include_background"
|
|
168
|
-
:disabled="screenshot_type === 'file' && output_extension !== 'png'"
|
|
283
|
+
:disabled="(screenshot_type === 'file' && output_extension !== 'png') || include_ui"
|
|
169
284
|
label="Include background"
|
|
170
285
|
density="compact"
|
|
171
286
|
hide-details
|
|
@@ -173,6 +288,14 @@ function handleClose(): void {
|
|
|
173
288
|
class="text-caption"
|
|
174
289
|
></v-switch>
|
|
175
290
|
</v-col>
|
|
291
|
+
<v-col v-if="include_background" cols="12" class="py-1">
|
|
292
|
+
<span class="text-caption">Background color</span>
|
|
293
|
+
<ViewerOptionsColorPicker
|
|
294
|
+
data-testid="screenshotBackgroundColorPicker"
|
|
295
|
+
v-model="background_color"
|
|
296
|
+
disabled-alpha
|
|
297
|
+
/>
|
|
298
|
+
</v-col>
|
|
176
299
|
</v-row>
|
|
177
300
|
</v-container>
|
|
178
301
|
|
|
@@ -95,8 +95,9 @@ watch(textureId, (value) => {
|
|
|
95
95
|
</style>
|
|
96
96
|
|
|
97
97
|
<template>
|
|
98
|
-
<v-col
|
|
98
|
+
<v-col class="pa-1">
|
|
99
99
|
<v-select
|
|
100
|
+
data-testid="textureNameSelector"
|
|
100
101
|
v-model="textureName"
|
|
101
102
|
:items="textureCoordinates"
|
|
102
103
|
label="Select a texture"
|
|
@@ -104,7 +105,7 @@ watch(textureId, (value) => {
|
|
|
104
105
|
hide-details
|
|
105
106
|
/>
|
|
106
107
|
</v-col>
|
|
107
|
-
<v-col cols="
|
|
108
|
+
<v-col cols="auto" class="pa-1 d-flex justify-center align-center">
|
|
108
109
|
<v-badge :model-value="textureId !== ''" color="white" floating dot offset-x="10" offset-y="10">
|
|
109
110
|
<FileUploader
|
|
110
111
|
@files_uploaded="files_uploaded_event($event)"
|
|
@@ -116,7 +117,7 @@ watch(textureId, (value) => {
|
|
|
116
117
|
/>
|
|
117
118
|
</v-badge>
|
|
118
119
|
</v-col>
|
|
119
|
-
<v-col v-if="textureName === '' || textureId === ''" cols="
|
|
120
|
+
<v-col v-if="textureName === '' || textureId === ''" cols="auto">
|
|
120
121
|
<v-icon size="20" icon="mdi-close-circle" v-tooltip:bottom="'Invalid texture'" />
|
|
121
122
|
</v-col>
|
|
122
123
|
</template>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MeshApplyTexturesParams } from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type AttributeConfig,
|
|
3
5
|
CELLS_DEFAULT_COLOR,
|
|
@@ -32,13 +34,11 @@ interface MeshEdgesColoring extends MeshComponentColoring {
|
|
|
32
34
|
|
|
33
35
|
interface MeshCellsColoring extends MeshComponentColoring {
|
|
34
36
|
cell: AttributeConfig;
|
|
35
|
-
|
|
36
|
-
textures: null;
|
|
37
|
+
textures: MeshApplyTexturesParams["textures"];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface MeshPolygonsColoring extends MeshComponentColoring {
|
|
40
|
-
|
|
41
|
-
textures: null;
|
|
41
|
+
textures: MeshApplyTexturesParams["textures"];
|
|
42
42
|
polygon: AttributeConfig;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -157,8 +157,7 @@ function meshCellsDefaultStyle(
|
|
|
157
157
|
active: MESH_DEFAULT_ACTIVE_COLORING,
|
|
158
158
|
cell: emptyAttributeConfig(),
|
|
159
159
|
constant,
|
|
160
|
-
|
|
161
|
-
textures: null,
|
|
160
|
+
textures: [],
|
|
162
161
|
vertex: emptyAttributeConfig(),
|
|
163
162
|
},
|
|
164
163
|
};
|
|
@@ -173,8 +172,7 @@ function meshPolygonsDefaultStyle(
|
|
|
173
172
|
coloring: {
|
|
174
173
|
active: MESH_DEFAULT_ACTIVE_COLORING,
|
|
175
174
|
constant,
|
|
176
|
-
|
|
177
|
-
textures: null,
|
|
175
|
+
textures: [],
|
|
178
176
|
polygon: emptyAttributeConfig(),
|
|
179
177
|
vertex: emptyAttributeConfig(),
|
|
180
178
|
},
|
|
@@ -68,10 +68,12 @@ async function setMeshCellsActiveColoring(
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
if (type === "textures") {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
const textures = deps.texturesStyle.meshCellsTextures(id);
|
|
72
|
+
// The viewer requires at least one texture: nothing to apply until one is picked.
|
|
73
|
+
if (textures === undefined || textures.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const result = await deps.texturesStyle.setMeshCellsTextures(id, textures);
|
|
75
77
|
return result;
|
|
76
78
|
}
|
|
77
79
|
if (type === "vertex") {
|
|
@@ -78,10 +78,12 @@ async function setMeshPolygonsActiveColoring(
|
|
|
78
78
|
return result;
|
|
79
79
|
}
|
|
80
80
|
if (type === "textures") {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const textures = deps.texturesStyle.meshPolygonsTextures(id);
|
|
82
|
+
// The viewer requires at least one texture: nothing to apply until one is picked.
|
|
83
|
+
if (textures === undefined || textures.length === 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const result = await deps.texturesStyle.setMeshPolygonsTextures(id, textures);
|
|
85
87
|
return result;
|
|
86
88
|
}
|
|
87
89
|
if (type === "vertex") {
|
|
@@ -1,15 +1,35 @@
|
|
|
1
|
+
import { ACTOR_COLOR, RGB_MAX } from "./constants";
|
|
1
2
|
import type { HybridDb, vtkActor as VtkActorInstance } from "./vtk_types";
|
|
2
3
|
import { requireRenderWindow, useHybridViewerCore } from "./core";
|
|
3
|
-
import {
|
|
4
|
+
import viewer_schemas, {
|
|
5
|
+
type ViewerSetBackgroundColorParams,
|
|
6
|
+
} from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";
|
|
4
7
|
import { base64ToArrayBuffer } from "./base64";
|
|
5
8
|
import { useDataStore } from "@ogw_front/stores/data";
|
|
6
9
|
import { useViewerStore } from "@ogw_front/stores/viewer";
|
|
7
|
-
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";
|
|
8
10
|
import { newInstance as vtkActor } from "@kitware/vtk.js/Rendering/Core/Actor";
|
|
9
11
|
import { newInstance as vtkMapper } from "@kitware/vtk.js/Rendering/Core/Mapper";
|
|
10
12
|
import type vtkPolyData from "@kitware/vtk.js/Common/DataModel/PolyData";
|
|
11
13
|
import { newInstance as vtkXMLPolyDataReader } from "@kitware/vtk.js/IO/XML/XMLPolyDataReader";
|
|
12
14
|
|
|
15
|
+
async function setBackgroundColor(color: ViewerSetBackgroundColorParams["color"]): Promise<void> {
|
|
16
|
+
const { genericRenderWindow, remoteRender } = useHybridViewerCore();
|
|
17
|
+
const renderer = requireRenderWindow(genericRenderWindow).getRenderer();
|
|
18
|
+
renderer.setBackground([color.r / RGB_MAX, color.g / RGB_MAX, color.b / RGB_MAX]);
|
|
19
|
+
const renderWindow = requireRenderWindow(genericRenderWindow).getRenderWindow();
|
|
20
|
+
renderWindow.render();
|
|
21
|
+
const viewerStore = useViewerStore();
|
|
22
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.set_background_color;
|
|
23
|
+
const params = {
|
|
24
|
+
color,
|
|
25
|
+
};
|
|
26
|
+
await viewerStore.request({
|
|
27
|
+
schema,
|
|
28
|
+
params,
|
|
29
|
+
});
|
|
30
|
+
await remoteRender();
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
// Shared via createSharedComposable (rather than merged into the parent hybridViewer store) so sibling slices, e.g. camera.ts and ruler.ts, can read hybridDb/setZScaling directly without importing the parent store and creating a cycle. A Pinia store would work too but its $id/$patch/... properties would leak into the composed store's spread and collapse its inferred type.
|
|
14
34
|
const useHybridViewerScene = createSharedComposable(() => {
|
|
15
35
|
const hybridDb = reactive<HybridDb>({});
|
|
@@ -122,6 +142,7 @@ const useHybridViewerScene = createSharedComposable(() => {
|
|
|
122
142
|
removeItem,
|
|
123
143
|
setVisibility,
|
|
124
144
|
setZScaling,
|
|
145
|
+
setBackgroundColor,
|
|
125
146
|
clear,
|
|
126
147
|
};
|
|
127
148
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Third party imports
|
|
2
2
|
import { consola } from "consola";
|
|
3
|
-
import { createError } from "h3";
|
|
4
3
|
|
|
5
4
|
// Local imports
|
|
6
5
|
import { setIsAppReady } from "@geode/opengeodeweb-front/server/utils/server_config.ts";
|
|
@@ -11,9 +10,6 @@ import schemas from "@geode/opengeodeweb-front/opengeodeweb_front_typed_schemas.
|
|
|
11
10
|
export default defineTypedEventHandler(
|
|
12
11
|
schemas.api.microservice.app.set_is_app_ready,
|
|
13
12
|
({ isReady }) => {
|
|
14
|
-
if (!isReady) {
|
|
15
|
-
throw createError({ statusCode: 400, statusMessage: "isReady is required" });
|
|
16
|
-
}
|
|
17
13
|
setIsAppReady(isReady);
|
|
18
14
|
consola.info(`Updated IS_APP_READY to ${isReady}`);
|
|
19
15
|
return { statusCode: 200, isReady };
|