Skip to content

Commit e893bf0

Browse files
committed
feat(camera-info): 3D viewport widget for CreateCameraInfo node
1 parent 5bce9c4 commit e893bf0

42 files changed

Lines changed: 4576 additions & 24 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<template>
2+
<div
3+
class="relative size-full min-h-[300px]"
4+
@pointerdown.stop
5+
@mousedown.stop
6+
>
7+
<div
8+
ref="container"
9+
class="relative size-full"
10+
data-capture-wheel="true"
11+
tabindex="-1"
12+
@pointerdown.stop="focusContainer"
13+
@contextmenu.stop.prevent
14+
@mouseenter="handleMouseEnter"
15+
@mouseleave="handleMouseLeave"
16+
/>
17+
<div class="pointer-events-none absolute inset-x-0 top-0">
18+
<div
19+
ref="toolbar"
20+
class="pointer-events-auto flex h-10 items-center gap-1 bg-interface-menu-surface px-2"
21+
@wheel.stop
22+
>
23+
<button
24+
v-tooltip.bottom="tip(gizmosLabel)"
25+
type="button"
26+
:disabled="lookingThrough"
27+
:class="
28+
cn(
29+
actionClass(!lookingThrough && gizmosOn),
30+
lookingThrough && 'cursor-not-allowed opacity-40'
31+
)
32+
"
33+
:aria-pressed="!lookingThrough && gizmosOn"
34+
:aria-label="compact ? gizmosLabel : undefined"
35+
@click="toggleGizmos"
36+
>
37+
<i
38+
:class="
39+
cn(
40+
'size-4',
41+
gizmosOn ? 'icon-[lucide--eye]' : 'icon-[lucide--eye-off]'
42+
)
43+
"
44+
/>
45+
<span v-if="!compact">{{ gizmosLabel }}</span>
46+
</button>
47+
<div class="mx-1 h-5 w-px shrink-0 bg-interface-menu-stroke" />
48+
<button
49+
v-for="option in transformGizmoOptions"
50+
:key="option.value"
51+
v-tooltip.bottom="tip($t(option.labelKey))"
52+
type="button"
53+
:disabled="lookingThrough || !option.enabled"
54+
:aria-pressed="!lookingThrough && transformGizmoMode === option.value"
55+
:aria-label="compact ? $t(option.labelKey) : undefined"
56+
:class="
57+
cn(
58+
actionClass(
59+
!lookingThrough && transformGizmoMode === option.value
60+
),
61+
(lookingThrough || !option.enabled) &&
62+
'cursor-not-allowed opacity-40'
63+
)
64+
"
65+
@click="selectTransformGizmo(option.value)"
66+
>
67+
<i :class="cn('size-4', option.icon)" />
68+
<span v-if="!compact">{{ $t(option.labelKey) }}</span>
69+
</button>
70+
</div>
71+
</div>
72+
<div class="pointer-events-none absolute inset-x-0 bottom-0">
73+
<div
74+
class="pointer-events-auto flex h-10 items-center justify-end gap-1 bg-interface-menu-surface px-2"
75+
@wheel.stop
76+
>
77+
<button
78+
v-tooltip.top="tip(lookThroughLabel)"
79+
type="button"
80+
:class="
81+
cn(iconBtnClass, lookingThrough && 'bg-button-active-surface')
82+
"
83+
:aria-pressed="lookingThrough"
84+
:aria-label="lookThroughLabel"
85+
@click="toggleLookThrough"
86+
>
87+
<i class="icon-[lucide--video] size-4" />
88+
</button>
89+
</div>
90+
</div>
91+
</div>
92+
</template>
93+
94+
<script setup lang="ts">
95+
import { useElementSize } from '@vueuse/core'
96+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
97+
import type { Ref } from 'vue'
98+
import { useI18n } from 'vue-i18n'
99+
100+
import {
101+
actionClass,
102+
iconBtnClass,
103+
tip
104+
} from '@/components/load3d/menubar/menuBarStyles'
105+
import { useCameraInfo } from '@/composables/useCameraInfo'
106+
import type { TransformGizmoMode } from '@/extensions/core/cameraInfo/CameraInfoViewport'
107+
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
108+
import type { ComponentWidget } from '@/scripts/domWidget'
109+
import type { NodeId } from '@/types/nodeId'
110+
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
111+
import { resolveNode } from '@/utils/litegraphUtil'
112+
import { cn } from '@comfyorg/tailwind-utils'
113+
114+
const { widget, nodeId } = defineProps<{
115+
widget: ComponentWidget<string[]> | SimplifiedWidget
116+
nodeId?: NodeId
117+
}>()
118+
119+
function isComponentWidget(
120+
w: ComponentWidget<string[]> | SimplifiedWidget
121+
): w is ComponentWidget<string[]> {
122+
return 'node' in w && w.node !== undefined
123+
}
124+
125+
const node = ref<LGraphNode | null>(null)
126+
if (isComponentWidget(widget)) {
127+
node.value = widget.node
128+
} else if (nodeId) {
129+
onMounted(() => {
130+
node.value = resolveNode(nodeId) ?? null
131+
})
132+
}
133+
134+
const { t } = useI18n()
135+
136+
const container = ref<HTMLElement | null>(null)
137+
const toolbar = ref<HTMLElement | null>(null)
138+
const { width: toolbarWidth } = useElementSize(toolbar)
139+
const compactWidthThreshold = 480
140+
const compact = computed(
141+
() => toolbarWidth.value > 0 && toolbarWidth.value < compactWidthThreshold
142+
)
143+
const gizmosOn = ref(true)
144+
const lookingThrough = ref(false)
145+
const transformGizmoMode = ref<TransformGizmoMode>('none')
146+
const {
147+
initialize,
148+
cleanup,
149+
handleMouseEnter,
150+
handleMouseLeave,
151+
setGizmosVisible,
152+
setTransformGizmoMode,
153+
setLookThrough,
154+
mode
155+
} = useCameraInfo(node as Ref<LGraphNode | null>)
156+
157+
const gizmosLabel = computed(() =>
158+
gizmosOn.value ? t('load3d.hideGizmos') : t('load3d.showGizmos')
159+
)
160+
161+
const lookThroughLabel = computed(() =>
162+
lookingThrough.value ? t('load3d.exitLookThrough') : t('load3d.lookThrough')
163+
)
164+
165+
const transformGizmoOptions = computed(() => [
166+
{
167+
value: 'none' as const,
168+
labelKey: 'load3d.transformGizmo.none',
169+
icon: 'icon-[lucide--ban]',
170+
enabled: true
171+
},
172+
{
173+
value: 'target' as const,
174+
labelKey: 'load3d.transformGizmo.target',
175+
icon: 'icon-[lucide--target]',
176+
enabled: mode.value === 'orbit' || mode.value === 'look_at'
177+
},
178+
{
179+
value: 'camera-translate' as const,
180+
labelKey: 'load3d.transformGizmo.cameraTranslate',
181+
icon: 'icon-[lucide--move-3d]',
182+
enabled: mode.value === 'look_at' || mode.value === 'quaternion'
183+
},
184+
{
185+
value: 'camera-rotate' as const,
186+
labelKey: 'load3d.transformGizmo.cameraRotate',
187+
icon: 'icon-[lucide--rotate-3d]',
188+
enabled: mode.value === 'quaternion'
189+
}
190+
])
191+
192+
function focusContainer() {
193+
container.value?.focus()
194+
}
195+
196+
function toggleGizmos() {
197+
gizmosOn.value = !gizmosOn.value
198+
}
199+
200+
function toggleLookThrough() {
201+
lookingThrough.value = !lookingThrough.value
202+
}
203+
204+
function selectTransformGizmo(value: TransformGizmoMode) {
205+
transformGizmoMode.value = value
206+
}
207+
208+
watch(gizmosOn, (on) => setGizmosVisible(on))
209+
watch(transformGizmoMode, (m) => setTransformGizmoMode(m))
210+
watch(lookingThrough, (on) => setLookThrough(on))
211+
212+
onMounted(() => {
213+
if (container.value) initialize(container.value)
214+
})
215+
216+
onUnmounted(() => {
217+
cleanup()
218+
})
219+
</script>

