Skip to content

Commit 05ef101

Browse files
committed
[3d] initial version of 3d editor
1 parent 6167861 commit 05ef101

36 files changed

+1933
-21
lines changed

src/assets/css/style.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,8 @@ audio.comfy-audio.empty-audio-widget {
616616
.comfy-load-3d canvas,
617617
.comfy-load-3d-animation canvas,
618618
.comfy-preview-3d canvas,
619-
.comfy-preview-3d-animation canvas{
619+
.comfy-preview-3d-animation canvas,
620+
.comfy-load-3d-viewer canvas{
620621
display: flex;
621622
width: 100% !important;
622623
height: 100% !important;

src/components/graph/SelectionToolbox.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<BypassButton />
1212
<PinButton />
1313
<EditModelButton />
14+
<Load3DViewerButton />
1415
<MaskEditorButton />
1516
<ConvertToSubgraphButton />
1617
<DeleteButton />
@@ -36,6 +37,7 @@ import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton
3637
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
3738
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
3839
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
40+
import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'
3941
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
4042
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
4143
import RefreshButton from '@/components/graph/selectionToolbox/RefreshButton.vue'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<Button
3+
v-show="is3DNode"
4+
v-tooltip.top="{
5+
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
6+
showDelay: 1000
7+
}"
8+
severity="secondary"
9+
text
10+
icon="pi pi-pencil"
11+
@click="open3DViewer"
12+
/>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import Button from 'primevue/button'
17+
import { computed } from 'vue'
18+
19+
import { t } from '@/i18n'
20+
import { useCommandStore } from '@/stores/commandStore'
21+
import { useCanvasStore } from '@/stores/graphStore'
22+
import { isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
23+
24+
const commandStore = useCommandStore()
25+
const canvasStore = useCanvasStore()
26+
27+
const is3DNode = computed(() => {
28+
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
29+
return nodes.length === 1 && nodes.some(isLoad3dNode)
30+
})
31+
32+
const open3DViewer = () => {
33+
void commandStore.execute('Comfy.3DViewer.Open3DViewer')
34+
}
35+
</script>

src/components/load3d/Load3D.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@
6060
<div
6161
v-if="showRecordingControls"
6262
class="absolute top-12 right-2 z-20 pointer-events-auto"
63+
>
64+
<ViewerControls :node="node" />
65+
</div>
66+
67+
<div
68+
v-if="showRecordingControls"
69+
class="absolute top-24 right-2 z-20 pointer-events-auto"
6370
>
6471
<RecordingControls
6572
:node="node"
@@ -82,6 +89,7 @@ import { useI18n } from 'vue-i18n'
8289
import Load3DControls from '@/components/load3d/Load3DControls.vue'
8390
import Load3DScene from '@/components/load3d/Load3DScene.vue'
8491
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
92+
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
8593
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
8694
import {
8795
CameraType,
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<template>
2+
<div
3+
ref="viewerContentRef"
4+
class="flex w-full"
5+
:class="[maximized ? 'h-full' : 'h-[70vh]']"
6+
@mouseenter="viewer.handleMouseEnter"
7+
@mouseleave="viewer.handleMouseLeave"
8+
>
9+
<div ref="mainContentRef" class="flex-1 relative">
10+
<div
11+
ref="containerRef"
12+
class="absolute w-full h-full comfy-load-3d-viewer"
13+
@resize="viewer.handleResize"
14+
/>
15+
</div>
16+
17+
<div class="w-72 flex flex-col">
18+
<div class="flex-1 overflow-y-auto p-4">
19+
<div class="space-y-2">
20+
<Panel v-model:collapsed="panelStates.scene" toggleable>
21+
<template #header>
22+
<div class="flex items-center gap-2">
23+
<i class="pi pi-image" />
24+
<span>{{ t('load3d.viewer.sceneSettings') }}</span>
25+
</div>
26+
</template>
27+
<div class="p-4 space-y-4">
28+
<SceneControls
29+
v-model:background-color="viewer.backgroundColor.value"
30+
v-model:show-grid="viewer.showGrid.value"
31+
:has-background-image="viewer.hasBackgroundImage.value"
32+
@update-background-image="viewer.handleBackgroundImageUpdate"
33+
/>
34+
</div>
35+
</Panel>
36+
37+
<Panel v-model:collapsed="panelStates.model" toggleable>
38+
<template #header>
39+
<div class="flex items-center gap-2">
40+
<i class="pi pi-box" />
41+
<span>{{ t('load3d.viewer.modelSettings') }}</span>
42+
</div>
43+
</template>
44+
<div class="p-4 space-y-4">
45+
<ModelControls
46+
v-model:up-direction="viewer.upDirection.value"
47+
v-model:material-mode="viewer.materialMode.value"
48+
/>
49+
</div>
50+
</Panel>
51+
52+
<Panel v-model:collapsed="panelStates.camera" toggleable>
53+
<template #header>
54+
<div class="flex items-center gap-2">
55+
<i class="pi pi-camera" />
56+
<span>{{ t('load3d.viewer.cameraSettings') }}</span>
57+
</div>
58+
</template>
59+
<div class="p-4 space-y-4">
60+
<CameraControls
61+
v-model:camera-type="viewer.cameraType.value"
62+
v-model:fov="viewer.fov.value"
63+
/>
64+
</div>
65+
</Panel>
66+
67+
<Panel v-model:collapsed="panelStates.light" toggleable>
68+
<template #header>
69+
<div class="flex items-center gap-2">
70+
<i class="pi pi-sun" />
71+
<span>{{ t('load3d.viewer.lightSettings') }}</span>
72+
</div>
73+
</template>
74+
<div class="p-4 space-y-4">
75+
<LightControls
76+
v-model:light-intensity="viewer.lightIntensity.value"
77+
/>
78+
</div>
79+
</Panel>
80+
81+
<Panel v-model:collapsed="panelStates.export" toggleable>
82+
<template #header>
83+
<div class="flex items-center gap-2">
84+
<i class="pi pi-download" />
85+
<span>{{ t('load3d.viewer.exportSettings') }}</span>
86+
</div>
87+
</template>
88+
<div class="p-4 space-y-4">
89+
<ExportControls @export-model="viewer.exportModel" />
90+
</div>
91+
</Panel>
92+
</div>
93+
</div>
94+
95+
<div class="p-4">
96+
<div class="flex gap-2">
97+
<Button
98+
icon="pi pi-times"
99+
severity="secondary"
100+
:label="t('g.cancel')"
101+
@click="handleCancel"
102+
/>
103+
<Button
104+
icon="pi pi-check"
105+
severity="secondary"
106+
:label="t('g.apply')"
107+
@click="handleConfirm"
108+
/>
109+
</div>
110+
</div>
111+
</div>
112+
</div>
113+
</template>
114+
115+
<script setup lang="ts">
116+
import { LGraphNode } from '@comfyorg/litegraph'
117+
import Button from 'primevue/button'
118+
import Panel from 'primevue/panel'
119+
import { onBeforeUnmount, onMounted, ref, toRef } from 'vue'
120+
121+
import CameraControls from '@/components/load3d/controls/viewer/CameraControls.vue'
122+
import ExportControls from '@/components/load3d/controls/viewer/ExportControls.vue'
123+
import LightControls from '@/components/load3d/controls/viewer/LightControls.vue'
124+
import ModelControls from '@/components/load3d/controls/viewer/ModelControls.vue'
125+
import SceneControls from '@/components/load3d/controls/viewer/SceneControls.vue'
126+
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
127+
import { t } from '@/i18n'
128+
import { useLoad3dService } from '@/services/load3dService'
129+
import { useDialogStore } from '@/stores/dialogStore'
130+
131+
const props = defineProps<{
132+
node: LGraphNode
133+
}>()
134+
135+
const viewerContentRef = ref<HTMLDivElement>()
136+
const containerRef = ref<HTMLDivElement>()
137+
const mainContentRef = ref<HTMLDivElement>()
138+
const maximized = ref(false)
139+
const mutationObserver = ref<MutationObserver | null>(null)
140+
141+
const panelStates = ref({
142+
scene: false,
143+
model: true,
144+
camera: true,
145+
light: true,
146+
export: true
147+
})
148+
149+
const viewer = useLoad3dViewer(toRef(props, 'node'))
150+
151+
onMounted(async () => {
152+
const source = useLoad3dService().getLoad3d(props.node)
153+
if (source && containerRef.value) {
154+
await viewer.initializeViewer(containerRef.value, source)
155+
}
156+
157+
if (viewerContentRef.value) {
158+
mutationObserver.value = new MutationObserver((mutations) => {
159+
mutations.forEach((mutation) => {
160+
if (
161+
mutation.type === 'attributes' &&
162+
mutation.attributeName === 'maximized'
163+
) {
164+
maximized.value =
165+
(mutation.target as HTMLElement).getAttribute('maximized') ===
166+
'true'
167+
168+
setTimeout(() => {
169+
viewer.refreshViewport()
170+
}, 0)
171+
}
172+
})
173+
})
174+
175+
mutationObserver.value.observe(viewerContentRef.value, {
176+
attributes: true,
177+
attributeFilter: ['maximized']
178+
})
179+
}
180+
181+
window.addEventListener('resize', viewer.handleResize)
182+
})
183+
184+
const handleCancel = () => {
185+
viewer.restoreInitialState()
186+
useDialogStore().closeDialog()
187+
}
188+
189+
const handleConfirm = async () => {
190+
const success = await viewer.applyChanges()
191+
if (!success) {
192+
viewer.restoreInitialState()
193+
}
194+
195+
useDialogStore().closeDialog()
196+
}
197+
198+
onBeforeUnmount(() => {
199+
window.removeEventListener('resize', viewer.handleResize)
200+
201+
if (mutationObserver.value) {
202+
mutationObserver.value.disconnect()
203+
mutationObserver.value = null
204+
}
205+
206+
viewer.cleanup()
207+
})
208+
</script>
209+
210+
<style scoped>
211+
:deep(.p-panel-content) {
212+
padding: 0;
213+
}
214+
</style>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<div class="relative bg-gray-700 bg-opacity-30 rounded-lg">
3+
<div class="flex flex-col gap-2">
4+
<Button class="p-button-rounded p-button-text" @click="openIn3DViewer">
5+
<i
6+
v-tooltip.right="{
7+
value: t('load3d.openIn3DViewer'),
8+
showDelay: 300
9+
}"
10+
class="pi pi-expand text-white text-lg"
11+
/>
12+
</Button>
13+
</div>
14+
</div>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { LGraphNode } from '@comfyorg/litegraph'
19+
import { Tooltip } from 'primevue'
20+
import Button from 'primevue/button'
21+
22+
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
23+
import { t } from '@/i18n'
24+
import { useDialogStore } from '@/stores/dialogStore'
25+
26+
const vTooltip = Tooltip
27+
28+
const { node } = defineProps<{
29+
node: LGraphNode
30+
}>()
31+
32+
const openIn3DViewer = () => {
33+
const props = { node: node }
34+
35+
useDialogStore().showDialog({
36+
key: 'global-load3d-viewer',
37+
title: t('load3d.viewer.title'),
38+
component: Load3DViewerContent,
39+
props: props,
40+
dialogComponentProps: {
41+
style: 'width: 80vw; height: 80vh;',
42+
maximizable: true
43+
}
44+
})
45+
}
46+
</script>
47+
48+
<style scoped></style>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<template>
2+
<div class="space-y-4">
3+
<label>
4+
{{ t('load3d.viewer.cameraType') }}
5+
</label>
6+
<Select
7+
v-model="cameraType"
8+
:options="cameras"
9+
option-label="title"
10+
option-value="value"
11+
>
12+
</Select>
13+
</div>
14+
15+
<div v-if="showFOVButton" class="space-y-4">
16+
<label>{{ t('load3d.fov') }}</label>
17+
<Slider v-model="fov" :min="10" :max="150" :step="1" aria-label="fov" />
18+
</div>
19+
</template>
20+
21+
<script setup lang="ts">
22+
import Select from 'primevue/select'
23+
import Slider from 'primevue/slider'
24+
import { computed } from 'vue'
25+
26+
import { CameraType } from '@/extensions/core/load3d/interfaces'
27+
import { t } from '@/i18n'
28+
29+
const cameras = [
30+
{ title: t('load3d.cameraType.perspective'), value: 'perspective' },
31+
{ title: t('load3d.cameraType.orthographic'), value: 'orthographic' }
32+
]
33+
34+
const cameraType = defineModel<CameraType>('cameraType')
35+
const fov = defineModel<number>('fov')
36+
const showFOVButton = computed(() => cameraType.value === 'perspective')
37+
</script>

0 commit comments

Comments
 (0)