Skip to content

Commit d22d62b

Browse files
jtydhr88github-actions
andauthored
[3d] initial version of 3d viewer (#3968)
Co-authored-by: github-actions <github-actions@github.com>
1 parent 8e357c4 commit d22d62b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2071
-42
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
@@ -13,6 +13,7 @@
1313
<BypassButton />
1414
<PinButton />
1515
<EditModelButton />
16+
<Load3DViewerButton />
1617
<MaskEditorButton />
1718
<ConvertToSubgraphButton />
1819
<DeleteButton />
@@ -38,6 +39,7 @@ import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton
3839
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
3940
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
4041
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
42+
import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'
4143
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
4244
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
4345
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 { useSettingStore } from '@/stores/settingStore'
23+
import { isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
24+
25+
const commandStore = useCommandStore()
26+
const canvasStore = useCanvasStore()
27+
28+
const is3DNode = computed(() => {
29+
const enable3DViewer = useSettingStore().get('Comfy.Load3D.3DViewerEnable')
30+
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
31+
32+
return nodes.length === 1 && nodes.some(isLoad3dNode) && enable3DViewer
33+
})
34+
35+
const open3DViewer = () => {
36+
void commandStore.execute('Comfy.3DViewer.Open3DViewer')
37+
}
38+
</script>

src/components/load3d/Load3D.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,19 @@
5858
@export-model="handleExportModel"
5959
/>
6060
<div
61-
v-if="showRecordingControls"
61+
v-if="enable3DViewer"
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 right-2 z-20 pointer-events-auto"
70+
:class="{
71+
'top-12': !enable3DViewer,
72+
'top-24': enable3DViewer
73+
}"
6374
>
6475
<RecordingControls
6576
:node="node"
@@ -82,6 +93,7 @@ import { useI18n } from 'vue-i18n'
8293
import Load3DControls from '@/components/load3d/Load3DControls.vue'
8394
import Load3DScene from '@/components/load3d/Load3DScene.vue'
8495
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
96+
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
8597
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
8698
import {
8799
CameraType,
@@ -91,6 +103,7 @@ import {
91103
} from '@/extensions/core/load3d/interfaces'
92104
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
93105
import type { ComponentWidget } from '@/scripts/domWidget'
106+
import { useSettingStore } from '@/stores/settingStore'
94107
import { useToastStore } from '@/stores/toastStore'
95108
96109
const { t } = useI18n()
@@ -121,6 +134,9 @@ const isRecording = ref(false)
121134
const hasRecording = ref(false)
122135
const recordingDuration = ref(0)
123136
const showRecordingControls = ref(!inputSpec.isPreview)
137+
const enable3DViewer = computed(() =>
138+
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
139+
)
124140
125141
const showPreviewButton = computed(() => {
126142
return !type.includes('Preview')
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
<div class="p-2 space-y-4">
21+
<SceneControls
22+
v-model:background-color="viewer.backgroundColor.value"
23+
v-model:show-grid="viewer.showGrid.value"
24+
:has-background-image="viewer.hasBackgroundImage.value"
25+
@update-background-image="viewer.handleBackgroundImageUpdate"
26+
/>
27+
</div>
28+
29+
<div class="p-2 space-y-4">
30+
<ModelControls
31+
v-model:up-direction="viewer.upDirection.value"
32+
v-model:material-mode="viewer.materialMode.value"
33+
/>
34+
</div>
35+
36+
<div class="p-2 space-y-4">
37+
<CameraControls
38+
v-model:camera-type="viewer.cameraType.value"
39+
v-model:fov="viewer.fov.value"
40+
/>
41+
</div>
42+
43+
<div class="p-2 space-y-4">
44+
<LightControls
45+
v-model:light-intensity="viewer.lightIntensity.value"
46+
/>
47+
</div>
48+
49+
<div class="p-2 space-y-4">
50+
<ExportControls @export-model="viewer.exportModel" />
51+
</div>
52+
</div>
53+
</div>
54+
55+
<div class="p-4">
56+
<div class="flex gap-2">
57+
<Button
58+
icon="pi pi-times"
59+
severity="secondary"
60+
:label="t('g.cancel')"
61+
@click="handleCancel"
62+
/>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
</template>
68+
69+
<script setup lang="ts">
70+
import Button from 'primevue/button'
71+
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
72+
73+
import CameraControls from '@/components/load3d/controls/viewer/CameraControls.vue'
74+
import ExportControls from '@/components/load3d/controls/viewer/ExportControls.vue'
75+
import LightControls from '@/components/load3d/controls/viewer/LightControls.vue'
76+
import ModelControls from '@/components/load3d/controls/viewer/ModelControls.vue'
77+
import SceneControls from '@/components/load3d/controls/viewer/SceneControls.vue'
78+
import { t } from '@/i18n'
79+
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
80+
import { useLoad3dService } from '@/services/load3dService'
81+
import { useDialogStore } from '@/stores/dialogStore'
82+
83+
const props = defineProps<{
84+
node: LGraphNode
85+
}>()
86+
87+
const viewerContentRef = ref<HTMLDivElement>()
88+
const containerRef = ref<HTMLDivElement>()
89+
const mainContentRef = ref<HTMLDivElement>()
90+
const maximized = ref(false)
91+
const mutationObserver = ref<MutationObserver | null>(null)
92+
93+
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
94+
95+
onMounted(async () => {
96+
const source = useLoad3dService().getLoad3d(props.node)
97+
if (source && containerRef.value) {
98+
await viewer.initializeViewer(containerRef.value, source)
99+
}
100+
101+
if (viewerContentRef.value) {
102+
mutationObserver.value = new MutationObserver((mutations) => {
103+
mutations.forEach((mutation) => {
104+
if (
105+
mutation.type === 'attributes' &&
106+
mutation.attributeName === 'maximized'
107+
) {
108+
maximized.value =
109+
(mutation.target as HTMLElement).getAttribute('maximized') ===
110+
'true'
111+
112+
setTimeout(() => {
113+
viewer.refreshViewport()
114+
}, 0)
115+
}
116+
})
117+
})
118+
119+
mutationObserver.value.observe(viewerContentRef.value, {
120+
attributes: true,
121+
attributeFilter: ['maximized']
122+
})
123+
}
124+
125+
window.addEventListener('resize', viewer.handleResize)
126+
})
127+
128+
const handleCancel = () => {
129+
viewer.restoreInitialState()
130+
useDialogStore().closeDialog()
131+
}
132+
133+
onBeforeUnmount(() => {
134+
window.removeEventListener('resize', viewer.handleResize)
135+
136+
if (mutationObserver.value) {
137+
mutationObserver.value.disconnect()
138+
mutationObserver.value = null
139+
}
140+
141+
// we will manually cleanup the viewer in dialog close handler
142+
})
143+
</script>
144+
145+
<style scoped>
146+
:deep(.p-panel-content) {
147+
padding: 0;
148+
}
149+
</style>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 { Tooltip } from 'primevue'
19+
import Button from 'primevue/button'
20+
21+
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
22+
import { t } from '@/i18n'
23+
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
24+
import { useLoad3dService } from '@/services/load3dService'
25+
import { useDialogStore } from '@/stores/dialogStore'
26+
27+
const vTooltip = Tooltip
28+
29+
const { node } = defineProps<{
30+
node: LGraphNode
31+
}>()
32+
33+
const openIn3DViewer = () => {
34+
const props = { node: node }
35+
36+
useDialogStore().showDialog({
37+
key: 'global-load3d-viewer',
38+
title: t('load3d.viewer.title'),
39+
component: Load3DViewerContent,
40+
props: props,
41+
dialogComponentProps: {
42+
style: 'width: 80vw; height: 80vh;',
43+
maximizable: true,
44+
onClose: async () => {
45+
await useLoad3dService().handleViewerClose(props.node)
46+
}
47+
}
48+
})
49+
}
50+
</script>
51+
52+
<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>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<template>
2+
<Select
3+
v-model="exportFormat"
4+
:options="exportFormats"
5+
option-label="label"
6+
option-value="value"
7+
>
8+
</Select>
9+
10+
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
11+
{{ t('load3d.export') }}
12+
</Button>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import Button from 'primevue/button'
17+
import Select from 'primevue/select'
18+
import { ref } from 'vue'
19+
20+
import { t } from '@/i18n'
21+
22+
const emit = defineEmits<{
23+
(e: 'exportModel', format: string): void
24+
}>()
25+
26+
const exportFormats = [
27+
{ label: 'GLB', value: 'glb' },
28+
{ label: 'OBJ', value: 'obj' },
29+
{ label: 'STL', value: 'stl' }
30+
]
31+
32+
const exportFormat = ref('obj')
33+
34+
const exportModel = (format: string) => {
35+
emit('exportModel', format)
36+
}
37+
</script>

0 commit comments

Comments
 (0)