lunchboxjs 0.2.1001-beta.0 → 0.2.1001-beta.1

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.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "lunchboxjs",
3
- "version": "0.2.1001-beta.0",
3
+ "version": "0.2.1001-beta.1",
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",
@@ -36,6 +36,7 @@
36
36
  "nice-color-palettes": "3.0.0",
37
37
  "prompt": "1.3.0",
38
38
  "prompts": "2.4.2",
39
+ "rimraf": "3.0.2",
39
40
  "rollup-plugin-delete": "2.0.0",
40
41
  "rollup-plugin-jsx": "1.0.3",
41
42
  "rollup-plugin-terser": "7.0.2",
@@ -0,0 +1,239 @@
1
+ import { defineComponent, onBeforeUnmount, ref, watch } from 'vue'
2
+ import {
3
+ Lunch,
4
+ useCamera,
5
+ useGlobals,
6
+ useLunchboxInteractables,
7
+ useRenderer,
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 camera = useCamera()
17
+ const renderer = useRenderer()
18
+ const globals = useGlobals()
19
+ const mousePos = ref({ x: Infinity, y: Infinity })
20
+ const inputActive = ref(false)
21
+
22
+ let currentIntersections: Array<{
23
+ element: Lunch.Node
24
+ intersection: THREE.Intersection<THREE.Object3D>
25
+ }> = []
26
+
27
+ const raycaster = new THREE.Raycaster(
28
+ new THREE.Vector3(),
29
+ new THREE.Vector3(0, 0, -1)
30
+ )
31
+
32
+ const fireEventsFromIntersections = ({
33
+ element,
34
+ eventKeys,
35
+ intersection,
36
+ }: {
37
+ element: Lunch.Node
38
+ eventKeys: Array<Lunch.EventKey>
39
+ intersection: THREE.Intersection<THREE.Object3D>
40
+ }) => {
41
+ if (!element) return
42
+ eventKeys.forEach((eventKey) => {
43
+ if (element.eventListeners[eventKey]) {
44
+ element.eventListeners[eventKey].forEach((cb) => {
45
+ cb({ intersection })
46
+ })
47
+ }
48
+ })
49
+ }
50
+
51
+ // add mouse listener to renderer DOM element when the element is ready
52
+ const stopWatch = watch(
53
+ renderer,
54
+ (v) => {
55
+ if (!v?.domElement) return
56
+
57
+ // we have a DOM element, so let's add mouse listeners
58
+ const { domElement } = v
59
+
60
+ const mouseMoveListener = (evt: PointerEvent) => {
61
+ const screenWidth = (domElement.width ?? 1) / globals.dpr
62
+ const screenHeight = (domElement.height ?? 1) / globals.dpr
63
+ mousePos.value.x = (evt.offsetX / screenWidth) * 2 - 1
64
+ mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1
65
+ }
66
+ const mouseDownListener = () => (inputActive.value = true)
67
+ const mouseUpListener = () => (inputActive.value = false)
68
+
69
+ // add mouse events
70
+ domElement.addEventListener('pointermove', mouseMoveListener)
71
+ domElement.addEventListener('pointerdown', mouseDownListener)
72
+ domElement.addEventListener('pointerup', mouseUpListener)
73
+
74
+ // stop the watcher
75
+ stopWatch()
76
+ },
77
+ { immediate: true }
78
+ )
79
+
80
+ const update = () => {
81
+ const c = camera.value
82
+ if (!c) return
83
+
84
+ raycaster.setFromCamera(mousePos.value, c)
85
+ const intersections = raycaster.intersectObjects(
86
+ interactables?.value.map(
87
+ (v) => v.instance as any as THREE.Object3D
88
+ ) ?? []
89
+ )
90
+
91
+ let enterValues: Array<THREE.Intersection<THREE.Object3D>> = [],
92
+ sameValues: Array<THREE.Intersection<THREE.Object3D>> = [],
93
+ leaveValues: Array<THREE.Intersection<THREE.Object3D>> = [],
94
+ entering: Array<{
95
+ element: Lunch.Node
96
+ intersection: THREE.Intersection<THREE.Object3D>
97
+ }> = [],
98
+ staying: Array<{
99
+ element: Lunch.Node
100
+ intersection: THREE.Intersection<THREE.Object3D>
101
+ }> = []
102
+
103
+ // intersection arrays
104
+ leaveValues = currentIntersections.map((v) => v.intersection)
105
+
106
+ // element arrays
107
+ intersections?.forEach((intersection) => {
108
+ const currentIdx = currentIntersections.findIndex(
109
+ (v) => v.intersection.object === intersection.object
110
+ )
111
+ if (currentIdx === -1) {
112
+ // new intersection
113
+ enterValues.push(intersection)
114
+
115
+ const found = interactables?.value.find(
116
+ (v) => v.instance?.uuid === intersection.object.uuid
117
+ )
118
+ if (found) {
119
+ entering.push({ element: found, intersection })
120
+ }
121
+ } else {
122
+ // existing intersection
123
+ sameValues.push(intersection)
124
+
125
+ const found = interactables?.value.find(
126
+ (v) => v.instance?.uuid === intersection.object.uuid
127
+ )
128
+ if (found) {
129
+ staying.push({ element: found, intersection })
130
+ }
131
+ }
132
+ // this is a current intersection, so it won't be in our `leave` array
133
+ const leaveIdx = leaveValues.findIndex(
134
+ (v) => v.object.uuid === intersection.object.uuid
135
+ )
136
+ if (leaveIdx !== -1) {
137
+ leaveValues.splice(leaveIdx, 1)
138
+ }
139
+ })
140
+
141
+ const leaving: Array<{
142
+ element: Lunch.Node
143
+ intersection: THREE.Intersection<THREE.Object3D>
144
+ }> = leaveValues.map((intersection) => {
145
+ return {
146
+ element: interactables?.value.find(
147
+ (interactable) =>
148
+ interactable.instance?.uuid ===
149
+ intersection.object.uuid
150
+ ) as any as Lunch.Node,
151
+ intersection,
152
+ }
153
+ })
154
+
155
+ // new interactions
156
+ entering.forEach(({ element, intersection }) => {
157
+ fireEventsFromIntersections({
158
+ element,
159
+ eventKeys: ['onPointerEnter'],
160
+ intersection,
161
+ })
162
+ })
163
+
164
+ // unchanged interactions
165
+ staying.forEach(({ element, intersection }) => {
166
+ const eventKeys: Array<Lunch.EventKey> = [
167
+ 'onPointerOver',
168
+ 'onPointerMove',
169
+ ]
170
+ fireEventsFromIntersections({
171
+ element,
172
+ eventKeys,
173
+ intersection,
174
+ })
175
+ })
176
+
177
+ // exited interactions
178
+ leaving.forEach(({ element, intersection }) => {
179
+ const eventKeys: Array<Lunch.EventKey> = [
180
+ 'onPointerLeave',
181
+ 'onPointerOut',
182
+ ]
183
+ fireEventsFromIntersections({
184
+ element,
185
+ eventKeys,
186
+ intersection,
187
+ })
188
+ })
189
+
190
+ currentIntersections = ([] as any).concat(entering, staying)
191
+ }
192
+
193
+ // update function
194
+ onBeforeRender(update)
195
+
196
+ const teardown = () => offBeforeRender(update)
197
+ onBeforeUnmount(teardown)
198
+
199
+ const clickEventKeys: Lunch.EventKey[] = [
200
+ 'onClick',
201
+ 'onPointerDown',
202
+ 'onPointerUp',
203
+ ]
204
+ watch(inputActive, (isDown) => {
205
+ // meshes with multiple intersections receive multiple callbacks by default -
206
+ // let's make it so they only receive one callback of each type per frame.
207
+ // (ie usually when you click on a mesh, you expect only one click event to fire, even
208
+ // if there are technically multiple intersections with that mesh)
209
+ const uuidsInteractedWithThisFrame: string[] = []
210
+ currentIntersections.forEach((v) => {
211
+ clickEventKeys.forEach((key) => {
212
+ const id = v.element.uuid + key
213
+ if (
214
+ isDown &&
215
+ (key === 'onClick' || key === 'onPointerDown')
216
+ ) {
217
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
218
+ v.element.eventListeners[key]?.forEach((cb) =>
219
+ cb({ intersection: v.intersection })
220
+ )
221
+ uuidsInteractedWithThisFrame.push(id)
222
+ }
223
+ } else if (!isDown && key === 'onPointerUp') {
224
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
225
+ v.element.eventListeners[key]?.forEach((cb) =>
226
+ cb({ intersection: v.intersection })
227
+ )
228
+ uuidsInteractedWithThisFrame.push(id)
229
+ }
230
+ }
231
+ })
232
+ })
233
+ })
234
+
235
+ // return arbitrary object to ensure instantiation
236
+ // TODO: why can't we return a <raycaster/> here?
237
+ return () => <object3D />
238
+ },
239
+ })
@@ -8,11 +8,12 @@ import {
8
8
  WatchSource,
9
9
  } from 'vue'
