Initial commit: 3D Viewer application
Features: - Vue 3 frontend with Three.js/Online3DViewer - Node.js API with PostgreSQL and Redis - Python worker for model conversion - Docker Compose for deployment - ViewCube navigation with drag rotation and 90° snap - Cross-section, exploded view, and render settings - Parts tree with visibility controls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
102
frontend/src/components/layout/AppLayout.vue
Normal file
102
frontend/src/components/layout/AppLayout.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import SidebarPanel from './SidebarPanel.vue'
|
||||
import ViewerPanel from './ViewerPanel.vue'
|
||||
import PartsTreePanel from '@/components/partsTree/PartsTreePanel.vue'
|
||||
import { usePartsTreeStore } from '@/stores/partsTree'
|
||||
import { useViewerStore } from '@/stores/viewer'
|
||||
|
||||
const partsTreeStore = usePartsTreeStore()
|
||||
const viewerStore = useViewerStore()
|
||||
|
||||
const isTreePanelCollapsed = ref(false)
|
||||
const treePanelWidth = ref(280)
|
||||
|
||||
// Build tree when model loads - must be here so it runs before conditional render
|
||||
watch(
|
||||
() => viewerStore.model,
|
||||
(model) => {
|
||||
if (model && viewerStore.scene) {
|
||||
partsTreeStore.buildTree()
|
||||
} else {
|
||||
partsTreeStore.reset()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const treePanelStyle = computed(() => ({
|
||||
width: isTreePanelCollapsed.value ? '0px' : `${treePanelWidth.value}px`,
|
||||
minWidth: isTreePanelCollapsed.value ? '0px' : `${treePanelWidth.value}px`,
|
||||
}))
|
||||
|
||||
function toggleTreePanel() {
|
||||
isTreePanelCollapsed.value = !isTreePanelCollapsed.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<SidebarPanel />
|
||||
<ViewerPanel />
|
||||
<div
|
||||
v-if="partsTreeStore.hasTree"
|
||||
class="tree-panel-wrapper"
|
||||
:style="treePanelStyle"
|
||||
>
|
||||
<button
|
||||
class="tree-panel-toggle"
|
||||
:class="{ collapsed: isTreePanelCollapsed }"
|
||||
@click="toggleTreePanel"
|
||||
:title="isTreePanelCollapsed ? 'Show Parts Tree' : 'Hide Parts Tree'"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<PartsTreePanel v-show="!isTreePanelCollapsed" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-panel-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0 !important;
|
||||
transition: width 0.2s, min-width 0.2s;
|
||||
/* Remove overflow: hidden to allow toggle button to be visible */
|
||||
}
|
||||
|
||||
.tree-panel-toggle {
|
||||
position: absolute;
|
||||
left: -1.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1.5rem;
|
||||
height: 3rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-right: none;
|
||||
border-radius: 0.375rem 0 0 0.375rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tree-panel-toggle:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.tree-panel-toggle svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--text-secondary);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.tree-panel-toggle.collapsed svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user