Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/assets/viewer_svgs/camera-bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion app/components/ActionButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ const emit = defineEmits(["click"]);
icon
@click="emit('click', $event)"
>
<v-icon :size="iconSize">{{ icon }}</v-icon>
<v-icon v-if="typeof icon === 'string' && icon.startsWith('mdi-')" :size="iconSize">{{
icon
}}</v-icon>
<v-img
v-else
:src="icon"
:height="iconSize"
:width="iconSize"
class="d-flex justify-center align-center"
/>
</v-btn>
</template>
221 changes: 221 additions & 0 deletions app/components/CameraManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<script setup>
import GlassCard from "@ogw_front/components/GlassCard";
import { useCameraManagerStore } from "@ogw_front/stores/camera_manager";
import { useDataStore } from "@ogw_front/stores/data";
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";

const emit = defineEmits(["close"]);

const { show_dialog, width } = defineProps({
show_dialog: { type: Boolean, required: true },
width: { type: Number, required: false, default: 450 },
});

const cameraManagerStore = useCameraManagerStore();
const dataStore = useDataStore();
const hybridViewerStore = useHybridViewerStore();

const savedPositions = cameraManagerStore.refAllCameraPositions();
const allObjects = dataStore.refAllItems();

const newPositionName = ref("");
const selectedObjectId = ref(undefined);

const editingId = ref(undefined);
const editingName = ref("");

async function saveCurrentPosition() {
if (!newPositionName.value) {
return;
}
await cameraManagerStore.saveCameraPosition(
newPositionName.value,
toRaw(hybridViewerStore.camera_options),
selectedObjectId.value,
);
newPositionName.value = "";
selectedObjectId.value = undefined;
}

async function restorePosition(positionId) {
const position = await cameraManagerStore.getCameraPosition(positionId);
if (position) {
if (hybridViewerStore.genericRenderWindow) {
hybridViewerStore.setCamera(position.camera_options);
} else {
await cameraManagerStore.restoreCameraPosition(positionId);
}
}
}

async function deletePosition(positionId) {
await cameraManagerStore.deleteCameraPosition(positionId);
}

function startEditing(position) {
editingId.value = position.id;
editingName.value = position.name;
}

async function saveRename() {
if (editingName.value) {
await cameraManagerStore.renameCameraPosition(editingId.value, editingName.value);
}
editingId.value = undefined;
}

function getObjectName(objectId) {
if (!objectId) {
return "None";
}
const obj = allObjects.value.find((object) => object.id === objectId);
return obj ? obj.name : "Unknown";
}
</script>

<template>
<GlassCard
v-if="show_dialog"
@click.stop
title="Camera Positions"
:width="width"
:ripple="false"
variant="panel"
padding="pa-0"
class="position-absolute elevation-24"
style="z-index: 2; top: 90px; right: 55px"
>
<v-card-text class="pa-0">
<!-- Save Current Position Section -->
<v-container class="pa-5 pb-2 bg-surface-variant-lighten-5">
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="newPositionName"
label="Position Name"
placeholder="e.g. Front View"
density="compact"
variant="outlined"
hide-details
class="mb-3"
></v-text-field>
</v-col>
<v-col cols="8">
<v-select
v-model="selectedObjectId"
:items="allObjects"
item-title="name"
item-value="id"
label="Attach to Object"
placeholder="Optional"
density="compact"
variant="outlined"
hide-details
clearable
></v-select>
</v-col>
<v-col cols="4" class="d-flex align-center">
<v-btn
color="primary"
variant="elevated"
block
:disabled="!newPositionName"
@click="saveCurrentPosition"
height="40"
>
Save
</v-btn>
</v-col>
</v-row>
</v-container>

<v-divider></v-divider>

<!-- Saved Positions List -->
<v-list v-if="savedPositions.length > 0" class="bg-transparent pa-2" lines="two">
<v-list-item
v-for="position in savedPositions"
:key="position.id"
class="rounded-lg mb-1"
:active="editingId === position.id"
active-color="primary"
>
<template #prepend>
<v-btn
icon
variant="tonal"
color="success"
size="small"
class="mr-2"
@click="restorePosition(position.id)"
>
<v-icon size="20">mdi-play</v-icon>
<v-tooltip activator="parent" location="top">Restore</v-tooltip>
</v-btn>
</template>

<v-list-item-title class="font-weight-bold">
<v-text-field
v-if="editingId === position.id"
v-model="editingName"
density="compact"
variant="underlined"
hide-details
autofocus
@keyup.enter="saveRename"
@blur="saveRename"
></v-text-field>
<span v-else>{{ position.name }}</span>
</v-list-item-title>

