mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 22:25:05 +00:00
Replace PrimeVue components in 3D node viewer controls with the
project's Reka UI equivalents across 7 files.
## Changes
| File | Replaced |
|------|---------|
| `AnimationControls.vue` | `Select` × 2 (speed + animation) |
| `ViewerModelControls.vue` | `Select` × 2 (up direction + material
mode) |
| `ViewerCameraControls.vue` | `Select` + `Slider` (camera type + FOV) |
| `ViewerExportControls.vue` | `Select` (export format) |
| `PopupSlider.vue` | `Slider` |
| `ViewerLightControls.vue` | `Slider` |
| `ViewerSceneControls.vue` | `Checkbox` → native `<input
type="checkbox">` |
## Implementation notes
- `Select` uses `@/components/ui/select/*` compound components. Numeric
model values (animation speed index) are stringified at the binding
boundary and converted back on update, matching Reka `SelectRoot`'s
`string`-only `modelValue` contract.
- `Slider` uses `@/components/ui/slider/Slider.vue`. Single-number
`defineModel` values are wrapped in a `computed` array and unwrapped in
the update handler, following the pattern established in
`LightControls.vue`.
- No new Reka UI wrapper components were created — existing ui/select
and ui/slider primitives were used directly.
## Test
https://github.com/user-attachments/assets/afca0fc8-a7b6-49ee-b221-ee5725bd127e
1. AnimationControls.vue
- **Add Load3D node** → Upload an animated GLB file (e.g., a character
model).
- **Node preview top bar:** Play/Pause button, speed dropdown, animation
name dropdown, and progress bar.
2. PopupSlider.vue
- **Hover over Load3D preview:** Icon buttons appear in the left
toolbar.
- **"Light Intensity" button (bulb icon)** → Slider pops up on the
right.
- **"FOV" button (view icon)** → Slider pops up on the right.
3. ViewerCameraControls.vue
- **Load3D node** → Settings panel (top-right) → **"Camera"** tab.
- **Features:** Camera type dropdown (Perspective / Orthographic), FOV
slider (visible in Perspective mode).
4. ViewerExportControls.vue
- **Settings panel** → **"Export"** tab.
- **Features:** Format dropdown (GLB / OBJ / STL), Export button.
5. ViewerLightControls.vue
- **Settings panel** → **"Light"** tab.
- **Features:** Light intensity slider.
6. ViewerModelControls.vue
- **Settings panel** → **"Model"** tab.
- **Features:** "Up direction" dropdown, Material mode dropdown
(Wireframe / Normal, etc.).
7. ViewerSceneControls.vue
- **Settings panel** → **"Scene"** tab.
- **Features:** Background color picker, "Show grid" checkbox, upload
background image button.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> UI component swap touches multiple interactive viewer controls
(selects/sliders/checkbox), so small binding/typing differences (string
vs number, array slider values) could cause subtle regressions despite
test updates.
>
> **Overview**
> Replaces PrimeVue `Select`, `Slider`, and `Checkbox` usages across
Load3D viewer controls with the project’s Reka UI-based primitives
(`@/components/ui/select/*`, `@/components/ui/slider/Slider.vue`) and a
native checkbox.
>
> Updates v-model wiring to match the new components’ contracts: selects
now bind via string `modelValue` with explicit number casting where
needed, and sliders now wrap single numeric values into `[number]`
arrays with corresponding update handlers. Unit tests are updated to
mock the new UI components and their updated event/value shapes.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46f99db256. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12020-refactor-load3d-replace-PrimeVue-Select-Slider-Checkbox-with-Reka-UI-3586d73d365081f58601d93031016afd)
by [Unito](https://www.unito.io)
136 lines
3.8 KiB
Vue
136 lines
3.8 KiB
Vue
<template>
|
|
<div
|
|
v-if="animations && animations.length > 0"
|
|
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full flex-col items-center gap-2 pt-2"
|
|
>
|
|
<div class="flex items-center justify-center gap-2">
|
|
<Button
|
|
size="icon"
|
|
variant="textonly"
|
|
class="rounded-full"
|
|
:aria-label="$t('g.playPause')"
|
|
@click="togglePlay"
|
|
>
|
|
<i
|
|
:class="[
|
|
'pi',
|
|
playing ? 'pi-pause' : 'pi-play',
|
|
'text-lg text-base-foreground'
|
|
]"
|
|
/>
|
|
</Button>
|
|
|
|
<Select
|
|
:model-value="selectedSpeed != null ? String(selectedSpeed) : undefined"
|
|
@update:model-value="(val) => (selectedSpeed = Number(val))"
|
|
>
|
|
<SelectTrigger size="md" class="w-24">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="opt in speedOptions"
|
|
:key="opt.value"
|
|
:value="String(opt.value)"
|
|
>
|
|
{{ opt.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select
|
|
:model-value="
|
|
selectedAnimation != null ? String(selectedAnimation) : undefined
|
|
"
|
|
@update:model-value="(val) => (selectedAnimation = Number(val))"
|
|
>
|
|
<SelectTrigger size="md" class="w-32">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="anim in animations"
|
|
:key="anim.index"
|
|
:value="String(anim.index)"
|
|
>
|
|
{{ anim.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="flex w-full max-w-xs items-center gap-2 px-4">
|
|
<Slider
|
|
:model-value="[animationProgress]"
|
|
:min="0"
|
|
:max="100"
|
|
:step="0.1"
|
|
class="flex-1"
|
|
@update:model-value="handleSliderChange"
|
|
/>
|
|
<span class="min-w-16 text-xs text-base-foreground">
|
|
{{ formatTime(currentTime) }} / {{ formatTime(animationDuration) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Select from '@/components/ui/select/Select.vue'
|
|
import SelectContent from '@/components/ui/select/SelectContent.vue'
|
|
import SelectItem from '@/components/ui/select/SelectItem.vue'
|
|
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
|
|
import SelectValue from '@/components/ui/select/SelectValue.vue'
|
|
import Slider from '@/components/ui/slider/Slider.vue'
|
|
|
|
type Animation = { name: string; index: number }
|
|
|
|
const animations = defineModel<Animation[]>('animations')
|
|
const playing = defineModel<boolean>('playing')
|
|
const selectedSpeed = defineModel<number>('selectedSpeed')
|
|
const selectedAnimation = defineModel<number>('selectedAnimation')
|
|
const animationProgress = defineModel<number>('animationProgress', {
|
|
default: 0
|
|
})
|
|
const animationDuration = defineModel<number>('animationDuration', {
|
|
default: 0
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
seek: [progress: number]
|
|
}>()
|
|
|
|
const speedOptions = [
|
|
{ name: '0.1x', value: 0.1 },
|
|
{ name: '0.5x', value: 0.5 },
|
|
{ name: '1x', value: 1 },
|
|
{ name: '1.5x', value: 1.5 },
|
|
{ name: '2x', value: 2 }
|
|
]
|
|
|
|
const currentTime = computed(() => {
|
|
if (!animationDuration.value) return 0
|
|
return (animationProgress.value / 100) * animationDuration.value
|
|
})
|
|
|
|
function formatTime(seconds: number): string {
|
|
const mins = Math.floor(seconds / 60)
|
|
const secs = (seconds % 60).toFixed(1)
|
|
return mins > 0 ? `${mins}:${secs.padStart(4, '0')}` : `${secs}s`
|
|
}
|
|
|
|
function togglePlay() {
|
|
playing.value = !playing.value
|
|
}
|
|
|
|
function handleSliderChange(value: number[] | undefined) {
|
|
if (!value) return
|
|
const progress = value[0]
|
|
animationProgress.value = progress
|
|
emit('seek', progress)
|
|
}
|
|
</script>
|