lunchboxjs 0.2.1001-beta.0 → 0.2.1001-beta.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,15 +11,8 @@
11
11
  </template>
12
12
 
13
13
  <script lang="ts" setup>
14
- import { computed, ref, watch } from 'vue'
15
- import {
16
- onBeforeRender,
17
- globals,
18
- Lunch,
19
- // camera,
20
- useRenderer,
21
- useCamera,
22
- } from '../src'
14
+ import { computed, ref } from 'vue'
15
+ import { onBeforeRender, Lunch, useCamera, useRenderer } from '../src'
23
16
 
24
17
  // props
25
18
  const props = defineProps<{
@@ -33,9 +26,6 @@ const ready = computed(() => {
33
26
  const camera = useCamera()
34
27
  const renderer = useRenderer()
35
28
  const orbitArgs = computed(() => [camera.value, renderer.value?.domElement])
36
- // watch(() => orbitArgs.value, console.log, { immediate: true })
37
- // console.log(renderer)
38
- watch(camera, console.log)
39
29
 
40
30
  // update
41
31
  const controls = ref<Lunch.LunchboxComponent>()
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "lunchboxjs",
3
- "version": "0.2.1001-beta.0",
3
+ "version": "0.2.1001-beta.301",
4
4
  "scripts": {
5
5
  "dev": "vite -c utils/vite.config.ts",
6
6
  "build": "vue-tsc --noEmit && vite build -c utils/vite.config.ts",
7
7
  "build:tsc": "tsc --project ./utils/tsconfig.lib.json",
8
8
  "build:rollup": "rollup -c ./utils/lib-rollup.ts",
9
9
  "build:dts": "cp utils/lib-dts.d.ts dist/lunchboxjs.es.d.ts && cp utils/lib-dts.d.ts dist/lunchboxjs.umd.d.ts",
10
- "build:lib": "rm -rf js && npm run build:tsc && npm run build:rollup && npm run build:dts",
10
+ "build:lib": "rimraf js && npm run build:tsc && npm run build:rollup && npm run build:dts",
11
11
  "prepare": "npm run build:lib",
12
12
  "docs:dev": "vitepress dev docs",
13
13
  "docs:build": "vitepress build docs",
14
14
  "docs:serve": "vitepress serve docs",
15
15
  "demo:create": "node utils/createExample"
16
16
  },
17
+ "engines": {
18
+ "node": ">=16.0.0"
19
+ },
17
20
  "dependencies": {
18
21
  "uuid": "8.3.2",
19
22
  "vue": "^3.2.16"
@@ -36,6 +39,7 @@
36
39
  "nice-color-palettes": "3.0.0",
37
40
  "prompt": "1.3.0",
38
41
  "prompts": "2.4.2",
42
+ "rimraf": "3.0.2",
39
43
  "rollup-plugin-delete": "2.0.0",
40
44
  "rollup-plugin-jsx": "1.0.3",
41
45
  "rollup-plugin-terser": "7.0.2",
@@ -0,0 +1,237 @@
1
+ import { defineComponent, onBeforeUnmount, ref, watch } from 'vue'
2
+ import {
3
+ useCamera,
4
+ Lunch,
5
+ useGlobals,
6
+ useLunchboxInteractables,
7
+ onRendererReady,
8
+ } from '..'
9
+ import * as THREE from 'three'
10
+ import { offBeforeRender, onBeforeRender } from '../core'
11
+
12
+ export const LunchboxEventHandlers = defineComponent({
13
+ name: 'LunchboxEventHandlers',
14
+ setup() {
15
+ const interactables = useLunchboxInteractables()
16
+ const globals = useGlobals()
17
+ const mousePos = ref({ x: Infinity, y: Infinity })
18
+ const inputActive = ref(false)
19
+
20
+ let currentIntersections: Array<{
21
+ element: Lunch.Node
22
+ intersection: THREE.Intersection<THREE.Object3D>
23
+ }> = []
24
+
25
+ const raycaster = new THREE.Raycaster(
26
+ new THREE.Vector3(),
27
+ new THREE.Vector3(0, 0, -1)
28
+ )
29
+
30
+ const fireEventsFromIntersections = ({
31
+ element,
32
+ eventKeys,
33
+ intersection,
34
+ }: {
35
+ element: Lunch.Node
36
+ eventKeys: Array<Lunch.EventKey>
37
+ intersection: THREE.Intersection<THREE.Object3D>
38
+ }) => {
39
+ if (!element) return
40
+ eventKeys.forEach((eventKey) => {
41
+ if (element.eventListeners[eventKey]) {
42
+ element.eventListeners[eventKey].forEach((cb) => {
43
+ cb({ intersection })
44
+ })
45
+ }
46
+ })
47
+ }
48
+
49
+ // add mouse listener to renderer DOM element when the element is ready
50
+ onRendererReady((v) => {
51
+ if (!v?.domElement) return
52
+
53
+ // we have a DOM element, so let's add mouse listeners
54
+ const { domElement } = v
55
+
56
+ const mouseMoveListener = (evt: PointerEvent) => {
57
+ const screenWidth = (domElement.width ?? 1) / globals.dpr
58
+ const screenHeight = (domElement.height ?? 1) / globals.dpr
59
+ mousePos.value.x = (evt.offsetX / screenWidth) * 2 - 1
60
+ mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1
61
+ }
62
+ const mouseDownListener = () => (inputActive.value = true)
63
+ const mouseUpListener = () => (inputActive.value = false)
64
+
65
+ // add mouse events
66
+ domElement.addEventListener('pointermove', mouseMoveListener)
67
+ domElement.addEventListener('pointerdown', mouseDownListener)
68
+ domElement.addEventListener('pointerup', mouseUpListener)
69
+ })
70
+
71
+ const camera = useCamera()
72
+ const update = () => {
73
+ const c = camera.value
74
+ if (!c) return
75
+
76
+ // console.log(camera.value)
77
+
78
+ raycaster.setFromCamera(mousePos.value, c)
79
+ const intersections = raycaster.intersectObjects(
80
+ interactables?.value.map(
81
+ (v) => v.instance as any as THREE.Object3D
82
+ ) ?? []
83
+ )
84
+
85
+ let enterValues: Array<THREE.Intersection<THREE.Object3D>> = [],
86
+ sameValues: Array<THREE.Intersection<THREE.Object3D>> = [],
87
+ leaveValues: Array<THREE.Intersection<THREE.Object3D>> = [],
88
+ entering: Array<{
89
+ element: Lunch.Node
90
+ intersection: THREE.Intersection<THREE.Object3D>
91
+ }> = [],
92
+ staying: Array<{
93
+ element: Lunch.Node
94
+ intersection: THREE.Intersection<THREE.Object3D>
95
+ }> = []
96
+
97
+ // intersection arrays
98
+ leaveValues = currentIntersections.map((v) => v.intersection)
99
+
100
+ // element arrays
101
+ intersections?.forEach((intersection) => {
102
+ const currentIdx = currentIntersections.findIndex(
103
+ (v) => v.intersection.object === intersection.object
104
+ )
105
+ if (currentIdx === -1) {
106
+ // new intersection
107
+ enterValues.push(intersection)
108
+
109
+ const found = interactables?.value.find(
110
+ (v) => v.instance?.uuid === intersection.object.uuid
111
+ )
112
+ if (found) {
113
+ entering.push({ element: found, intersection })
114
+ }
115
+ } else {
116
+ // existing intersection
117
+ sameValues.push(intersection)
118
+
119
+ const found = interactables?.value.find(
120
+ (v) => v.instance?.uuid === intersection.object.uuid
121
+ )
122
+ if (found) {
123
+ staying.push({ element: found, intersection })
124
+ }
125
+ }
126
+ // this is a current intersection, so it won't be in our `leave` array
127
+ const leaveIdx = leaveValues.findIndex(
128
+ (v) => v.object.uuid === intersection.object.uuid
129
+ )
130
+ if (leaveIdx !== -1) {
131
+ leaveValues.splice(leaveIdx, 1)
132
+ }
133
+ })
134
+
135
+ const leaving: Array<{
136
+ element: Lunch.Node
137
+ intersection: THREE.Intersection<THREE.Object3D>
138
+ }> = leaveValues.map((intersection) => {
139
+ return {
140
+ element: interactables?.value.find(
141
+ (interactable) =>
142
+ interactable.instance?.uuid ===
143
+ intersection.object.uuid
144
+ ) as any as Lunch.Node,
145
+ intersection,
146
+ }
147
+ })
148
+
149
+ // new interactions
150
+ entering.forEach(({ element, intersection }) => {
151
+ fireEventsFromIntersections({
152
+ element,
153
+ eventKeys: ['onPointerEnter'],
154
+ intersection,
155
+ })
156
+ })
157
+
158
+ // unchanged interactions
159
+ staying.forEach(({ element, intersection }) => {
160
+ const eventKeys: Array<Lunch.EventKey> = [
161
+ 'onPointerOver',
162
+ 'onPointerMove',
163
+ ]
164
+ fireEventsFromIntersections({
165
+ element,
166
+ eventKeys,
167
+ intersection,
168
+ })
169
+ })
170
+
171
+ // exited interactions
172
+ leaving.forEach(({ element, intersection }) => {
173
+ const eventKeys: Array<Lunch.EventKey> = [
174
+ 'onPointerLeave',
175
+ 'onPointerOut',
176
+ ]
177
+ fireEventsFromIntersections({
178
+ element,
179
+ eventKeys,
180
+ intersection,
181
+ })
182
+ })
183
+
184
+ currentIntersections = ([] as any).concat(entering, staying)
185
+ }
186
+
187
+ // update function
188
+ onBeforeRender(update)
189
+
190
+ const teardown = () => offBeforeRender(update)
191
+ onBeforeUnmount(teardown)
192
+
193
+ const clickEventKeys: Lunch.EventKey[] = [
194
+ 'onClick',
195
+ 'onPointerDown',
196
+ 'onPointerUp',
197
+ ]
198
+ watch(inputActive, (isDown) => {
199
+ // run raycaster on click (necessary when `update` is not automatically called,
200
+ // for example in `updateSource` functions)
201
+ update()
202
+
203
+ // meshes with multiple intersections receive multiple callbacks by default -
204
+ // let's make it so they only receive one callback of each type per frame.
205
+ // (ie usually when you click on a mesh, you expect only one click event to fire, even
206
+ // if there are technically multiple intersections with that mesh)
207
+ const uuidsInteractedWithThisFrame: string[] = []
208
+ currentIntersections.forEach((v) => {
209
+ clickEventKeys.forEach((key) => {
210
+ const id = v.element.uuid + key
211
+ if (
212
+ isDown &&
213
+ (key === 'onClick' || key === 'onPointerDown')
214
+ ) {
215
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
216
+ v.element.eventListeners[key]?.forEach((cb) =>
217
+ cb({ intersection: v.intersection })
218
+ )
219
+ uuidsInteractedWithThisFrame.push(id)
220
+ }
221
+ } else if (!isDown && key === 'onPointerUp') {
222
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
223
+ v.element.eventListeners[key]?.forEach((cb) =>
224
+ cb({ intersection: v.intersection })
225
+ )
226
+ uuidsInteractedWithThisFrame.push(id)
227
+ }
228
+ }
229
+ })
230
+ })
231
+ })
232
+
233
+ // return arbitrary object to ensure instantiation
234
+ // TODO: why can't we return a <raycaster/> here?
235
+ return () => <object3D />
236
+ },
237
+ })
@@ -1,18 +1,23 @@
1
1
  import {
2
+ computed,
2
3
  defineComponent,
3
4
  onBeforeUnmount,
4
5
  onMounted,
5
6
  PropType,
6
7
  reactive,
7
8
  ref,
9
+ watch,
8
10
  WatchSource,
9
11
  } from 'vue'
10
12
  import { cancelUpdate, cancelUpdateSource, MiniDom, update } from '../../core'
11
- import { Lunch, useApp, useGlobals } from '../..'
13
+ import { Lunch, useApp, useGlobals, useLunchboxInteractables } from '../..'
12
14
  import * as THREE from 'three'
13
15
  import { prepCanvas } from './prepCanvas'
14
16
  import { useUpdateGlobals, useStartCallbacks } from '../..'
15
17
  import { LunchboxScene } from './LunchboxScene'
18
+ import { LunchboxEventHandlers } from '../LunchboxEventHandlers'
19
+ import * as Keys from '../../keys'
20
+ import { waitFor } from '../../utils'
16
21
 
17
22
  /** fixed & fill styling for container */
18
23
  const fillStyle = (position: string) => {
@@ -67,6 +72,8 @@ export const LunchboxWrapper = defineComponent({
67
72
  ;(THREE as any).ColorManagement.legacyMode = false
68
73
  }
69
74
 
75
+ const interactables = useLunchboxInteractables()
76
+
70
77
  // MOUNT
71
78
  // ====================
72
79
  onMounted(async () => {
@@ -104,26 +111,21 @@ export const LunchboxWrapper = defineComponent({
104
111
  }
105
112
  updateGlobals?.({ dpr })
106
113
 
107
- console.log(1)
108
114
  while (
109
115
  !renderer.value?.$el?.instance &&
110
116
  // TODO: remove `as any`
111
117
  !(renderer.value as any)?.component?.ctx.$el?.instance
112
118
  ) {
113
- console.log(2)
114
119
  await new Promise((r) => requestAnimationFrame(r))
115
120
  }
116
121
 
117
- console.log(3)
118
122
  while (
119
123
  !scene.value?.$el?.instance &&
120
124
  // TODO: remove `as any`
121
125
  !(scene.value as any)?.component?.ctx.$el?.instance
122
126
  ) {
123
- console.log(4)
124
127
  await new Promise((r) => requestAnimationFrame(r))
125
128
  }
126
- console.log(5)
127
129
 
128
130
  const normalizedRenderer = (renderer.value?.$el?.instance ??
129
131
  (renderer.value as any)?.component?.ctx.$el
@@ -214,6 +216,43 @@ export const LunchboxWrapper = defineComponent({
214
216
  const canvasFillStyle =
215
217
  props.sizePolicy === 'container' ? 'static' : 'fixed'
216
218
 
219
+ // REACTIVE CUSTOM CAMERAS
220
+ // ====================
221
+ // find first camera with `type.name` property
222
+ // (which indicates a Lunch.Node)
223
+ const activeCamera = computed(() => {
224
+ const output = context.slots
225
+ ?.camera?.()
226
+ .find((c) => (c.type as any)?.name)
227
+ if (output) {
228
+ return output
229
+ }
230
+
231
+ return output
232
+ })
233
+
234
+ // TODO: make custom cameras reactive
235
+ watch(
236
+ activeCamera,
237
+ async (newVal, oldVal) => {
238
+ // console.log('got camera', newVal)
239
+ if (newVal && newVal?.props?.key !== oldVal?.props?.key) {
240
+ // TODO: remove cast
241
+ camera.value = newVal as any
242
+
243
+ // TODO: why isn't this updating app camera?
244
+ // const el = await waitFor(() => newVal.el)
245
+ // console.log(el)
246
+ // camera.value = el
247
+ // console.log(newVal.uuid)
248
+ // updateGlobals?.({ camera: el })
249
+ }
250
+ },
251
+ { immediate: true }
252
+ )
253
+
254
+ // RENDER FUNCTION
255
+ // ====================
217
256
  return () => (
218
257
  <>
219
258
  {/* use renderer slot if provided... */}
@@ -272,7 +311,7 @@ export const LunchboxWrapper = defineComponent({
272
311
  {/* use camera slot if provided... */}
273
312
  {context.slots?.camera?.()?.length ? (
274
313
  // TODO: remove `any` cast
275
- (camera.value = context.slots?.camera?.()[0] as any)
314
+ camera.value
276
315
  ) : props.ortho || props.orthographic ? (
277
316
  <orthographicCamera
278
317
  ref={camera}
@@ -293,6 +332,9 @@ export const LunchboxWrapper = defineComponent({
293
332
  {...consolidatedCameraProperties}
294
333
  />
295
334
  )}
335
+
336
+ {/* Lunchbox interaction handlers */}
337
+ {interactables?.value.length && <LunchboxEventHandlers />}
296
338
  </>
297
339
  )
298
340
  },
@@ -106,6 +106,7 @@ export const autoGeneratedComponents = [
106
106
  'group',
107
107
  'catmullRomCurve3',
108
108
  'points',
109
+ 'raycaster',
109
110
 
110
111
  // helpers
111
112
  'cameraHelper',
@@ -157,7 +158,6 @@ export const autoGeneratedComponents = [
157
158
 
158
159
 
159
160
  // misc
160
- raycaster: RaycasterProps
161
161
  vector2: Vector2Props
162
162
  vector3: Vector3Props
163
163
  vector4: Vector4Props
@@ -1,9 +1,9 @@
1
1
  import { h, defineComponent } from 'vue'
2
2
  import { LunchboxWrapper } from './LunchboxWrapper/LunchboxWrapper'
3
3
  import { autoGeneratedComponents } from './autoGeneratedComponents'
4
+ import type { Lunch } from '../types'
4
5
 
5
- import { catalogue } from './catalogue'
6
- export { catalogue }
6
+ export const catalogue: Lunch.Catalogue = {}
7
7
 
8
8
  // component creation utility
9
9
  const createComponent = (tag: string) =>
@@ -1,6 +1,6 @@
1
1
  import { isLunchboxRootNode } from '../utils'
2
2
  import { instantiateThreeObject, MiniDom } from '.'
3
- import { Lunch } from '..'
3
+ import type { Lunch } from '..'
4
4
 
5
5
  /** Create a new Lunchbox comment node. */
6
6
  export function createCommentNode(options: Partial<Lunch.CommentMeta> = {}) {
@@ -1,6 +1,6 @@
1
1
  import { h, defineComponent } from 'vue'
2
2
  import { catalogue } from '../components'
3
- import { Lunch } from '..'
3
+ import type { Lunch } from '..'
4
4
 
5
5
  const createComponent = (tag: string) =>
6
6
  defineComponent({
package/src/core/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './createNode'
2
- export * from './ensure'
3
2
  export * from './interaction'
4
3
  export * from './extend'
5
4
  export * from './instantiateThreeObject'
@@ -1,7 +1,7 @@
1
1
  import { catalogue } from '../../components'
2
2
  import * as THREE from 'three'
3
3
  import { processPropAsArray } from './processProps'
4
- import { Lunch } from '../..'
4
+ import type { Lunch } from '../..'
5
5
 
6
6
  export function instantiateThreeObject<T>(node: Lunch.StandardMeta<T>) {
7
7
  if (!node.type) return null
@@ -1,4 +1,4 @@
1
- import { Lunch } from '../..'
1
+ import type { Lunch } from '../..'
2
2
 
3
3
  /** Process props into either themselves or the $attached value */
4
4
  export function processProp<T, U = THREE.Object3D>({
@@ -0,0 +1,55 @@
1
+ import type { Ref } from 'vue'
2
+ import type { Lunch } from '..'
3
+
4
+ /** Add an event listener to the given node. Also creates the event teardown function and any necessary raycaster/interaction dictionary updates. */
5
+ export function addEventListener({
6
+ node,
7
+ key,
8
+ interactables,
9
+ value,
10
+ }: {
11
+ node: Lunch.Node
12
+ key: Lunch.EventKey
13
+ interactables: Ref<Lunch.Node[]>
14
+ value: Lunch.EventCallback
15
+ }) {
16
+ // create new records for this key if needed
17
+ if (!node.eventListeners[key]) {
18
+ node.eventListeners[key] = []
19
+ }
20
+ if (!node.eventListenerRemoveFunctions[key]) {
21
+ node.eventListenerRemoveFunctions[key] = []
22
+ }
23
+
24
+ // add event listener
25
+ node.eventListeners[key].push(value)
26
+
27
+ // if we need it, let's get/create the main raycaster
28
+ if (interactionsRequiringRaycaster.includes(key)) {
29
+ if (node.instance && !interactables.value.includes(node)) {
30
+ // add to interactables
31
+ interactables.value.push(node)
32
+ node.eventListenerRemoveFunctions[key].push(() => {
33
+ // remove from interactables
34
+ const idx = interactables.value.indexOf(node)
35
+ if (idx !== -1) {
36
+ interactables.value.splice(idx, 1)
37
+ }
38
+ })
39
+ }
40
+ }
41
+
42
+ return node
43
+ }
44
+
45
+ const interactionsRequiringRaycaster = [
46
+ 'onClick',
47
+ 'onPointerUp',
48
+ 'onPointerDown',
49
+ 'onPointerOver',
50
+ 'onPointerOut',
51
+ 'onPointerEnter',
52
+ 'onPointerLeave',
53
+ 'onPointerMove',
54
+ // 'onPointerMissed',
55
+ ]
@@ -1,5 +1,5 @@
1
1
  import { v4 as createUuid } from 'uuid'
2
- import { Lunch } from '..'
2
+ import type { Lunch } from '..'
3
3
 
4
4
  // MiniDom recreates DOM node properties and methods.
5
5
  // Since Vue 3 is a DOM-first framework, many of its nodeOps depend on