TresCanvas.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <script setup lang="ts">
  2. import type {
  3. Camera,
  4. ColorSpace,
  5. ShadowMapType,
  6. ToneMapping,
  7. WebGLRenderer,
  8. WebGLRendererParameters,
  9. } from 'three'
  10. import type { App, Ref } from 'vue'
  11. import type { RendererPresetsType } from '../composables/useRenderer/const'
  12. import type { TresObject, TresPointerEvent, TresScene } from '../types/'
  13. import { PerspectiveCamera, Scene } from 'three'
  14. import * as THREE from 'three'
  15. import {
  16. createRenderer,
  17. defineComponent,
  18. Fragment,
  19. getCurrentInstance,
  20. h,
  21. onMounted,
  22. onUnmounted,
  23. provide,
  24. ref,
  25. shallowRef,
  26. toValue,
  27. watch,
  28. watchEffect,
  29. } from 'vue'
  30. import pkg from '../../package.json'
  31. import {
  32. type TresContext,
  33. useTresContextProvider,
  34. } from '../composables'
  35. import { extend } from '../core/catalogue'
  36. import { nodeOps } from '../core/nodeOps'
  37. import { disposeObject3D, kebabToCamel } from '../utils/'
  38. import { registerTresDevtools } from '../devtools'
  39. import { whenever } from '@vueuse/core'
  40. export interface TresCanvasProps
  41. extends Omit<WebGLRendererParameters, 'canvas'> {
  42. // required by for useRenderer
  43. shadows?: boolean
  44. clearColor?: string
  45. toneMapping?: ToneMapping
  46. shadowMapType?: ShadowMapType
  47. useLegacyLights?: boolean
  48. outputColorSpace?: ColorSpace
  49. toneMappingExposure?: number
  50. renderMode?: 'always' | 'on-demand' | 'manual'
  51. dpr?: number | [number, number]
  52. // required by useTresContextProvider
  53. camera?: Camera
  54. preset?: RendererPresetsType
  55. windowSize?: boolean
  56. // Misc opt-out flags
  57. enableProvideBridge?: boolean
  58. }
  59. const props = withDefaults(defineProps<TresCanvasProps>(), {
  60. alpha: undefined,
  61. depth: undefined,
  62. shadows: undefined,
  63. stencil: undefined,
  64. antialias: undefined,
  65. windowSize: undefined,
  66. useLegacyLights: undefined,
  67. preserveDrawingBuffer: undefined,
  68. logarithmicDepthBuffer: undefined,
  69. failIfMajorPerformanceCaveat: undefined,
  70. renderMode: 'always',
  71. enableProvideBridge: true,
  72. })
  73. const emit = defineEmits<{
  74. ready: [context: TresContext]
  75. render: [renderer: WebGLRenderer]
  76. click: [event: TresPointerEvent]
  77. doubleClick: [event: TresPointerEvent]
  78. contextMenu: [event: TresPointerEvent]
  79. pointerMove: [event: TresPointerEvent]
  80. pointerUp: [event: TresPointerEvent]
  81. pointerDown: [event: TresPointerEvent]
  82. pointerEnter: [event: TresPointerEvent]
  83. pointerLeave: [event: TresPointerEvent]
  84. pointerOver: [event: TresPointerEvent]
  85. pointerOut: [event: TresPointerEvent]
  86. pointerMissed: [event: TresPointerEvent]
  87. wheel: [event: TresPointerEvent]
  88. }>()
  89. const slots = defineSlots<{
  90. default: () => any
  91. }>()
  92. const canvas = ref<HTMLCanvasElement>()
  93. /*
  94. `scene` is defined here and not in `useTresContextProvider` because the custom
  95. renderer uses it to mount the app nodes. This happens before `useTresContextProvider` is called.
  96. The custom renderer requires `scene` to be editable (not readonly).
  97. */
  98. const scene = shallowRef<TresScene | Scene>(new Scene())
  99. const instance = getCurrentInstance()
  100. extend(THREE)
  101. const createInternalComponent = (context: TresContext, empty = false) =>
  102. defineComponent({
  103. setup() {
  104. const ctx = getCurrentInstance()?.appContext
  105. if (ctx) { ctx.app = instance?.appContext.app as App }
  106. const provides: { [key: string | symbol]: unknown } = {}
  107. // Helper function to recursively merge provides from parents
  108. function mergeProvides(currentInstance: any) {
  109. if (!currentInstance) { return }
  110. // Recursively process the parent instance
  111. if (currentInstance.parent) {
  112. mergeProvides(currentInstance.parent)
  113. }
  114. // Extract provides from the current instance and merge them
  115. if (currentInstance.provides) {
  116. Object.assign(provides, currentInstance.provides)
  117. }
  118. }
  119. // Start the recursion from the initial instance
  120. if (instance?.parent && props.enableProvideBridge) {
  121. mergeProvides(instance.parent)
  122. Reflect.ownKeys(provides)
  123. .forEach((key) => {
  124. provide(key, provides[key])
  125. })
  126. }
  127. provide('useTres', context)
  128. provide('extend', extend)
  129. if (typeof window !== 'undefined') {
  130. registerTresDevtools(ctx?.app, context)
  131. }
  132. return () => h(Fragment, null, !empty ? slots.default() : [])
  133. },
  134. })
  135. const mountCustomRenderer = (context: TresContext, empty = false) => {
  136. const InternalComponent = createInternalComponent(context, empty)
  137. const { render } = createRenderer(nodeOps(context))
  138. render(h(InternalComponent), scene.value as unknown as TresObject)
  139. }
  140. const dispose = (context: TresContext, force = false) => {
  141. disposeObject3D(context.scene.value as unknown as TresObject)
  142. if (force) {
  143. context.renderer.instance.value.dispose()
  144. context.renderer.instance.value.renderLists.dispose()
  145. context.renderer.instance.value.forceContextLoss()
  146. }
  147. (scene.value as TresScene).__tres = {
  148. root: context,
  149. }
  150. }
  151. const context = shallowRef<TresContext | null>(null)
  152. defineExpose({ context, dispose: () => dispose(context.value as TresContext, true) })
  153. const handleHMR = (context: TresContext) => {
  154. dispose(context)
  155. mountCustomRenderer(context)
  156. }
  157. const unmountCanvas = () => {
  158. dispose(context.value as TresContext)
  159. mountCustomRenderer(context.value as TresContext, true)
  160. }
  161. onMounted(() => {
  162. const existingCanvas = canvas as Ref<HTMLCanvasElement>
  163. context.value = useTresContextProvider({
  164. scene: scene.value as TresScene,
  165. canvas: existingCanvas,
  166. windowSize: props.windowSize ?? false,
  167. rendererOptions: props,
  168. })
  169. const { camera, renderer } = context.value
  170. const { registerCamera, cameras, activeCamera, deregisterCamera } = camera
  171. mountCustomRenderer(context.value)
  172. const addDefaultCamera = () => {
  173. const camera = new PerspectiveCamera(
  174. 45,
  175. window.innerWidth / window.innerHeight,
  176. 0.1,
  177. 1000,
  178. )
  179. camera.position.set(3, 3, 3)
  180. camera.lookAt(0, 0, 0)
  181. registerCamera(camera)
  182. const unwatch = watchEffect(() => {
  183. if (cameras.value.length >= 2) {
  184. camera.removeFromParent()
  185. deregisterCamera(camera)
  186. unwatch?.()
  187. }
  188. })
  189. }
  190. watch(
  191. () => props.camera,
  192. (newCamera, oldCamera) => {
  193. if (newCamera) {
  194. registerCamera(toValue(newCamera), true)
  195. }
  196. if (oldCamera) {
  197. toValue(oldCamera).removeFromParent()
  198. deregisterCamera(toValue(oldCamera))
  199. }
  200. },
  201. {
  202. immediate: true,
  203. },
  204. )
  205. if (!activeCamera.value) {
  206. addDefaultCamera()
  207. }
  208. renderer.onRender.on((renderer) => {
  209. emit('render', renderer)
  210. })
  211. context.value.eventManager?.onEvent(({ type, event, intersection }) => {
  212. emit(
  213. kebabToCamel(type) as any, // typescript doesn't know that kebabToCamel(type) is a valid key of PointerEmits
  214. { type, event, intersection },
  215. )
  216. })
  217. // HMR support
  218. if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => handleHMR(context.value as TresContext)) }
  219. })
  220. whenever(() => context.value?.renderer.isReady, () => {
  221. if (context.value) { emit('ready', context.value) }
  222. }, { once: true })
  223. onUnmounted(unmountCanvas)
  224. </script>
  225. <template>
  226. <canvas
  227. ref="canvas"
  228. :data-scene="scene.uuid"
  229. :class="$attrs.class"
  230. :data-tres="`tresjs ${pkg.version}`"
  231. :style="{
  232. display: 'block',
  233. width: '100%',
  234. height: '100%',
  235. position: windowSize ? 'fixed' : 'relative',
  236. top: 0,
  237. left: 0,
  238. pointerEvents: 'auto',
  239. touchAction: 'none',
  240. ...$attrs.style as Object,
  241. }"
  242. ></canvas>
  243. </template>