Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
59a040e
feat(ContextMenu): New multiple context menu system
SpliiT May 18, 2026
f65c0df
Adaptative background
SpliiT May 18, 2026
c346207
test adapt
SpliiT May 18, 2026
443dee0
card order labels
SpliiT May 19, 2026
4866d3e
clean code
SpliiT May 19, 2026
9031ff5
Apply prepare changes
SpliiT May 19, 2026
3d5f541
oxcgo
SpliiT May 19, 2026
f962cdb
split responsabilities
SpliiT May 19, 2026
7182428
Apply prepare changes
SpliiT May 19, 2026
a8c95a9
rm useless ref
SpliiT May 19, 2026
34d041d
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWe…
SpliiT May 19, 2026
cc67efa
rm useless imports
SpliiT May 19, 2026
7c8bde4
Apply prepare changes
SpliiT May 19, 2026
73583a2
isOverToolbar
SpliiT May 19, 2026
a3857f1
Merge branch 'feat/SelectMenu' of https://github.com/Geode-solutions/…
SpliiT May 19, 2026
999e1ee
oxlint
SpliiT May 19, 2026
3f3aded
Apply prepare changes
SpliiT May 19, 2026
7c1bc45
color button
SpliiT May 19, 2026
c4fcff4
Merge branch 'feat/SelectMenu' of https://github.com/Geode-solutions/…
SpliiT May 19, 2026
6f8d4ef
oxc fix
SpliiT May 19, 2026
437af35
center button
SpliiT May 19, 2026
1c0e474
name troncate
SpliiT May 19, 2026
3f55026
Apply prepare changes
SpliiT May 19, 2026
a2932c1
opacity
SpliiT May 20, 2026
5b19ee5
Merge branch 'feat/SelectMenu' of https://github.com/Geode-solutions/…
SpliiT May 20, 2026
7be4dec
Apply prepare changes
SpliiT May 20, 2026
ed07f72
rename
SpliiT May 20, 2026
377c7e6
Merge branch 'feat/SelectMenu' of https://github.com/Geode-solutions/…
SpliiT May 20, 2026
52d39df
fix projects
SpliiT May 20, 2026
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
245 changes: 243 additions & 2 deletions app/components/Viewer/ContextMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script setup>
import { geode_objects } from "@ogw_front/assets/geode_objects";
import { useAdaptiveStyles } from "@ogw_front/composables/use_adaptive_styles";
import { useEventListener } from "@vueuse/core";
import { useMenuStore } from "@ogw_front/stores/menu";
import { useTreeviewStore } from "@ogw_front/stores/treeview";

const RADIUS = 80;
const MARGIN_OFFSET = 40;
Expand All @@ -13,9 +16,27 @@ const ANGLE_225 = 225;
const ANGLE_315 = 315;
const CLOSE_DELAY = 100;

const COPIED_TIMEOUT = 1500;
const MAX_SHORT_ID_LENGTH = 15;
const ID_SLICE_START = 8;
const ID_SLICE_END_OFFSET = 7;
const TREEVIEW_MARGIN_LEFT = 10;
const TREEVIEW_ICON_WIDTH = 48;
const TREEVIEW_MARGIN_RIGHT = 20;
const ADAPTIVE_BLUR_VAL = "15px";
const ADAPTIVE_OPACITY_VAL = 0.85;
const ADAPTIVE_BRIGHTNESS_VAL = 1.15;
const DRAG_THRESHOLD_VAL = 3;

const menuStore = useMenuStore();