10
10
  import { cancelUpdate, cancelUpdateSource, MiniDom, update } from '../../core'
11
- import { Lunch, useApp, useGlobals } from '../..'
11
+ import { Lunch, useApp, useGlobals, useLunchboxInteractables } from '../..'
12
12
  import * as THREE from 'three'
13
13
  import { prepCanvas } from './prepCanvas'
14
14
  import { useUpdateGlobals, useStartCallbacks } from '../..'
15
15
  import { LunchboxScene } from './LunchboxScene'
16
+ import { LunchboxEventHandlers } from '../LunchboxEventHandlers'
16
17
 
17
18
  /** fixed & fill styling for container */
18
19
  const fillStyle = (position: string) => {
@@ -67,6 +68,8 @@ export const LunchboxWrapper = defineComponent({
67
68
  ;(THREE as any).ColorManagement.legacyMode = false
68
69
  }
69
70
 
71
+ const interactables = useLunchboxInteractables()
72
+
70
73
  // MOUNT
71
74
  // ====================
72
75
  onMounted(async () => {
@@ -104,26 +107,21 @@ export const LunchboxWrapper = defineComponent({
104
107
  }
105
108
  updateGlobals?.({ dpr })
106
109
 
107
- console.log(1)
108
110
  while (
109
111
  !renderer.value?.$el?.instance &&
110
112
  // TODO: remove `as any`
111
113
  !(renderer.value as any)?.component?.ctx.$el?.instance
112
114
  ) {
113
- console.log(2)
114
115
  await new Promise((r) => requestAnimationFrame(r))
115
116
  }
116
117
 
117
- console.log(3)
118
118
  while (
119
119
  !scene.value?.$el?.instance &&
120
120
  // TODO: remove `as any`
121
121
  !(scene.value as any)?.component?.ctx.$el?.instance
122
122
  ) {
123
- console.log(4)
124
123
  await new Promise((r) => requestAnimationFrame(r))
125
124
  }
126
- console.log(5)
127
125
 
128
126
  const normalizedRenderer = (renderer.value?.$el?.instance ??
129
127
  (renderer.value as any)?.component?.ctx.$el
@@ -293,6 +291,9 @@ export const LunchboxWrapper = defineComponent({
293
291
  {...consolidatedCameraProperties}
294
292
  />
295
293
  )}
294
+
295
+ {/* Lunchbox interaction handlers */}
296
+ {interactables?.value.length && <LunchboxEventHandlers />}
296
297
  </>
297
298
  )
298
299
  },
@@ -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({
@@ -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
@@ -1,18 +1,7 @@
1
- // import {
2
- //ensureRenderer,
3
- // ensuredScene,
4
- // ensuredCamera,
5
- // } from '.'
6
- import { Lunch } from '..'
7
- import { inject, toRaw, watch, WatchStopHandle } from 'vue'
1
+ import type { Lunch } from '..'
2
+ import { inject, toRaw, watch } from 'vue'
8
3
  import * as Keys from '../keys'
9
4
 
10
- // let frameID: number
11
- // let watchStopHandle: WatchStopHandle
12
-
13
- // export const beforeRender = [] as Lunch.UpdateCallback[]
14
- // export const afterRender = [] as Lunch.UpdateCallback[]
15
-
16
5
  const requestUpdate = (opts: Lunch.UpdateCallbackProperties) => {
17
6
  if (typeof opts.app.config.globalProperties.lunchbox.frameId === 'number') {
18
7
  cancelAnimationFrame(opts.app.config.globalProperties.lunchbox.frameId)
@@ -117,23 +106,6 @@ export const offAfterRender = (cb: Lunch.UpdateCallback | number) => {
117
106
  useBeforeRender().offBeforeRender?.(cb)
118
107
  }
119
108
 
120
- // export const onAfterRender = (cb: Lunch.UpdateCallback, index = Infinity) => {
121
- // if (index === Infinity) {
122
- // afterRender.push(cb)
123
- // } else {
124
- // afterRender.splice(index, 0, cb)
125
- // }
126
- // }
127
-
128
- // export const offAfterRender = (cb: Lunch.UpdateCallback | number) => {
129
- // if (isFinite(cb as number)) {
130
- // afterRender.splice(cb as number, 1)
131
- // } else {
132
- // const idx = afterRender.findIndex((v) => v == cb)
133
- // afterRender.splice(idx, 1)
134
- // }
135
- // }
136
-
137
109
  // TODO: document
138
110
  export const useCancelUpdate = () => {
139
111
  const frameId = inject<number>(Keys.frameIdKey)
@@ -1,34 +1,25 @@
1
1
  import { isEventKey, isLunchboxStandardNode } from '../utils'
2
2
  import { addEventListener } from './interaction'
3
3
  import { get, isNumber, set } from 'lodash'
4
- import { Lunch } from '..'
5
-
6
- /** Update the given node so all of its props are current. */
7
- export function updateAllObjectProps({ node }: { node: Lunch.Node }) {
8
- // set props
9
- const props = node.props || {}
10
- let output = node
11
- Object.keys(props).forEach((key) => {
12
- output = updateObjectProp({ node, key, value: props[key] })
13
- })
14
-
15
- return output
16
- }
4
+ import type { Lunch } from '..'
5
+ import type { Ref } from 'vue'
17
6
 
18
7
  /** Update a single prop on a given node. */
19
8
  export function updateObjectProp({
20
9
  node,
21
10
  key,
11
+ interactables,
22
12
  value,
23
13
  }: {
24
14
  node: Lunch.Node
25
15
  key: string
16
+ interactables: Ref<Lunch.Node[]>
26
17
  value: any
27
18
  }) {
28
19
  // handle and return early if prop is an event
29
20
  // (event list from react-three-fiber)
30
21
  if (isEventKey(key)) {
31
- return addEventListener({ node, key, value })
22
+ return addEventListener({ node, key, interactables, value })
32
23
  }
33
24
 
34
25
  // update THREE property
package/src/index.ts CHANGED
@@ -5,8 +5,9 @@ import {
5
5
  inject,
6
6
  watch,
7
7
  reactive,
8
+ Ref,
8
9
  } from 'vue'
9
- import { nodeOps } from './nodeOps'
10
+ import { createNodeOps } from './nodeOps'
10
11
  import {
11
12
  ensuredCamera,
12
13
  ensureRenderer,
@@ -17,7 +18,6 @@ import {
17
18
  import { components } from './components'
18
19
  import { Lunch } from './types'
19
20
 
20
- // export { lunchboxRootNode as lunchboxTree } from './core'
21
21
  export * from './core'
22
22
  export * from './types'
23
23
 
@@ -48,7 +48,7 @@ export function useScene(callback: (newScene: THREE.Scene) => void) {
48
48
  scene,
49
49
  (newVal) => {
50
50
  if (!newVal) return
51
- callback(newVal.value)
51
+ callback(newVal.value as THREE.Scene)
52
52
  },
53
53
  { immediate: true }
54
54
  )
@@ -137,11 +137,20 @@ export const onStart = (cb: Lunch.UpdateCallback, index = Infinity) => {
137
137
  }
138
138
  }
139
139
 
140
+ // TODO: document
141
+ export const useLunchboxInteractables = () =>
142
+ inject<Ref<Lunch.Node[]>>(Keys.lunchboxInteractables)
143
+
140
144
  // CREATE APP
141
145
  // ====================
142
146
  export const createApp = (root: Component) => {
147
+ const { nodeOps, interactables } = createNodeOps()
143
148
  const app = createRenderer(nodeOps).createApp(root) as Lunch.App
144
149
 
150
+ // provide Lunchbox interaction handlers flag (modified when user references events via
151
+ // @click, etc)
152
+ app.provide(Keys.lunchboxInteractables, interactables)
153
+
145
154
  // register all components
146
155
  // ====================
147
156
  Object.keys(components).forEach((key) => {
package/src/keys.ts CHANGED
@@ -20,5 +20,6 @@ export const appKey = Symbol()
20
20
  export const appRenderersKey = Symbol()
21
21
  export const appSceneKey = Symbol()
22
22
  export const appCameraKey = Symbol()
23
+ export const lunchboxInteractables = Symbol()
23
24
 
24
25
  export const startCallbackKey = Symbol()