<v-list-item-subtitle v-if="editingId !== position.id">
<v-icon size="14" class="mr-1">mdi-paperclip</v-icon>
{{ getObjectName(position.object_id) }}
</v-list-item-subtitle>

<template #append>
<div class="d-flex g-1">
<v-btn
icon
variant="text"
size="small"
color="grey-darken-1"
@click="startEditing(position)"
>
<v-icon size="18">mdi-pencil</v-icon>
<v-tooltip activator="parent" location="top">Rename</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
size="small"
color="error"
@click="deletePosition(position.id)"
>
<v-icon size="18">mdi-delete</v-icon>
<v-tooltip activator="parent" location="top">Delete</v-tooltip>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
<div v-else class="text-center text-grey-lighten-1 py-8 italic">
<v-icon size="48" class="mb-2 d-block mx-auto opacity-20">mdi-camera-off</v-icon>
No saved positions yet.
</div>
</v-card-text>

<v-divider></v-divider>

<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" color="grey-darken-1" @click="emit('close')">Close</v-btn>
</v-card-actions>
</GlassCard>
</template>

<style scoped>
:deep(.v-list-item__prepend) {
margin-inline-end: 12px !important;
}
</style>
17 changes: 14 additions & 3 deletions app/components/ViewToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<script setup>
import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";

import ActionButton from "@ogw_front/components/ActionButton.vue";
import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.svg";
import CameraManager from "@ogw_front/components/CameraManager";
import CameraOrientation from "@ogw_front/components/CameraOrientation.vue";
import Screenshot from "@ogw_front/components/Screenshot";
import ZScaling from "@ogw_front/components/ZScaling";

import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
import { useViewerStore } from "@ogw_front/stores/viewer";

const hybridViewerStore = useHybridViewerStore();
const viewerStore = useViewerStore();
const take_screenshot = ref(false);
const show_camera_manager = ref(false);
const showCameraOrientation = ref(false);
const showZScaling = ref(false);
const grid_scale = ref(false);
Expand Down Expand Up @@ -44,6 +45,14 @@ const camera_options = [
showCameraOrientation.value = !showCameraOrientation.value;
},
},
{
tooltip: "Manage camera positions",
icon: CameraBookmarkIcon,
iconSize: 34,
action: () => {
show_camera_manager.value = !show_camera_manager.value;
},
},
{
tooltip: "Take a screenshot",
icon: "mdi-camera",
Expand Down Expand Up @@ -84,6 +93,7 @@ const camera_options = [
<ActionButton
:icon="camera_option.icon"
:tooltip="camera_option.tooltip"
:icon-size="camera_option.iconSize"
tooltip-location="left"
@click.stop="camera_option.action"
/>
Expand All @@ -96,6 +106,7 @@ const camera_options = [
@select="hybridViewerStore.setCameraOrientation"
/>
<Screenshot v-model="take_screenshot" />
<CameraManager :show_dialog="show_camera_manager" @close="show_camera_manager = false" />
<ZScaling
v-model:show="showZScaling"
v-model="zScale"
Expand Down
57 changes: 57 additions & 0 deletions app/stores/camera_manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Third party imports
import { liveQuery } from "dexie";
import { useObservable } from "@vueuse/rxjs";
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";

// Local imports
import { database } from "@ogw_internal/database/database.js";
import { useViewerStore } from "@ogw_front/stores/viewer";

export const useCameraManagerStore = defineStore("camera_manager", () => {
const viewerStore = useViewerStore();

function refAllCameraPositions() {
return useObservable(
liveQuery(() => database.camera_positions.toArray()),
{ initialValue: [] },
);
}

async function getCameraPosition(id) {
return await database.camera_positions.get(id);
}

async function saveCameraPosition(name, camera_options, object_id = undefined) {
await database.camera_positions.put({
name,
object_id,
camera_options,
});
}

async function restoreCameraPosition(id) {
const position = await database.camera_positions.get(id);
if (position) {
await viewerStore.request(viewer_schemas.opengeodeweb_viewer.viewer.update_camera, {
camera_options: position.camera_options,
});
}
}

async function deleteCameraPosition(id) {
await database.camera_positions.delete(id);
}

async function renameCameraPosition(id, newName) {
await database.camera_positions.update(id, { name: newName });
}

return {
refAllCameraPositions,
getCameraPosition,
saveCameraPosition,
restoreCameraPosition,
deleteCameraPosition,
renameCameraPosition,
};
});
Loading
Loading