Files
3Dviewer/frontend/src/components/viewer/toolbar/ExplodePopup.vue
likegears 8daf1a601c 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>
2025-12-12 17:51:47 +08:00

177 lines
3.9 KiB
Vue

<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>