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
236 changes: 218 additions & 18 deletions src/components/panels/ElementsPanel.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import GenericPanel from './GenericPanel.vue'
import SchemaControls from './ConfigControls.vue'
import ElementItem from './ElementItem.vue'
Expand All @@ -8,7 +8,7 @@ import ElementGroup from './ElementGroup.vue'
import ElementSpawn from './ElementSpawn.vue'
import IconButton from '@/components/IconButton.vue'
import { Button } from '@/components/ui/button'
import { Box, Camera, CheckSquare, Image, Square } from 'lucide-vue-next'
import { Box, Camera, CheckSquare, ChevronDown, ChevronRight, Image, Square } from 'lucide-vue-next'
import type { Component } from 'vue'
import { useDebugSceneStore } from '@/stores/debugScene'
import { useElementPropertiesStore } from '@/stores/elementProperties'
Expand Down Expand Up @@ -102,14 +102,39 @@ interface AddButton {
icon: Component
label: string
title: string
disabled?: boolean
}

const addButtons: AddButton[] = [
{ type: 'camera', icon: Camera, label: 'Camera', title: 'Add Camera' },
{ type: 'mesh', icon: Box, label: 'Mesh', title: 'Add Mesh' },
{ type: 'textureArea', icon: Image, label: 'Texture', title: 'Add Texture Area' }
{
type: 'camera',
icon: Camera,
label: 'New camera',
title: 'Not yet implemented',
disabled: true
},
{ type: 'mesh', icon: Box, label: 'Add model', title: 'Not yet implemented', disabled: true },
{ type: 'textureArea', icon: Image, label: 'Add image', title: 'Load an image for stamping' }
]

const stampGroupId = ref<string | null>(null)
const isListOpen = ref(false)

const toggleStampGroup = (groupId: string) => {
stampGroupId.value = stampGroupId.value === groupId ? null : groupId
textureStore.handlers?.onStampGroupSelect?.(stampGroupId.value)
}

watch(
() => textureStore.groups.length,
() => {
if (stampGroupId.value && !textureStore.groups.some((g) => g.id === stampGroupId.value)) {
stampGroupId.value = null
textureStore.handlers?.onStampGroupSelect?.(null)
}
}
)