src/components/load3d/Load3DControls.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
v-if="showCameraControls"
7373
v-model:camera-type="cameraConfig!.cameraType"
7474
v-model:fov="cameraConfig!.fov"
75+
v-model:use-custom-up="cameraConfig!.useCustomUp"
76+
:has-custom-up="cameraConfig!.hasCustomUp ?? false"
7577
/>
7678

7779
<div v-if="showLightControls" class="flex flex-col">

src/components/load3d/controls/CameraControls.vue

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@
1313
>
1414
<i class="pi pi-camera text-lg text-base-foreground" />
1515
</Button>
16+
<Button
17+
v-if="hasCustomUp"
18+
v-tooltip.right="{
19+
value: useCustomUp
20+
? $t('load3d.useNaturalUp')
21+
: $t('load3d.useCustomUp'),
22+
showDelay: 300
23+
}"
24+
size="icon"
25+
variant="textonly"
26+
class="rounded-full"
27+
:aria-label="
28+
useCustomUp ? $t('load3d.useNaturalUp') : $t('load3d.useCustomUp')
29+
"
30+
@click="toggleUp"
31+
>
32+
<i
33+
:class="
34+
cn(
35+
useCustomUp
36+
? 'icon-[lucide--compass]'
37+
: 'icon-[lucide--rotate-ccw]',
38+
'text-lg text-base-foreground'
39+
)
40+
"
41+
/>
42+
</Button>
1643
<PopupSlider
1744
v-if="showFOVButton"
1845
v-model="fov"
@@ -27,13 +54,24 @@ import { computed } from 'vue'
2754
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
2855
import Button from '@/components/ui/button/Button.vue'
2956
import type { CameraType } from '@/extensions/core/load3d/interfaces'
57+
import { cn } from '@comfyorg/tailwind-utils'
58+
59+
const { hasCustomUp = false } = defineProps<{
60+
hasCustomUp?: boolean
61+
}>()
3062
3163
const cameraType = defineModel<CameraType>('cameraType')
3264
const fov = defineModel<number>('fov')
65+
const useCustomUp = defineModel<boolean>('useCustomUp', { default: false })
66+
3367
const showFOVButton = computed(() => cameraType.value === 'perspective')
3468
3569
const switchCamera = () => {
3670
cameraType.value =
3771
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
3872
}
73+
74+
const toggleUp = () => {
75+
useCustomUp.value = !useCustomUp.value
76+
}
3977
</script>

0 commit comments

Comments
 (0)