Refactor settings panel to compact floating toolbar
Replace the bulky side panel (290-340px wide) with a compact floating toolbar at the bottom center of the viewer, maximizing the model viewing area. New toolbar components: - FloatingToolbar.vue: Main container with all controls - ToolbarButton.vue: Reusable icon button with active state - ToolbarPopup.vue: Reusable popup with click-outside close - RenderModeSelector.vue: 3-segment render mode control - ExplodePopup.vue: Explosion settings popup - SectionPopup.vue: Cross-section settings popup - RenderPopup.vue: Material and lighting settings popup Toolbar features: - Direct controls: Render mode, edge lines toggle, auto-color toggle - Popup controls: Explode (click for animation), section, render settings - Screenshot button with preview modal Removed old components: - FeaturePanel.vue, ExplodedView.vue, CrossSection.vue - RenderSettings.vue, ThumbnailCapture.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
frontend/src/components/viewer/toolbar/ExplodePopup.vue
Normal file
176
frontend/src/components/viewer/toolbar/ExplodePopup.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted, computed } from 'vue'
|
||||
import { useViewerStore } from '@/stores/viewer'
|
||||
import { getExplodeService, resetExplodeService } from '@/services/explodeService'
|
||||
import { getRenderService } from '@/services/renderService'
|
||||
import { getClippingService } from '@/services/clippingService'
|
||||
import { getPartsTreeService } from '@/services/partsTreeService'
|
||||
import ToolbarPopup from './ToolbarPopup.vue'
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const viewerStore = useViewerStore()
|
||||
const isInitialized = ref(false)
|
||||
const partsCount = ref(0)
|
||||
const isAnimating = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
// Initialize when model changes
|
||||
watch(
|
||||
() => viewerStore.model,
|
||||
(model) => {
|
||||
if (model && viewerStore.scene) {
|
||||
const service = getExplodeService()
|
||||
service.initializeFromScene(viewerStore.scene, () => viewerStore.forceRender())
|
||||
isInitialized.value = service.isInitialized()
|
||||
partsCount.value = service.getPartsCount()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Apply explosion when factor changes
|
||||
watch(
|
||||
() => viewerStore.explosionFactor,
|
||||
(factor) => {
|
||||
if (isInitialized.value && !isAnimating.value) {
|
||||
const service = getExplodeService()
|
||||
service.applyExplosion(factor, isDragging.value)
|
||||
viewerStore.forceRender()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
resetExplodeService()
|
||||
})
|
||||
|
||||
function handleSliderInput(event: Event) {
|
||||
isDragging.value = true
|
||||
const target = event.target as HTMLInputElement
|
||||
viewerStore.setExplosionFactor(Number(target.value))
|
||||
}
|
||||
|
||||
function handleSliderChange() {
|
||||
isDragging.value = false
|
||||
finalizeExplosion()
|
||||
}
|
||||
|
||||
function finalizeExplosion() {
|
||||
getRenderService().syncEdgeTransforms()
|
||||
getClippingService().updateBounds()
|
||||
getPartsTreeService().syncSelectionOverlays()
|
||||
viewerStore.forceRender()
|
||||
}
|
||||
|
||||
const canExplode = computed(() => isInitialized.value && partsCount.value >= 2)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToolbarPopup :show="show" title="爆炸图" @close="emit('close')">
|
||||
<div v-if="canExplode" class="explode-content">
|
||||
<div class="slider-row">
|
||||
<span class="label">爆炸系数</span>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
class="slider"
|
||||
min="0"
|
||||
max="100"
|
||||
:value="viewerStore.explosionFactor"
|
||||
@input="handleSliderInput"
|
||||
@change="handleSliderChange"
|
||||
/>
|
||||
<span class="slider-value">{{ viewerStore.explosionFactor }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-text">检测到 {{ partsCount }} 个零件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<span>需要至少2个零件才能使用爆炸图</span>
|
||||
</div>
|
||||
</ToolbarPopup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.explode-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.slider-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
-webkit-appearance: none;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 40px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user