const isCameraExpanded = computed(() => {
if (!expandedName.value) return false
if (activeProperties.value?.type === 'camera') return true
Expand Down Expand Up @@ -148,25 +173,26 @@ const hasExpandedSchema = computed(
<GenericPanel panel-type="elements" side="left" title="Elements">
<!-- Add bar -->
<div class="elements-panel__add-bar">
<span class="elements-panel__add-label">Add</span>
<Button
v-for="btn in addButtons"
:key="btn.type"
variant="outline"
size="sm"
class="elements-panel__add-btn"
:title="btn.title"
@click="addElement(btn.type)"
:disabled="btn.disabled"
@click="!btn.disabled && addElement(btn.type)"
>
<component :is="btn.icon" class="h-3 w-3 mr-1" />{{ btn.label }}
<component :is="btn.icon" class="elements-panel__add-icon" />{{ btn.label }}
</Button>
</div>

<!-- Filter bar -->
<div class="elements-panel__filter-bar">
<IconButton
size="sm"
:active="allVisible"
variant="ghost"
:class="{ 'elements-panel__filter-btn--active': allVisible }"
:title="allVisible ? 'Hide all' : 'Show all'"
@click="allVisible ? hideAllCategories() : showAllCategories()"
>
Expand All @@ -178,17 +204,58 @@ const hasExpandedSchema = computed(
v-for="cat in ELEMENT_CATEGORIES"
:key="cat.category"
size="sm"
:active="!hiddenCategories.has(cat.category)"
variant="ghost"
:class="{ 'elements-panel__filter-btn--active': !hiddenCategories.has(cat.category) }"
:title="hiddenCategories.has(cat.category) ? `Show ${cat.label}` : `Hide ${cat.label}`"
@click="toggleCategory(cat.category)"
>
<component :is="cat.icon" />
</IconButton>
</div>

<p v-if="!hasContent" class="elements-panel__empty">No scene elements.</p>
<!-- Image stamp palette -->
<div v-if="textureStore.groups.length > 0" class="elements-panel__palette">
<p class="elements-panel__palette-label">Stamp</p>
<div class="elements-panel__palette-items">
<button
v-for="group in textureStore.groups"
:key="group.id"
class="elements-panel__palette-item"
:class="{ 'elements-panel__palette-item--active': stampGroupId === group.id }"
:title="
stampGroupId === group.id ? `Cancel stamp: ${group.name}` : `Stamp: ${group.name}`
"
@click="toggleStampGroup(group.id)"
>
<img
v-if="group.textures[0]"
:src="group.textures[0].url"
:alt="group.name"
class="elements-panel__palette-thumb"
/>
<span class="elements-panel__palette-name">{{ group.name }}</span>
</button>
</div>
</div>

<div v-else class="elements-panel__list">
<!-- Collapsible element list header -->
<div
class="elements-panel__list-header"
role="button"
tabindex="0"
@click="isListOpen = !isListOpen"
@keydown.enter.space.prevent="isListOpen = !isListOpen"
>
<component
:is="isListOpen ? ChevronDown : ChevronRight"
class="elements-panel__list-chevron"
/>
<span class="elements-panel__list-title">Elements</span>
</div>

<p v-if="isListOpen && !hasContent" class="elements-panel__empty">No scene elements.</p>

<div v-if="isListOpen && hasContent" class="elements-panel__list">
<!-- Ungrouped scene elements -->
<div
v-for="(element, index) in grouped.ungrouped"
Expand All @@ -215,6 +282,19 @@ const hasExpandedSchema = computed(
<p v-if="element.type === 'TextureArea'" class="elements-panel__type-description">
Texture Area
</p>

<!-- Stamp billboard → area toggler -->
<div v-if="element.type === 'Stamp'" class="elements-panel__stamp-convert">
<span class="elements-panel__stamp-convert-label">Billboard</span>
<button
class="elements-panel__stamp-convert-btn"
title="Convert to area distribution"
@click="textureStore.handlers?.onConvertStampToArea?.(element.name)"
>
Convert to area
</button>
</div>

<ElementCamera
v-if="isCameraExpanded"
:is-recording="isRecording"
Expand Down Expand Up @@ -276,19 +356,19 @@ const hasExpandedSchema = computed(
flex-wrap: wrap;
}

.elements-panel__add-label {
font-size: var(--font-size-xs);
color: var(--color-muted-foreground);
flex-shrink: 0;
}

.elements-panel__add-btn {
height: var(--btn-sm-height);
padding: 0 var(--spacing-2);
font-size: var(--font-size-xs);
gap: var(--spacing-1);
}

.elements-panel__add-icon {
width: var(--font-size-sm);
height: var(--font-size-sm);
flex-shrink: 0;
}

.elements-panel__filter-bar {
display: flex;
align-items: center;
Expand All @@ -305,6 +385,126 @@ const hasExpandedSchema = computed(
flex-shrink: 0;
}

.elements-panel__filter-btn--active {
color: var(--color-foreground);
opacity: 1;
}

.elements-panel__filter-bar .icon-btn:not(.elements-panel__filter-btn--active) {
opacity: var(--opacity-muted);
}

.elements-panel__palette {
padding-bottom: var(--spacing-2);
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--spacing-1-5);
}

.elements-panel__palette-label {
font-size: var(--font-size-xs);
color: var(--color-muted-foreground);
margin: 0 0 var(--spacing-1);
}

.elements-panel__palette-items {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-1);
}

.elements-panel__palette-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-0-5);
padding: var(--spacing-1);
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-secondary);
cursor: pointer;
width: 4rem;
}

.elements-panel__palette-item:hover {
border-color: var(--color-muted-foreground);
}

.elements-panel__palette-item--active {
border-color: var(--color-primary);
background: var(--color-muted);
}

.elements-panel__palette-thumb {
width: 3rem;
height: 3rem;
object-fit: cover;
border-radius: var(--radius-xs, 2px);
}

.elements-panel__palette-name {
font-size: var(--font-size-xs);
color: var(--color-muted-foreground);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
text-align: center;
}

.elements-panel__list-header {
display: flex;
align-items: center;
gap: var(--spacing-1);
padding: var(--spacing-1) 0;
cursor: pointer;
user-select: none;
color: var(--color-muted-foreground);
font-size: var(--font-size-xs);
font-weight: 500;
}

.elements-panel__list-header:hover {
color: var(--color-foreground);
}

.elements-panel__list-chevron {
width: var(--font-size-sm);
height: var(--font-size-sm);
flex-shrink: 0;
}

.elements-panel__list-title {
flex: 1;
}

.elements-panel__stamp-convert {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-2);
padding: var(--spacing-1) 0;
}

.elements-panel__stamp-convert-label {
font-size: var(--font-size-xs);
color: var(--color-muted-foreground);
}

.elements-panel__stamp-convert-btn {
font-size: var(--font-size-xs);
padding: var(--spacing-1) var(--spacing-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-secondary);
color: var(--color-foreground);
cursor: pointer;
}

.elements-panel__stamp-convert-btn:hover {
background: var(--color-muted);
border-color: var(--color-primary);
}

.elements-panel__empty {
font-size: var(--font-size-base);
color: var(--color-foreground);
Expand Down
2 changes: 2 additions & 0 deletions src/stores/textureGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface TextureGroupHandlers {
onAddNewGroup: (event: Event) => void
onManualUpdate: () => void
onAddElement: (type: 'camera' | 'mesh') => void
onStampGroupSelect?: (groupId: string | null) => void
onConvertStampToArea?: (stampName: string) => void
}

const DEFAULT_BASE_SIZE = 20
Expand Down
Loading
Loading