mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-03 04:00:31 +00:00
[3d] add support to export different formats (#3176)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
@updateUpDirection="handleUpdateUpDirection"
|
||||
@updateMaterialMode="handleUpdateMaterialMode"
|
||||
@updateEdgeThreshold="handleUpdateEdgeThreshold"
|
||||
@exportModel="handleExportModel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,6 +73,7 @@ import {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const { widget } = defineProps<{
|
||||
widget: ComponentWidget<string[]>
|
||||
@@ -183,6 +185,22 @@ const handleUpdateMaterialMode = (value: MaterialMode) => {
|
||||
node.properties['Material Mode'] = value
|
||||
}
|
||||
|
||||
const handleExportModel = async (format: string) => {
|
||||
if (!load3DSceneRef.value?.load3d) {
|
||||
useToastStore().addAlert('No 3D scene to export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await load3DSceneRef.value.load3d.exportModel(format)
|
||||
} catch (error) {
|
||||
console.error('Error exporting model:', error)
|
||||
useToastStore().addAlert(
|
||||
`Failed to export model as ${format.toUpperCase()}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const listenMaterialModeChange = (mode: MaterialMode) => {
|
||||
materialMode.value = mode
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<Button
|
||||
v-for="(label, category) in categories"
|
||||
v-for="category in availableCategories"
|
||||
:key="category"
|
||||
class="p-button-text w-full flex items-center justify-start"
|
||||
:class="{ 'bg-gray-600': activeCategory === category }"
|
||||
@click="selectCategory(category)"
|
||||
>
|
||||
<i :class="getCategoryIcon(category)"></i>
|
||||
<span class="text-white">{{ t(label) }}</span>
|
||||
<span class="text-white">{{ t(categoryLabels[category]) }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,6 +70,12 @@
|
||||
@updateLightIntensity="handleUpdateLightIntensity"
|
||||
ref="lightControlsRef"
|
||||
/>
|
||||
|
||||
<ExportControls
|
||||
v-if="activeCategory === 'export'"
|
||||
@exportModel="handleExportModel"
|
||||
ref="exportControlsRef"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showPreviewButton">
|
||||
<Button class="p-button-rounded p-button-text" @click="togglePreview">
|
||||
@@ -89,9 +95,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import CameraControls from '@/components/load3d/controls/CameraControls.vue'
|
||||
import ExportControls from '@/components/load3d/controls/ExportControls.vue'
|
||||
import LightControls from '@/components/load3d/controls/LightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
|
||||
@@ -123,19 +130,30 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const isMenuOpen = ref(false)
|
||||
const activeCategory = ref<'scene' | 'model' | 'camera' | 'light'>('scene')
|
||||
const categories = {
|
||||
const activeCategory = ref<string>('scene')
|
||||
const categoryLabels: Record<string, string> = {
|
||||
scene: 'load3d.scene',
|
||||
model: 'load3d.model',
|
||||
camera: 'load3d.camera',
|
||||
light: 'load3d.light'
|
||||
light: 'load3d.light',
|
||||
export: 'load3d.export'
|
||||
}
|
||||
|
||||
const availableCategories = computed(() => {
|
||||
const baseCategories = ['scene', 'model', 'camera', 'light']
|
||||
|
||||
if (!props.inputSpec.isAnimation) {
|
||||
return [...baseCategories, 'export']
|
||||
}
|
||||
|
||||
return baseCategories
|
||||
})
|
||||
|
||||
const toggleMenu = () => {
|
||||
isMenuOpen.value = !isMenuOpen.value
|
||||
}
|
||||
|
||||
const selectCategory = (category: 'scene' | 'model' | 'camera' | 'light') => {
|
||||
const selectCategory = (category: string) => {
|
||||
activeCategory.value = category
|
||||
isMenuOpen.value = false
|
||||
}
|
||||
@@ -145,7 +163,8 @@ const getCategoryIcon = (category: string) => {
|
||||
scene: 'pi pi-image',
|
||||
model: 'pi pi-box',
|
||||
camera: 'pi pi-camera',
|
||||
light: 'pi pi-sun'
|
||||
light: 'pi pi-sun',
|
||||
export: 'pi pi-download'
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return `${icons[category]} text-white text-lg`
|
||||
@@ -162,6 +181,7 @@ const emit = defineEmits<{
|
||||
(e: 'updateUpDirection', direction: UpDirection): void
|
||||
(e: 'updateMaterialMode', mode: MaterialMode): void
|
||||
(e: 'updateEdgeThreshold', value: number): void
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const backgroundColor = ref(props.backgroundColor)
|
||||
@@ -218,6 +238,10 @@ const handleUpdateFOV = (value: number) => {
|
||||
emit('updateFOV', value)
|
||||
}
|
||||
|
||||
const handleExportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
}
|
||||
|
||||
const closeSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
|
||||
@@ -59,7 +59,13 @@ const eventConfig = {
|
||||
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
materialLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
|
||||
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading()
|
||||
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
exportLoadingStart: (message: string) => {
|
||||
loadingOverlayRef.value?.startLoading(message || t('load3d.exportingModel'))
|
||||
},
|
||||
exportLoadingEnd: () => {
|
||||
loadingOverlayRef.value?.endLoading()
|
||||
}
|
||||
} as const
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
@@ -15,19 +15,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const modelLoading = ref(false)
|
||||
const loadingMessage = ref('')
|
||||
|
||||
const startLoading = (message?: string) => {
|
||||
modelLoading.value = true
|
||||
const startLoading = async (message?: string) => {
|
||||
loadingMessage.value = message || t('load3d.loadingModel')
|
||||
modelLoading.value = true
|
||||
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const endLoading = () => {
|
||||
const endLoading = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
modelLoading.value = false
|
||||
}
|
||||
|
||||
|
||||
81
src/components/load3d/controls/ExportControls.vue
Normal file
81
src/components/load3d/controls/ExportControls.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="relative show-export-formats">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="toggleExportFormats"
|
||||
>
|
||||
<i
|
||||
class="pi pi-download text-white text-lg"
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.exportModel'),
|
||||
showDelay: 300
|
||||
}"
|
||||
></i>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showExportFormats"
|
||||
class="absolute left-12 top-0 bg-black bg-opacity-50 rounded-lg shadow-lg"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<Button
|
||||
v-for="format in exportFormats"
|
||||
:key="format.value"
|
||||
class="p-button-text text-white"
|
||||
@click="exportModel(format.value)"
|
||||
>
|
||||
{{ format.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const showExportFormats = ref(false)
|
||||
|
||||
const exportFormats = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' }
|
||||
]
|
||||
|
||||
const toggleExportFormats = () => {
|
||||
showExportFormats.value = !showExportFormats.value
|
||||
}
|
||||
|
||||
const exportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
|
||||
showExportFormats.value = false
|
||||
}
|
||||
|
||||
const closeExportFormatsList = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (!target.closest('.show-export-formats')) {
|
||||
showExportFormats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeExportFormatsList)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeExportFormatsList)
|
||||
})
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@ import { ControlsManager } from './ControlsManager'
|
||||
import { EventManager } from './EventManager'
|
||||
import { LightingManager } from './LightingManager'
|
||||
import { LoaderManager } from './LoaderManager'
|
||||
import { ModelExporter } from './ModelExporter'
|
||||
import { ModelManager } from './ModelManager'
|
||||
import { NodeStorage } from './NodeStorage'
|
||||
import { PreviewManager } from './PreviewManager'
|
||||
@@ -225,6 +226,47 @@ class Load3d {
|
||||
)
|
||||
}
|
||||
|
||||
async exportModel(format: string): Promise<void> {
|
||||
if (!this.modelManager.currentModel) {
|
||||
throw new Error('No model to export')
|
||||
}
|
||||
|
||||
const exportMessage = `Exporting as ${format.toUpperCase()}...`
|
||||
this.eventManager.emitEvent('exportLoadingStart', exportMessage)
|
||||
|
||||
try {
|
||||
const model = this.modelManager.currentModel.clone()
|
||||
|
||||
const originalFileName = this.modelManager.originalFileName || 'model'
|
||||
const filename = `${originalFileName}.${format}`
|
||||
|
||||
const originalURL = this.modelManager.originalURL
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
switch (format) {
|
||||
case 'glb':
|
||||
await ModelExporter.exportGLB(model, filename, originalURL)
|
||||
break
|
||||
case 'obj':
|
||||
await ModelExporter.exportOBJ(model, filename, originalURL)
|
||||
break
|
||||
case 'stl':
|
||||
await ModelExporter.exportSTL(model, filename), originalURL
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
} catch (error) {
|
||||
console.error(`Error exporting model as ${format}:`, error)
|
||||
throw error
|
||||
} finally {
|
||||
this.eventManager.emitEvent('exportLoadingEnd', null)
|
||||
}
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
this.sceneManager.setBackgroundColor(color)
|
||||
this.forceRender()
|
||||
|
||||
@@ -47,12 +47,23 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
|
||||
this.modelManager.clearModel()
|
||||
|
||||
this.modelManager.originalURL = url
|
||||
|
||||
let fileExtension: string | undefined
|
||||
if (originalFileName) {
|
||||
fileExtension = originalFileName.split('.').pop()?.toLowerCase()
|
||||
|
||||
this.modelManager.originalFileName =
|
||||
originalFileName.split('/').pop()?.split('.')[0] || 'model'
|
||||
} else {
|
||||
const filename = new URLSearchParams(url.split('?')[1]).get('filename')
|
||||
fileExtension = filename?.split('.').pop()?.toLowerCase()
|
||||
|
||||
if (filename) {
|
||||
this.modelManager.originalFileName = filename.split('.')[0] || 'model'
|
||||
} else {
|
||||
this.modelManager.originalFileName = 'model'
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileExtension) {
|
||||
|
||||
163
src/extensions/core/load3d/ModelExporter.ts
Normal file
163
src/extensions/core/load3d/ModelExporter.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as THREE from 'three'
|
||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'
|
||||
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
|
||||
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
export class ModelExporter {
|
||||
static detectFormatFromURL(url: string): string | null {
|
||||
try {
|
||||
const filenameParam = new URLSearchParams(url.split('?')[1]).get(
|
||||
'filename'
|
||||
)
|
||||
if (filenameParam) {
|
||||
const extension = filenameParam.split('.').pop()?.toLowerCase()
|
||||
return extension || null
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing URL:', e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static canUseDirectURL(url: string | null, format: string): boolean {
|
||||
if (!url) return false
|
||||
|
||||
const urlFormat = ModelExporter.detectFormatFromURL(url)
|
||||
if (!urlFormat) return false
|
||||
|
||||
return urlFormat.toLowerCase() === format.toLowerCase()
|
||||
}
|
||||
|
||||
static async downloadFromURL(
|
||||
url: string,
|
||||
desiredFilename: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = desiredFilename
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (error) {
|
||||
console.error('Error downloading from URL:', error)
|
||||
useToastStore().addAlert('Failed to download file from URL')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static async exportGLB(
|
||||
model: THREE.Object3D,
|
||||
filename: string = 'model.glb',
|
||||
originalURL?: string | null
|
||||
): Promise<void> {
|
||||
if (originalURL && ModelExporter.canUseDirectURL(originalURL, 'glb')) {
|
||||
console.log('Using direct URL download for GLB')
|
||||
return ModelExporter.downloadFromURL(originalURL, filename)
|
||||
}
|
||||
|
||||
const exporter = new GLTFExporter()
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const result = await new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
exporter.parse(
|
||||
model,
|
||||
(gltf) => {
|
||||
resolve(gltf as ArrayBuffer)
|
||||
},
|
||||
(error) => {
|
||||
reject(error)
|
||||
},
|
||||
{ binary: true }
|
||||
)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
ModelExporter.saveArrayBuffer(result, filename)
|
||||
} catch (error) {
|
||||
console.error('Error exporting GLB:', error)
|
||||
useToastStore().addAlert('Failed to export model as GLB')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static async exportOBJ(
|
||||
model: THREE.Object3D,
|
||||
filename: string = 'model.obj',
|
||||
originalURL?: string | null
|
||||
): Promise<void> {
|
||||
if (originalURL && ModelExporter.canUseDirectURL(originalURL, 'obj')) {
|
||||
console.log('Using direct URL download for OBJ')
|
||||
return ModelExporter.downloadFromURL(originalURL, filename)
|
||||
}
|
||||
|
||||
const exporter = new OBJExporter()
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const result = exporter.parse(model)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
ModelExporter.saveString(result, filename)
|
||||
} catch (error) {
|
||||
console.error('Error exporting OBJ:', error)
|
||||
useToastStore().addAlert('Failed to export model as OBJ')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static async exportSTL(
|
||||
model: THREE.Object3D,
|
||||
filename: string = 'model.stl',
|
||||
originalURL?: string | null
|
||||
): Promise<void> {
|
||||
if (originalURL && ModelExporter.canUseDirectURL(originalURL, 'stl')) {
|
||||
console.log('Using direct URL download for STL')
|
||||
return ModelExporter.downloadFromURL(originalURL, filename)
|
||||
}
|
||||
|
||||
const exporter = new STLExporter()
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const result = exporter.parse(model)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
ModelExporter.saveString(result, filename)
|
||||
} catch (error) {
|
||||
console.error('Error exporting STL:', error)
|
||||
useToastStore().addAlert('Failed to export model as STL')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
|
||||
const blob = new Blob([buffer], { type: 'application/octet-stream' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
}
|
||||
|
||||
private static saveString(text: string, filename: string): void {
|
||||
const blob = new Blob([text], { type: 'text/plain' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ export class ModelManager implements ModelManagerInterface {
|
||||
standardMaterial: THREE.MeshStandardMaterial
|
||||
wireframeMaterial: THREE.MeshBasicMaterial
|
||||
depthMaterial: THREE.MeshDepthMaterial
|
||||
originalFileName: string | null = null
|
||||
originalURL: string | null = null
|
||||
|
||||
private scene: THREE.Scene
|
||||
private renderer: THREE.WebGLRenderer
|
||||
@@ -633,6 +635,8 @@ export class ModelManager implements ModelManagerInterface {
|
||||
this.originalRotation = null
|
||||
this.currentUpDirection = 'original'
|
||||
this.setMaterialMode('original')
|
||||
this.originalFileName = null
|
||||
this.originalURL = null
|
||||
|
||||
this.originalMaterials = new WeakMap()
|
||||
}
|
||||
|
||||
@@ -142,6 +142,8 @@ export interface AnimationManagerInterface extends BaseManager {
|
||||
}
|
||||
|
||||
export interface ModelManagerInterface {
|
||||
originalFileName: string | null
|
||||
originalURL: string | null
|
||||
currentModel: THREE.Object3D | null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null
|
||||
originalRotation: THREE.Euler | null
|
||||
|
||||
@@ -978,6 +978,9 @@
|
||||
"camera": "Camera",
|
||||
"light": "Light",
|
||||
"switchingMaterialMode": "Switching Material Mode...",
|
||||
"edgeThreshold": "Edge Threshold"
|
||||
"edgeThreshold": "Edge Threshold",
|
||||
"export": "Export",
|
||||
"exportModel": "Export Model",
|
||||
"exportingModel": "Exporting model..."
|
||||
}
|
||||
}
|
||||
@@ -349,6 +349,9 @@
|
||||
"backgroundColor": "Couleur de fond",
|
||||
"camera": "Caméra",
|
||||
"edgeThreshold": "Seuil de Bordure",
|
||||
"export": "Exportation",
|
||||
"exportModel": "Exportation du modèle",
|
||||
"exportingModel": "Exportation du modèle en cours...",
|
||||
"fov": "FOV",
|
||||
"light": "Lumière",
|
||||
"lightIntensity": "Intensité de la lumière",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"backgroundColor": "背景色",
|
||||
"camera": "カメラ",
|
||||
"edgeThreshold": "エッジ閾値",
|
||||
"export": "エクスポート",
|
||||
"exportModel": "モデルをエクスポート",
|
||||
"exportingModel": "モデルをエクスポート中...",
|
||||
"fov": "FOV",
|
||||
"light": "ライト",
|
||||
"lightIntensity": "光の強度",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"backgroundColor": "배경색",
|
||||
"camera": "카메라",
|
||||
"edgeThreshold": "엣지 임계값",
|
||||
"export": "수출",
|
||||
"exportModel": "모델 수출",
|
||||
"exportingModel": "모델 수출 중...",
|
||||
"fov": "FOV",
|
||||
"light": "빛",
|
||||
"lightIntensity": "조명 강도",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"backgroundColor": "Цвет фона",
|
||||
"camera": "Камера",
|
||||
"edgeThreshold": "Пороговое значение края",
|
||||
"export": "Экспорт",
|
||||
"exportModel": "Экспорт модели",
|
||||
"exportingModel": "Экспорт модели...",
|
||||
"fov": "Угол обзора",
|
||||
"light": "Свет",
|
||||
"lightIntensity": "Интенсивность света",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"backgroundColor": "背景颜色",
|
||||
"camera": "相机",
|
||||
"edgeThreshold": "边缘阈值",
|
||||
"export": "导出",
|
||||
"exportModel": "导出模型",
|
||||
"exportingModel": "正在导出模型...",
|
||||
"fov": "视场",
|
||||
"light": "灯光",
|
||||
"lightIntensity": "光照强度",
|
||||
|
||||
Reference in New Issue
Block a user