const { id, x, y, containerWidth, containerHeight } = defineProps({
const {
id: propId,
x,
y,
containerWidth,
containerHeight,
} = defineProps({
id: { type: String, required: true },
x: { type: Number, required: true },
y: { type: Number, required: true },
Expand All @@ -25,6 +46,14 @@ const { id, x, y, containerWidth, containerHeight } = defineProps({

const meta_data = computed(() => menuStore.current_meta_data || {});

const cleanName = computed(() => {
const meta = menuStore.current_meta_data;
if (!meta) {
return "Unnamed Object";
}
return meta.name || "Unnamed Object";
});

const show_menu = ref(true);
const isDragging = ref(false);
const dragStartX = ref(0);
Expand Down Expand Up @@ -73,11 +102,77 @@ watch(

const menuItemCount = computed(() => menu_items.value.length);

let dragMoved = false;
const dragThreshold = DRAG_THRESHOLD_VAL;
let dragStartClientX = 0;
let dragStartClientY = 0;
const showName = ref(false);

const copied = ref(false);
async function copyId(targetId) {
Comment thread
SpliiT marked this conversation as resolved.
Outdated
if (!targetId) {
return;
}
try {
await navigator.clipboard.writeText(targetId);
copied.value = true;
setTimeout(() => {
copied.value = false;
}, COPIED_TIMEOUT);
} catch (error) {
console.error("Failed to copy ID:", error);
}
}

const formattedId = computed(() => {
const metaId = meta_data.value?.id;
if (!metaId) {
return "";
}
if (metaId.length <= MAX_SHORT_ID_LENGTH) {
return metaId;
}
return `${metaId.slice(0, ID_SLICE_START)}...${metaId.slice(metaId.length - ID_SLICE_END_OFFSET)}`;
});

const treeviewStore = useTreeviewStore();
const isOverTreeview = computed(() => {
const hasAdditional = treeviewStore.opened_views.some((view) => view.id !== "main");
const hasMain = treeviewStore.opened_views.some((view) => view.id === "main");
const firstColWidth = hasMain ? treeviewStore.panelWidth : 0;
const secondColWidth = hasAdditional ? treeviewStore.additionalPanelWidth : 0;
const treeviewWidth =
TREEVIEW_MARGIN_LEFT +
TREEVIEW_ICON_WIDTH +
firstColWidth +
secondColWidth +
TREEVIEW_MARGIN_RIGHT;
return menuX.value < treeviewWidth;
});

const activatorBtn = ref(undefined);
const { adaptiveStyles } = useAdaptiveStyles(activatorBtn, {
maxOpacity: 0.85,
});

const computedItemStyles = computed(() => {
if (isOverTreeview.value) {
return {
"--adaptive-blur": ADAPTIVE_BLUR_VAL,
"--adaptive-opacity": ADAPTIVE_OPACITY_VAL,
"--adaptive-brightness": ADAPTIVE_BRIGHTNESS_VAL,
};
}
return adaptiveStyles.value;
});

function startDrag(event) {
isDragging.value = true;
dragMoved = false;
dragStartX.value = event.clientX - menuX.value;
dragStartY.value = event.clientY - menuY.value;
event.preventDefault();
dragStartClientX = event.clientX;
dragStartClientY = event.clientY;
}

function clampPosition(posX, posY) {
Expand All @@ -89,6 +184,14 @@ function clampPosition(posX, posY) {
}

function handleDrag(event) {
const deltaX = event.clientX - dragStartClientX;
const deltaY = event.clientY - dragStartClientY;
const distance = Math.hypot(deltaX, deltaY);

if (distance > dragThreshold) {
dragMoved = true;
}

const { x: clampedX, y: clampedY } = clampPosition(
event.clientX - dragStartX.value,
event.clientY - dragStartY.value,
Expand All @@ -104,6 +207,13 @@ function stopDrag(event) {
menuStore.setMenuPosition(menuX.value, menuY.value);
}

function onCenterClick(event) {
event.stopPropagation();
if (!dragMoved) {
showName.value = !showName.value;
}
}

function getMenuStyle() {
return {
position: "fixed",
Expand Down Expand Up @@ -181,6 +291,67 @@ function getItemStyle(index) {
:style="getItemStyle(index)"
@mousedown.stop
/>

<!-- Active Object Central Indicator -->
Comment thread
SpliiT marked this conversation as resolved.
Outdated
<v-btn
ref="activatorBtn"
icon
variant="outlined"
class="central-selector-btn elevation-6"
style="width: 52px; height: 52px; z-index: 5"
:style="computedItemStyles"
@mousedown="startDrag"
@click.stop="onCenterClick"
>
<v-icon
icon="mdi-information-outline"
size="28"
color="primary"
style="pointer-events: none"
/>
</v-btn>

<!-- Direct local name display (no teleportation, no lag!) -->
<transition name="fade-scale">
<div v-if="showName" class="object-name-popover" @mousedown.stop @click.stop>
<GlassCard
variant="panel"
padding="pa-2 px-3"
rounded="lg"
class="elevation-12 text-center border-thin"
min-width="140"
max-width="250"
>
<div
Comment thread
SpliiT marked this conversation as resolved.
Outdated
class="text-subtitle-2 font-weight-bold text-truncate text-white"
style="line-height: 1.3"
>
{{ cleanName }}
</div>
<div
class="text-caption font-weight-black text-uppercase text-secondary"
style="font-size: 0.68rem; line-height: 1.2"
>
{{ meta_data.geode_object_type }}
</div>
<div
v-if="meta_data.id"
class="id-badge-container mt-1 d-inline-flex align-center px-2 py-0.5"
@click.stop="copyId(meta_data.id)"
>
<span class="id-text">
{{ copied ? "COPIED!" : formattedId }}
</span>
<v-icon
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
size="10"
:color="copied ? 'success' : 'white'"
class="ml-1"
/>
</div>
</GlassCard>
</div>
</transition>
</div>
</div>
</v-menu>
Expand All @@ -200,6 +371,8 @@ function getItemStyle(index) {
height: 100%;
border-radius: 50%;
cursor: grab;
user-select: none;
-webkit-user-select: none;
}

.circular-menu-drag-handle:active {
Expand All @@ -221,4 +394,72 @@ function getItemStyle(index) {
transform-origin: center;
will-change: transform, opacity;
}

.central-selector-btn {
background: transparent !important;
border: 2px solid rgba(var(--v-theme-primary), 0.35) !important;
position: relative;
overflow: hidden;
transition: all 0.2s ease;
}

.central-selector-btn::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, var(--adaptive-opacity));
backdrop-filter: blur(var(--adaptive-blur)) brightness(var(--adaptive-brightness));
-webkit-backdrop-filter: blur(var(--adaptive-blur)) brightness(var(--adaptive-brightness));
z-index: 0;
pointer-events: none;
border-radius: inherit;
transition:
background-color 0.2s ease,
backdrop-filter 0.2s ease;
}

.central-selector-btn:hover {
transform: scale(1.08);
border-color: rgba(var(--v-theme-primary), 0.85) !important;
}

.central-selector-btn:hover::before {
background: rgba(255, 255, 255, calc(var(--adaptive-opacity, 0.15) + 0.15));
}

.object-name-popover {
position: absolute;
bottom: 110px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
}

.id-badge-container {
font-family: monospace;
font-size: 0.6rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
opacity: 0.8;
}

.id-badge-container:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.35);
opacity: 1;
transform: scale(1.03);
}

.id-badge-container:active {
transform: scale(0.97);
}

.id-text {
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.85);
}
</style>
Loading
Loading