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.
@@ -124,242 +124,11 @@
124
124
  return node;
125
125
  }
126
126
 
127
- const globalsInjectionKey = Symbol();
128
- const updateGlobalsInjectionKey = Symbol();
129
- const setCustomRenderKey = Symbol();
130
- const clearCustomRenderKey = Symbol();
131
- const beforeRenderKey = Symbol();
132
- const onBeforeRenderKey = Symbol();
133
- const offBeforeRenderKey = Symbol();
134
- const afterRenderKey = Symbol();
135
- const onAfterRenderKey = Symbol();
136
- const offAfterRenderKey = Symbol();
137
- const frameIdKey = Symbol();
138
- const watchStopHandleKey = Symbol();
139
- const appRootNodeKey = Symbol();
140
- const appKey = Symbol();
141
- const appRenderersKey = Symbol();
142
- const appSceneKey = Symbol();
143
- const appCameraKey = Symbol();
144
- const startCallbackKey = Symbol();
145
-
146
- const ensuredCamera = () => vue.inject(appCameraKey); // ENSURE RENDERER
147
- // ====================
148
-
149
- const ensureRenderer = () => vue.inject(appRenderersKey); // ENSURE SCENE
150
- // ====================
151
-
152
- const ensuredScene = () => vue.inject(appSceneKey);
153
-
154
- const interactables = [];
155
- const addInteractable = target => {
156
- interactables.push(target);
157
- };
158
- const removeInteractable = target => {
159
- const idx = interactables.indexOf(target);
160
-
161
- if (idx !== -1) {
162
- interactables.splice(idx, 1);
163
- }
164
- };
165
-
166
- /** Mouse is down, touch is pressed, etc */
167
-
168
- const inputActive = vue.ref(false);
169
-
170
- // let mouseDownListener: (event: MouseEvent) => void
171
- // let mouseUpListener: (event: MouseEvent) => void
172
-
173
- const mousePos = vue.ref({
174
- x: Infinity,
175
- y: Infinity
176
- }); // let autoRaycasterEventsInitialized = false
177
- // let frameID: number
178
- // export const setupAutoRaycaster = (node: Lunch.Node<THREE.Raycaster>) => {
179
- // const instance = node.instance
180
- // if (!instance) return
181
- // // TODO: inject doesn't work here. replace this raycaster with a component so we can
182
- // // `inject` in `setup`?
183
- // const appLevelGlobals = { dpr: window.devicePixelRatio } //useGlobals()
184
- // // add mouse events once renderer is ready
185
- // let stopWatcher: WatchStopHandle | null = null
186
- // stopWatcher = watch(
187
- // () => ensureRenderer.value,
188
- // (renderer) => {
189
- // // make sure renderer exists
190
- // if (!renderer?.instance) return
191
- // // cancel early if autoraycaster exists
192
- // if (autoRaycasterEventsInitialized) {
193
- // if (stopWatcher) stopWatcher()
194
- // return
195
- // }
196
- // // create mouse events
197
- // mouseMoveListener = (evt) => {
198
- // const screenWidth =
199
- // (renderer.instance!.domElement.width ?? 1) /
200
- // appLevelGlobals.dpr
201
- // const screenHeight =
202
- // (renderer.instance!.domElement.height ?? 1) /
203
- // appLevelGlobals.dpr
204
- // mousePos.value.x = (evt.offsetX / screenWidth) * 2 - 1
205
- // mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1
206
- // }
207
- // mouseDownListener = () => (inputActive.value = true)
208
- // mouseUpListener = () => (inputActive.value = false)
209
- // // add mouse events
210
- // renderer.instance.domElement.addEventListener(
211
- // 'mousemove',
212
- // mouseMoveListener
213
- // )
214
- // renderer.instance.domElement.addEventListener(
215
- // 'mousedown',
216
- // mouseDownListener
217
- // )
218
- // renderer.instance.domElement.addEventListener(
219
- // 'mouseup',
220
- // mouseUpListener
221
- // )
222
- // // TODO: add touch events
223
- // // process mouse events asynchronously, whenever the mouse state changes
224
- // watch(
225
- // () => [inputActive.value, mousePos.value.x, mousePos.value.y],
226
- // () => {
227
- // if (frameID) cancelAnimationFrame(frameID)
228
- // frameID = requestAnimationFrame(() => {
229
- // autoRaycasterBeforeRender()
230
- // })
231
- // }
232
- // )
233
- // // mark complete
234
- // autoRaycasterEventsInitialized = true
235
- // // cancel setup watcher
236
- // if (stopWatcher) {
237
- // stopWatcher()
238
- // }
239
- // },
240
- // { immediate: true }
241
- // )
242
- // }
243
- // AUTO-RAYCASTER CALLBACK
244
- // ====================
245
-
246
- let currentIntersections = []; // const autoRaycasterBeforeRender = () => {
247
- // // setup
248
- // const raycaster = ensuredRaycaster.value?.instance
249
- // const camera = ensuredCamera.value?.instance
250
- // if (!raycaster || !camera) return
251
- // raycaster.setFromCamera(globals.mousePos.value, camera)
252
- // const intersections = raycaster.intersectObjects(
253
- // interactables.map((v) => v.instance as any as THREE.Object3D)
254
- // )
255
- // let enterValues: Array<Intersection<THREE.Object3D>> = [],
256
- // sameValues: Array<Intersection<THREE.Object3D>> = [],
257
- // leaveValues: Array<Intersection<THREE.Object3D>> = [],
258
- // entering: Array<{
259
- // element: Lunch.Node
260
- // intersection: Intersection<THREE.Object3D>
261
- // }> = [],
262
- // staying: Array<{
263
- // element: Lunch.Node
264
- // intersection: Intersection<THREE.Object3D>
265
- // }> = []
266
- // // intersection arrays
267
- // leaveValues = currentIntersections.map((v) => v.intersection)
268
- // // element arrays
269
- // intersections?.forEach((intersection) => {
270
- // const currentIdx = currentIntersections.findIndex(
271
- // (v) => v.intersection.object === intersection.object
272
- // )
273
- // if (currentIdx === -1) {
274
- // // new intersection
275
- // enterValues.push(intersection)
276
- // const found = interactables.find(
277
- // (v) => v.instance?.uuid === intersection.object.uuid
278
- // )
279
- // if (found) {
280
- // entering.push({ element: found, intersection })
281
- // }
282
- // } else {
283
- // // existing intersection
284
- // sameValues.push(intersection)
285
- // const found = interactables.find(
286
- // (v) => v.instance?.uuid === intersection.object.uuid
287
- // )
288
- // if (found) {
289
- // staying.push({ element: found, intersection })
290
- // }
291
- // }
292
- // // this is a current intersection, so it won't be in our `leave` array
293
- // const leaveIdx = leaveValues.findIndex(
294
- // (v) => v.object.uuid === intersection.object.uuid
295
- // )
296
- // if (leaveIdx !== -1) {
297
- // leaveValues.splice(leaveIdx, 1)
298
- // }
299
- // })
300
- // const leaving: Array<{
301
- // element: Lunch.Node
302
- // intersection: Intersection<THREE.Object3D>
303
- // }> = leaveValues.map((intersection) => {
304
- // return {
305
- // element: interactables.find(
306
- // (interactable) =>
307
- // interactable.instance?.uuid === intersection.object.uuid
308
- // ) as any as Lunch.Node,
309
- // intersection,
310
- // }
311
- // })
312
- // // new interactions
313
- // entering.forEach(({ element, intersection }) => {
314
- // fireEventsFromIntersections({
315
- // element,
316
- // eventKeys: ['onPointerEnter'],
317
- // intersection,
318
- // })
319
- // })
320
- // // unchanged interactions
321
- // staying.forEach(({ element, intersection }) => {
322
- // const eventKeys: Array<Lunch.EventKey> = [
323
- // 'onPointerOver',
324
- // 'onPointerMove',
325
- // ]
326
- // fireEventsFromIntersections({ element, eventKeys, intersection })
327
- // })
328
- // // exited interactions
329
- // leaving.forEach(({ element, intersection }) => {
330
- // const eventKeys: Array<Lunch.EventKey> = [
331
- // 'onPointerLeave',
332
- // 'onPointerOut',
333
- // ]
334
- // fireEventsFromIntersections({ element, eventKeys, intersection })
335
- // })
336
- // currentIntersections = ([] as any).concat(entering, staying)
337
- // }
338
- // utility function for firing multiple callbacks and multiple events on a Lunchbox.Element
339
- // const fireEventsFromIntersections = ({
340
- // element,
341
- // eventKeys,
342
- // intersection,
343
- // }: {
344
- // element: Lunch.Node
345
- // eventKeys: Array<Lunch.EventKey>
346
- // intersection: Intersection<THREE.Object3D>
347
- // }) => {
348
- // if (!element) return
349
- // eventKeys.forEach((eventKey) => {
350
- // if (element.eventListeners[eventKey]) {
351
- // element.eventListeners[eventKey].forEach((cb) => {
352
- // cb({ intersection })
353
- // })
354
- // }
355
- // })
356
- // }
357
-
358
127
  /** Add an event listener to the given node. Also creates the event teardown function and any necessary raycaster/interaction dictionary updates. */
359
-
360
128
  function addEventListener({
361
129
  node,
362
130
  key,
131
+ interactables,
363
132
  value
364
133
  }) {
365
134
  // create new records for this key if needed
@@ -375,37 +144,18 @@
375
144
  node.eventListeners[key].push(value); // if we need it, let's get/create the main raycaster
376
145
 
377
146
  if (interactionsRequiringRaycaster.includes(key)) {
378
- // we're not using `v` here, we're just making sure the raycaster has been created
379
- // TODO: is this necessary?
380
- // const v = ensuredRaycaster.value
381
- if (node.instance && !interactables.includes(node)) {
382
- addInteractable(node);
383
- node.eventListenerRemoveFunctions[key].push(() => removeInteractable(node));
384
- }
385
- } // register click, pointerdown, pointerup
386
-
387
-
388
- if (key === 'onClick' || key === 'onPointerDown' || key === 'onPointerUp') {
389
- const stop = vue.watch(() => inputActive.value, isDown => {
390
- const idx = currentIntersections.map(v => v.element).findIndex(v => v.instance && v.instance.uuid === node.instance?.uuid);
391
-
392
- if (idx !== -1) {
393
- if (isDown && (key === 'onClick' || key === 'onPointerDown')) {
394
- node.eventListeners[key].forEach(func => {
395
- func({
396
- intersection: currentIntersections[idx].intersection
397
- });
398
- });
399
- } else if (!isDown && key === 'onPointerUp') {
400
- node.eventListeners[key].forEach(func => {
401
- func({
402
- intersection: currentIntersections[idx].intersection
403
- });
404
- });
147
+ if (node.instance && !interactables.value.includes(node)) {
148
+ // add to interactables
149
+ interactables.value.push(node);
150
+ node.eventListenerRemoveFunctions[key].push(() => {
151
+ // remove from interactables
152
+ const idx = interactables.value.indexOf(node);
153
+
154
+ if (idx !== -1) {
155
+ interactables.value.splice(idx, 1);
405
156
  }
406
- }
407
- });
408
- node.eventListenerRemoveFunctions[key].push(stop);
157
+ });
158
+ }
409
159
  }
410
160
 
411
161
  return node;
@@ -499,6 +249,194 @@
499
249
 
500
250
  });
501
251
 
252
+ const LunchboxEventHandlers = vue.defineComponent({
253
+ name: 'LunchboxEventHandlers',
254
+
255
+ setup() {
256
+ const interactables = useLunchboxInteractables();
257
+ const globals = useGlobals();
258
+ const mousePos = vue.ref({
259
+ x: Infinity,
260
+ y: Infinity
261
+ });
262
+ const inputActive = vue.ref(false);
263
+ let currentIntersections = [];
264
+ const raycaster = new THREE__namespace.Raycaster(new THREE__namespace.Vector3(), new THREE__namespace.Vector3(0, 0, -1));
265
+
266
+ const fireEventsFromIntersections = ({
267
+ element,
268
+ eventKeys,
269
+ intersection
270
+ }) => {
271
+ if (!element) return;
272
+ eventKeys.forEach(eventKey => {
273
+ if (element.eventListeners[eventKey]) {
274
+ element.eventListeners[eventKey].forEach(cb => {
275
+ cb({
276
+ intersection
277
+ });
278
+ });
279
+ }
280
+ });
281
+ }; // add mouse listener to renderer DOM element when the element is ready
282
+
283
+
284
+ onRendererReady(v => {
285
+ if (!v?.domElement) return; // we have a DOM element, so let's add mouse listeners
286
+
287
+ const {
288
+ domElement
289
+ } = v;
290
+
291
+ const mouseMoveListener = evt => {
292
+ const screenWidth = (domElement.width ?? 1) / globals.dpr;
293
+ const screenHeight = (domElement.height ?? 1) / globals.dpr;
294
+ mousePos.value.x = evt.offsetX / screenWidth * 2 - 1;
295
+ mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1;
296
+ };
297
+
298
+ const mouseDownListener = () => inputActive.value = true;
299
+
300
+ const mouseUpListener = () => inputActive.value = false; // add mouse events
301
+
302
+
303
+ domElement.addEventListener('pointermove', mouseMoveListener);
304
+ domElement.addEventListener('pointerdown', mouseDownListener);
305
+ domElement.addEventListener('pointerup', mouseUpListener);
306
+ });
307
+ const camera = useCamera();
308
+
309
+ const update = () => {
310
+ const c = camera.value;
311
+ if (!c) return; // console.log(camera.value)
312
+
313
+ raycaster.setFromCamera(mousePos.value, c);
314
+ const intersections = raycaster.intersectObjects(interactables?.value.map(v => v.instance) ?? []);
315
+ let leaveValues = [],
316
+ entering = [],
317
+ staying = []; // intersection arrays
318
+
319
+ leaveValues = currentIntersections.map(v => v.intersection); // element arrays
320
+
321
+ intersections?.forEach(intersection => {
322
+ const currentIdx = currentIntersections.findIndex(v => v.intersection.object === intersection.object);
323
+
324
+ if (currentIdx === -1) {
325
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
326
+
327
+ if (found) {
328
+ entering.push({
329
+ element: found,
330
+ intersection
331
+ });
332
+ }
333
+ } else {
334
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
335
+
336
+ if (found) {
337
+ staying.push({
338
+ element: found,
339
+ intersection
340
+ });
341
+ }
342
+ } // this is a current intersection, so it won't be in our `leave` array
343
+
344
+
345
+ const leaveIdx = leaveValues.findIndex(v => v.object.uuid === intersection.object.uuid);
346
+
347
+ if (leaveIdx !== -1) {
348
+ leaveValues.splice(leaveIdx, 1);
349
+ }
350
+ });
351
+ const leaving = leaveValues.map(intersection => {
352
+ return {
353
+ element: interactables?.value.find(interactable => interactable.instance?.uuid === intersection.object.uuid),
354
+ intersection
355
+ };
356
+ }); // new interactions
357
+
358
+ entering.forEach(({
359
+ element,
360
+ intersection
361
+ }) => {
362
+ fireEventsFromIntersections({
363
+ element,
364
+ eventKeys: ['onPointerEnter'],
365
+ intersection
366
+ });
367
+ }); // unchanged interactions
368
+
369
+ staying.forEach(({
370
+ element,
371
+ intersection
372
+ }) => {
373
+ const eventKeys = ['onPointerOver', 'onPointerMove'];
374
+ fireEventsFromIntersections({
375
+ element,
376
+ eventKeys,
377
+ intersection
378
+ });
379
+ }); // exited interactions
380
+
381
+ leaving.forEach(({
382
+ element,
383
+ intersection
384
+ }) => {
385
+ const eventKeys = ['onPointerLeave', 'onPointerOut'];
386
+ fireEventsFromIntersections({
387
+ element,
388
+ eventKeys,
389
+ intersection
390
+ });
391
+ });
392
+ currentIntersections = [].concat(entering, staying);
393
+ }; // update function
394
+
395
+
396
+ onBeforeRender(update);
397
+
398
+ const teardown = () => offBeforeRender(update);
399
+
400
+ vue.onBeforeUnmount(teardown);
401
+ const clickEventKeys = ['onClick', 'onPointerDown', 'onPointerUp'];
402
+ vue.watch(inputActive, isDown => {
403
+ // run raycaster on click (necessary when `update` is not automatically called,
404
+ // for example in `updateSource` functions)
405
+ update(); // meshes with multiple intersections receive multiple callbacks by default -
406
+ // let's make it so they only receive one callback of each type per frame.
407
+ // (ie usually when you click on a mesh, you expect only one click event to fire, even
408
+ // if there are technically multiple intersections with that mesh)
409
+
410
+ const uuidsInteractedWithThisFrame = [];
411
+ currentIntersections.forEach(v => {
412
+ clickEventKeys.forEach(key => {
413
+ const id = v.element.uuid + key;
414
+
415
+ if (isDown && (key === 'onClick' || key === 'onPointerDown')) {
416
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
417
+ v.element.eventListeners[key]?.forEach(cb => cb({
418
+ intersection: v.intersection
419
+ }));
420
+ uuidsInteractedWithThisFrame.push(id);
421
+ }
422
+ } else if (!isDown && key === 'onPointerUp') {
423
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
424
+ v.element.eventListeners[key]?.forEach(cb => cb({
425
+ intersection: v.intersection
426
+ }));
427
+ uuidsInteractedWithThisFrame.push(id);
428
+ }
429
+ }
430
+ });
431
+ });
432
+ }); // return arbitrary object to ensure instantiation
433
+ // TODO: why can't we return a <raycaster/> here?
434
+
435
+ return () => vue.createVNode(vue.resolveComponent("object3D"), null, null);
436
+ }
437
+
438
+ });
439
+
502
440
  /** fixed & fill styling for container */
503
441
 
504
442
  const fillStyle = position => {
@@ -551,9 +489,10 @@
551
489
 
552
490
  if (props.r3f && THREE__namespace?.ColorManagement) {
553
491
  THREE__namespace.ColorManagement.legacyMode = false;
554
- } // MOUNT
555
- // ====================
492
+ }
556
493
 
494
+ const interactables = useLunchboxInteractables(); // MOUNT
495
+ // ====================
557
496
 
558
497
  vue.onMounted(async () => {
559
498
  // canvas needs to exist (or user needs to handle it on their own)
@@ -589,23 +528,17 @@
589
528
  updateGlobals?.({
590
529
  dpr
591
530
  });
592
- console.log(1);
593
531
 
594
532
  while (!renderer.value?.$el?.instance && // TODO: remove `as any`
595
533
  !renderer.value?.component?.ctx.$el?.instance) {
596
- console.log(2);
597
534
  await new Promise(r => requestAnimationFrame(r));
598
535
  }
599
536
 
600
- console.log(3);
601
-
602
537
  while (!scene.value?.$el?.instance && // TODO: remove `as any`
603
538
  !scene.value?.component?.ctx.$el?.instance) {
604
- console.log(4);
605
539
  await new Promise(r => requestAnimationFrame(r));
606
540
  }
607
541
 
608
- console.log(5);
609
542
  const normalizedRenderer = renderer.value?.$el?.instance ?? renderer.value?.component?.ctx.$el?.instance;
610
543
  normalizedRenderer.setPixelRatio(globals.dpr);
611
544
  const normalizedScene = scene.value?.$el?.instance ?? scene.value?.component?.ctx.$el?.instance;
@@ -675,7 +608,37 @@
675
608
  // ====================
676
609
 
677
610
  const containerFillStyle = props.sizePolicy === 'container' ? 'static' : 'absolute';
678
- const canvasFillStyle = props.sizePolicy === 'container' ? 'static' : 'fixed';
611
+ const canvasFillStyle = props.sizePolicy === 'container' ? 'static' : 'fixed'; // REACTIVE CUSTOM CAMERAS
612
+ // ====================
613
+ // find first camera with `type.name` property
614
+ // (which indicates a Lunch.Node)
615
+
616
+ const activeCamera = vue.computed(() => {
617
+ const output = context.slots?.camera?.().find(c => c.type?.name);
618
+
619
+ if (output) {
620
+ return output;
621
+ }
622
+
623
+ return output;
624
+ }); // TODO: make custom cameras reactive
625
+
626
+ vue.watch(activeCamera, async (newVal, oldVal) => {
627
+ // console.log('got camera', newVal)
628
+ if (newVal && newVal?.props?.key !== oldVal?.props?.key) {
629
+ // TODO: remove cast
630
+ camera.value = newVal; // TODO: why isn't this updating app camera?
631
+ // const el = await waitFor(() => newVal.el)
632
+ // console.log(el)
633
+ // camera.value = el
634
+ // console.log(newVal.uuid)
635
+ // updateGlobals?.({ camera: el })
636
+ }
637
+ }, {
638
+ immediate: true
639
+ }); // RENDER FUNCTION
640
+ // ====================
641
+
679
642
  return () => vue.createVNode(vue.Fragment, null, [context.slots?.renderer?.()?.length ? // TODO: remove `as any` cast
680
643
  renderer.value = context.slots?.renderer?.()[0] : // ...otherwise, add canvas...
681
644
  vue.createVNode(vue.Fragment, null, [vue.createVNode("div", {
@@ -705,13 +668,13 @@
705
668
  }, {
706
669
  default: () => [context.slots?.default?.()]
707
670
  }), context.slots?.camera?.()?.length ? // TODO: remove `any` cast
708
- camera.value = context.slots?.camera?.()[0] : props.ortho || props.orthographic ? vue.createVNode(vue.resolveComponent("orthographicCamera"), vue.mergeProps({
671
+ camera.value : props.ortho || props.orthographic ? vue.createVNode(vue.resolveComponent("orthographicCamera"), vue.mergeProps({
709
672
  "ref": camera,
710
673
  "args": props.cameraArgs ?? []
711
674
  }, consolidatedCameraProperties), null) : vue.createVNode(vue.resolveComponent("perspectiveCamera"), vue.mergeProps({
712
675
  "ref": camera,
713
676
  "args": props.cameraArgs ?? [props.r3f ? 75 : 45, 0.5625, 1, 1000]
714
- }, consolidatedCameraProperties), null)]);
677
+ }, consolidatedCameraProperties), null), interactables?.value.length && vue.createVNode(LunchboxEventHandlers, null, null)]);
715
678
  }
716
679
 
717
680
  });
@@ -726,7 +689,7 @@
726
689
  'light', 'spotLightShadow', 'spotLight', 'pointLight', 'rectAreaLight', 'hemisphereLight', 'directionalLightShadow', 'directionalLight', 'ambientLight', 'lightShadow', 'ambientLightProbe', 'hemisphereLightProbe', 'lightProbe', // textures
727
690
  'texture', 'videoTexture', 'dataTexture', 'dataTexture3D', 'compressedTexture', 'cubeTexture', 'canvasTexture', 'depthTexture', // Texture loaders
728
691
  'textureLoader', // misc
729
- 'group', 'catmullRomCurve3', 'points', // helpers
692
+ 'group', 'catmullRomCurve3', 'points', 'raycaster', // helpers
730
693
  'cameraHelper', // cameras
731
694
  'camera', 'perspectiveCamera', 'orthographicCamera', 'cubeCamera', 'arrayCamera', // renderers
732
695
  'webGLRenderer'
@@ -761,7 +724,6 @@
761
724
  arrowHelper: ArrowHelperProps
762
725
  axesHelper: AxesHelperProps
763
726
  // misc
764
- raycaster: RaycasterProps
765
727
  vector2: Vector2Props
766
728
  vector3: Vector3Props
767
729
  vector4: Vector4Props
@@ -778,7 +740,7 @@
778
740
  */
779
741
  ];
780
742
 
781
- const catalogue = {};
743
+ const catalogue = {}; // component creation utility
782
744
 
783
745
  const createComponent$1 = tag => vue.defineComponent({
784
746
  inheritAttrs: false,
@@ -1174,9 +1136,25 @@
1174
1136
  return item?.minidomType === 'RendererNode';
1175
1137
  }
1176
1138
 
1177
- // let watchStopHandle: WatchStopHandle
1178
- // export const beforeRender = [] as Lunch.UpdateCallback[]
1179
- // export const afterRender = [] as Lunch.UpdateCallback[]
1139
+ const globalsInjectionKey = Symbol();
1140
+ const updateGlobalsInjectionKey = Symbol();
1141
+ const setCustomRenderKey = Symbol();
1142
+ const clearCustomRenderKey = Symbol();
1143
+ const beforeRenderKey = Symbol();
1144
+ const onBeforeRenderKey = Symbol();
1145
+ const offBeforeRenderKey = Symbol();
1146
+ const afterRenderKey = Symbol();
1147
+ const onAfterRenderKey = Symbol();
1148
+ const offAfterRenderKey = Symbol();
1149
+ const frameIdKey = Symbol();
1150
+ const watchStopHandleKey = Symbol();
1151
+ const appRootNodeKey = Symbol();
1152
+ const appKey = Symbol();
1153
+ const appRenderersKey = Symbol();
1154
+ const appSceneKey = Symbol();
1155
+ const appCameraKey = Symbol();
1156
+ const lunchboxInteractables = Symbol();
1157
+ const startCallbackKey = Symbol();
1180
1158
 
1181
1159
  const requestUpdate = opts => {
1182
1160
  if (typeof opts.app.config.globalProperties.lunchbox.frameId === 'number') {
@@ -1211,20 +1189,19 @@
1211
1189
  const {
1212
1190
  app,
1213
1191
  renderer,
1214
- scene,
1215
- camera
1192
+ scene
1216
1193
  } = opts; // BEFORE RENDER
1217
1194
 
1218
1195
  app.config.globalProperties.lunchbox.beforeRender.forEach(cb => {
1219
1196
  cb?.(opts);
1220
1197
  }); // RENDER
1221
1198
 
1222
- if (renderer && scene && camera) {
1199
+ if (renderer && scene && opts.app.config.globalProperties.lunchbox.camera) {
1223
1200
  if (app.customRender) {
1224
1201
  app.customRender(opts);
1225
1202
  } else {
1226
- renderer.render(vue.toRaw(scene), // opts.app.config.globalProperties.lunchbox.camera!
1227
- vue.toRaw(camera));
1203
+ renderer.render(vue.toRaw(scene), opts.app.config.globalProperties.lunchbox.camera // toRaw(camera)
1204
+ );
1228
1205
  }
1229
1206
  } // AFTER RENDER
1230
1207
 
@@ -1234,97 +1211,92 @@
1234
1211
  });
1235
1212
  }; // before render
1236
1213
  // ====================
1237
- // TODO: document
1214
+
1215
+ /** Obtain callback methods for `onBeforeRender` and `offBeforeRender`. Usually used internally by Lunchbox. */
1238
1216
 
1239
1217
  const useBeforeRender = () => {
1240
1218
  return {
1241
1219
  onBeforeRender: vue.inject(onBeforeRenderKey),
1242
1220
  offBeforeRender: vue.inject(offBeforeRenderKey)
1243
1221
  };
1244
- }; // TODO: document
1222
+ };
1223
+ /** Run a function before every render.
1224
+ *
1225
+ * Note that if `updateSource` is set in the Lunchbox wrapper component, this will **only** run
1226
+ * before a render triggered by that `updateSource`. Normally, the function should run every frame.
1227
+ */
1245
1228
 
1246
1229
  const onBeforeRender = (cb, index = Infinity) => {
1247
1230
  useBeforeRender().onBeforeRender?.(cb, index);
1248
- }; // TODO: document
1231
+ };
1232
+ /** Remove a function from the `beforeRender` callback list. Useful for tearing down functions added
1233
+ * by `onBeforeRender`.
1234
+ */
1249
1235
 
1250
1236
  const offBeforeRender = cb => {
1251
1237
  useBeforeRender().offBeforeRender?.(cb);
1252
1238
  }; // after render
1253
1239
  // ====================
1254
- // TODO: document
1240
+
1241
+ /** Obtain callback methods for `onAfterRender` and `offAfterRender`. Usually used internally by Lunchbox. */
1255
1242
 
1256
1243
  const useAfterRender = () => {
1257
1244
  return {
1258
1245
  onAfterRender: vue.inject(onBeforeRenderKey),
1259
1246
  offAfterRender: vue.inject(offBeforeRenderKey)
1260
1247
  };
1261
- }; // TODO: document
1248
+ };
1249
+ /** Run a function after every render.
1250
+ *
1251
+ * Note that if `updateSource` is set in the Lunchbox wrapper component, this will **only** run
1252
+ * after a render triggered by that `updateSource`. Normally, the function should run every frame.
1253
+ */
1262
1254
 
1263
1255
  const onAfterRender = (cb, index = Infinity) => {
1264
1256
  useBeforeRender().onBeforeRender?.(cb, index);
1265
- }; // TODO: document
1257
+ };
1258
+ /** Remove a function from the `afterRender` callback list. Useful for tearing down functions added
1259
+ * by `onAfterRender`.
1260
+ */
1266
1261
 
1267
1262
  const offAfterRender = cb => {
1268
1263
  useBeforeRender().offBeforeRender?.(cb);
1269
- }; // export const onAfterRender = (cb: Lunch.UpdateCallback, index = Infinity) => {
1270
- // if (index === Infinity) {
1271
- // afterRender.push(cb)
1272
- // } else {
1273
- // afterRender.splice(index, 0, cb)
1274
- // }
1275
- // }
1276
- // export const offAfterRender = (cb: Lunch.UpdateCallback | number) => {
1277
- // if (isFinite(cb as number)) {
1278
- // afterRender.splice(cb as number, 1)
1279
- // } else {
1280
- // const idx = afterRender.findIndex((v) => v == cb)
1281
- // afterRender.splice(idx, 1)
1282
- // }
1283
- // }
1284
- // TODO: document
1264
+ };
1265
+ /** Obtain a function used to cancel the current update frame. Use `cancelUpdate` if you wish
1266
+ * to immediately invoke the cancellation function. Usually used internally by Lunchbox.
1267
+ */
1285
1268
 
1286
1269
  const useCancelUpdate = () => {
1287
1270
  const frameId = vue.inject(frameIdKey);
1288
1271
  return () => {
1289
1272
  if (frameId !== undefined) cancelAnimationFrame(frameId);
1290
1273
  };
1291
- }; // TODO: document
1274
+ };
1275
+ /** Cancel the current update frame. Usually used internally by Lunchbox. */
1292
1276
 
1293
1277
  const cancelUpdate = () => {
1294
1278
  useCancelUpdate()?.();
1295
- }; // TODO: document
1279
+ };
1280
+ /** Obtain a function used to cancel an update source. Use `cancelUpdateSource` if you wish to
1281
+ * immediately invoke the cancellation function. Usually used internally by Lunchbox.
1282
+ */
1296
1283
 
1297
1284
  const useCancelUpdateSource = () => {
1298
1285
  const cancel = vue.inject(watchStopHandleKey);
1299
1286
  return () => cancel?.();
1300
- }; // TODO: document
1287
+ };
1288
+ /** Cancel an update source. Usually used internally by Lunchbox. */
1301
1289
 
1302
1290
  const cancelUpdateSource = () => {
1303
1291
  useCancelUpdateSource()?.();
1304
1292
  };
1305
1293
 
1306
- /** Update the given node so all of its props are current. */
1307
-
1308
- function updateAllObjectProps({
1309
- node
1310
- }) {
1311
- // set props
1312
- const props = node.props || {};
1313
- let output = node;
1314
- Object.keys(props).forEach(key => {
1315
- output = updateObjectProp({
1316
- node,
1317
- key,
1318
- value: props[key]
1319
- });
1320
- });
1321
- return output;
1322
- }
1323
1294
  /** Update a single prop on a given node. */
1324
1295
 
1325
1296
  function updateObjectProp({
1326
1297
  node,
1327
1298
  key,
1299
+ interactables,
1328
1300
  value
1329
1301
  }) {
1330
1302
  // handle and return early if prop is an event
@@ -1333,6 +1305,7 @@
1333
1305
  return addEventListener({
1334
1306
  node,
1335
1307
  key,
1308
+ interactables,
1336
1309
  value
1337
1310
  });
1338
1311
  } // update THREE property
@@ -1363,6 +1336,7 @@
1363
1336
  const fullPath = [nestedProperty, finalKey].filter(Boolean).join('.');
1364
1337
  liveProperty = liveProperty = lodash.get(target, fullPath);
1365
1338
  } // change property
1339
+ // first, save as array in case we need to spread it
1366
1340
 
1367
1341
 
1368
1342
  if (liveProperty && lodash.isNumber(value) && liveProperty.setScalar) {
@@ -1373,14 +1347,24 @@
1373
1347
  const nextValueAsArray = Array.isArray(value) ? value : [value];
1374
1348
  target[finalKey].set(...nextValueAsArray);
1375
1349
  } else if (typeof liveProperty === 'function') {
1376
- // if property is a function, let's try calling it
1377
- liveProperty.bind(node.instance)(...value); // pass the result to the parent
1350
+ // some function properties are set rather than called, so let's handle them
1351
+ if (finalKey.toLowerCase() === 'onbeforerender' || finalKey.toLowerCase() === 'onafterrender') {
1352
+ target[finalKey] = value;
1353
+ } else {
1354
+ if (!Array.isArray(value)) {
1355
+ throw new Error('Arguments on a declarative method must be wrapped in an array.\nWorks:\n<example :methodCall="[256]" />\nDoesn\'t work:\n<example :methodCall="256" />');
1356
+ } // if property is a function, let's try calling it
1357
+
1358
+
1359
+ liveProperty.bind(node.instance)(...value);
1360
+ } // pass the result to the parent
1378
1361
  // const parent = node.parentNode
1379
1362
  // if (parent) {
1380
1363
  // const parentAsLunchboxNode = parent as Lunchbox.Node
1381
1364
  // parentAsLunchboxNode.attached[finalKey] = result
1382
1365
  // ; (parentAsLunchboxNode.instance as any)[finalKey] = result
1383
1366
  // }
1367
+
1384
1368
  } else if (lodash.get(target, finalKey, undefined) !== undefined) {
1385
1369
  // blank strings evaluate to `true`
1386
1370
  // <mesh castShadow receiveShadow /> will work the same as
@@ -1588,101 +1572,133 @@
1588
1572
  Elements are `create`d from the outside in, then `insert`ed from the inside out.
1589
1573
  */
1590
1574
 
1591
- const nodeOps = {
1592
- createElement,
1593
-
1594
- createText(text) {
1595
- return createTextNode({
1596
- text
1597
- });
1598
- },
1599
-
1600
- createComment(text) {
1601
- return createCommentNode({
1602
- text
1603
- });
1604
- },
1605
-
1606
- insert,
1575
+ const createNodeOps = () => {
1576
+ // APP-LEVEL GLOBALS
1577
+ // ====================
1578
+ // These need to exist at the app level in a place where the node ops can access them.
1579
+ // It'd be better to set these via `app.provide` at app creation, but the node ops need access
1580
+ // to these values before the app is instantiated, so this is the next-best place for them to exist.
1581
+ const interactables = vue.ref([]); // NODE OPS
1582
+ // ====================
1607
1583
 
1608
- nextSibling(node) {
1609
- const result = node.nextSibling; // console.log('found', result)
1584
+ const nodeOps = {
1585
+ createElement,
1610
1586
 
1611
- if (!result) return null;
1612
- return result;
1613
- },
1614
-
1615
- parentNode(node) {
1616
- const result = node.parentNode;
1617
- if (!result) return null;
1618
- return result;
1619
- },
1587
+ createText(text) {
1588
+ return createTextNode({
1589
+ text
1590
+ });
1591
+ },
1620
1592
 
1621
- patchProp(node, key, prevValue, nextValue) {
1622
- if (isLunchboxDomComponent(node)) {
1623
- // handle DOM node
1624
- if (key === 'style') {
1625
- // special handling for style
1626
- Object.keys(nextValue).forEach(k => {
1627
- node.domElement.style[k] = nextValue[k];
1628
- });
1629
- } else {
1630
- node.domElement.setAttribute(key, nextValue);
1631
- }
1593
+ createComment(text) {
1594
+ return createCommentNode({
1595
+ text
1596
+ });
1597
+ },
1598
+
1599
+ insert,
1600
+
1601
+ nextSibling(node) {
1602
+ const result = node.nextSibling;
1603
+ if (!result) return null;
1604
+ return result;
1605
+ },
1606
+
1607
+ parentNode(node) {
1608
+ const result = node.parentNode;
1609
+ if (!result) return null;
1610
+ return result;
1611
+ },
1612
+
1613
+ patchProp(node, key, prevValue, nextValue) {
1614
+ if (isLunchboxDomComponent(node)) {
1615
+ // handle DOM node
1616
+ if (key === 'style') {
1617
+ // special handling for style
1618
+ Object.keys(nextValue).forEach(k => {
1619
+ node.domElement.style[k] = nextValue[k];
1620
+ });
1621
+ } else {
1622
+ node.domElement.setAttribute(key, nextValue);
1623
+ }
1632
1624
 
1633
- return;
1634
- } // ignore if root node, or Lunchbox internal prop
1625
+ return;
1626
+ } // ignore if root node, or Lunchbox internal prop
1635
1627
 
1636
1628
 
1637
- if (isLunchboxRootNode(node) || key.startsWith('$')) {
1638
- return;
1639
- } // otherwise, update prop
1629
+ if (isLunchboxRootNode(node) || key.startsWith('$')) {
1630
+ return;
1631
+ } // otherwise, update prop
1640
1632
 
1641
1633
 
1642
- updateObjectProp({
1643
- node: node,
1644
- key,
1645
- value: nextValue
1646
- });
1647
- },
1634
+ updateObjectProp({
1635
+ node: node,
1636
+ key,
1637
+ interactables,
1638
+ value: nextValue
1639
+ });
1640
+ },
1648
1641
 
1649
- remove,
1642
+ remove,
1650
1643
 
1651
- setElementText() {// noop
1652
- },
1644
+ setElementText() {// noop
1645
+ },
1653
1646
 
1654
- setText() {// noop
1655
- }
1647
+ setText() {// noop
1648
+ }
1656
1649
 
1650
+ };
1651
+ return {
1652
+ nodeOps,
1653
+ interactables
1654
+ };
1657
1655
  };
1658
1656
 
1659
- /** The current camera. Often easier to use `useCamera` instead of this. */
1660
- // TODO: update docs
1657
+ /** The current camera as a computed value. */
1661
1658
 
1662
- const camera = ensuredCamera; // TODO: update docs
1659
+ const useCamera = () => vue.inject(appCameraKey);
1660
+ /** Run a function using the current camera when it's present. */
1663
1661
 
1664
- const useCamera = () => ensuredCamera();
1665
- /** The current renderer as a computed value. Often easier to use `useRenderer` instead of this. */
1662
+ const onCameraReady = cb => {
1663
+ const stopWatch = vue.watch(useCamera(), newVal => {
1664
+ if (newVal) {
1665
+ cb(newVal);
1666
+ stopWatch();
1667
+ }
1668
+ }, {
1669
+ immediate: true
1670
+ });
1671
+ };
1672
+ /** The current renderer as a computed value. */
1666
1673
 
1667
- const renderer = ensureRenderer;
1674
+ const useRenderer = () => vue.inject(appRenderersKey);
1668
1675
  /** Run a function using the current renderer when it's present. */
1669
1676
 
1670
- const useRenderer = () => ensureRenderer();
1671
- /** The current scene. Often easier to use `useScene` instead of this. */
1672
- // TODO: update docs
1677
+ const onRendererReady = cb => {
1678
+ const stopWatch = vue.watch(useRenderer(), newVal => {
1679
+ if (newVal) {
1680
+ cb(newVal);
1681
+ stopWatch();
1682
+ }
1683
+ }, {
1684
+ immediate: true
1685
+ });
1686
+ };
1687
+ /** The current scene as a computed value. */
1673
1688
 
1674
- const scene = ensuredScene;
1689
+ const useScene = () => vue.inject(appSceneKey);
1675
1690
  /** Run a function using the current scene when it's present. */
1676
- // TODO: update docs
1677
1691
 
1678
- function useScene(callback) {
1679
- return vue.watch(scene, newVal => {
1680
- if (!newVal) return;
1681
- callback(newVal.value);
1692
+ const onSceneReady = cb => {
1693
+ const stopWatch = vue.watch(useScene(), newVal => {
1694
+ if (newVal) {
1695
+ cb(newVal);
1696
+ stopWatch();
1697
+ }
1682
1698
  }, {
1683
1699
  immediate: true
1684
1700
  });
1685
- } // CUSTOM RENDER SUPPORT
1701
+ }; // CUSTOM RENDER SUPPORT
1686
1702
  // ====================
1687
1703
 
1688
1704
  /** Set a custom render function, overriding the Lunchbox app's default render function.
@@ -1739,14 +1755,15 @@
1739
1755
 
1740
1756
  const updateGlobals = newValue => {
1741
1757
  useUpdateGlobals()?.(newValue);
1742
- }; // TODO: document
1743
-
1744
- const useRootNode = () => vue.inject(appRootNodeKey); // TODO: document
1758
+ };
1759
+ /** Use the current Lunchbox app. Usually used internally by Lunchbox. */
1745
1760
 
1746
- const useApp = () => vue.inject(appKey); // TODO: document
1761
+ const useApp = () => vue.inject(appKey);
1762
+ /** Obtain a list of the start callback functions. Usually used internally by Lunchbox. */
1747
1763
 
1748
- const useStartCallbacks = () => vue.inject(startCallbackKey); //[] as Lunch.UpdateCallback[]
1749
- // TODO: document
1764
+ const useStartCallbacks = () => vue.inject(startCallbackKey);
1765
+ /** Run a given callback once when the Lunchbox app starts. Include an index to
1766
+ * splice the callback at that index in the callback queue. */
1750
1767
 
1751
1768
  const onStart = (cb, index = Infinity) => {
1752
1769
  const callbacks = useStartCallbacks();
@@ -1756,11 +1773,21 @@
1756
1773
  } else {
1757
1774
  callbacks?.splice(index, 0, cb);
1758
1775
  }
1759
- }; // CREATE APP
1776
+ };
1777
+ /** Obtain a list of interactable objects (registered via onClick, onHover, etc events). Usually used internally by Lunchbox. */
1778
+
1779
+ const useLunchboxInteractables = () => vue.inject(lunchboxInteractables); // CREATE APP
1760
1780
  // ====================
1761
1781
 
1762
1782
  const createApp = root => {
1763
- const app = vue.createRenderer(nodeOps).createApp(root); // register all components
1783
+ const {
1784
+ nodeOps,
1785
+ interactables
1786
+ } = createNodeOps();
1787
+ const app = vue.createRenderer(nodeOps).createApp(root); // provide Lunchbox interaction handlers flag (modified when user references events via
1788
+ // @click, etc)
1789
+
1790
+ app.provide(lunchboxInteractables, interactables); // register all components
1764
1791
  // ====================
1765
1792
 
1766
1793
  Object.keys(components).forEach(key => {
@@ -1911,7 +1938,6 @@
1911
1938
  };
1912
1939
 
1913
1940
  exports.addEventListener = addEventListener;
1914
- exports.addInteractable = addInteractable;
1915
1941
  exports.afterRenderKey = afterRenderKey;
1916
1942
  exports.appCameraKey = appCameraKey;
1917
1943
  exports.appKey = appKey;
@@ -1919,7 +1945,6 @@
1919
1945
  exports.appRootNodeKey = appRootNodeKey;
1920
1946
  exports.appSceneKey = appSceneKey;
1921
1947
  exports.beforeRenderKey = beforeRenderKey;
1922
- exports.camera = camera;
1923
1948
  exports.cancelUpdate = cancelUpdate;
1924
1949
  exports.cancelUpdateSource = cancelUpdateSource;
1925
1950
  exports.clearCustomRender = clearCustomRender;
@@ -1929,19 +1954,13 @@
1929
1954
  exports.createDomNode = createDomNode;
1930
1955
  exports.createNode = createNode;
1931
1956
  exports.createTextNode = createTextNode;
1932
- exports.currentIntersections = currentIntersections;
1933
- exports.ensureRenderer = ensureRenderer;
1934
- exports.ensuredCamera = ensuredCamera;
1935
- exports.ensuredScene = ensuredScene;
1936
1957
  exports.extend = extend;
1937
1958
  exports.find = find;
1938
1959
  exports.frameIdKey = frameIdKey;
1939
1960
  exports.globalsInjectionKey = globalsInjectionKey;
1940
- exports.inputActive = inputActive;
1941
1961
  exports.instantiateThreeObject = instantiateThreeObject;
1942
- exports.interactables = interactables;
1943
1962
  exports.isMinidomNode = isMinidomNode;
1944
- exports.mousePos = mousePos;
1963
+ exports.lunchboxInteractables = lunchboxInteractables;
1945
1964
  exports.nestedPropertiesToCheck = nestedPropertiesToCheck;
1946
1965
  exports.offAfterRender = offAfterRender;
1947
1966
  exports.offAfterRenderKey = offAfterRenderKey;
@@ -1951,15 +1970,14 @@
1951
1970
  exports.onAfterRenderKey = onAfterRenderKey;
1952
1971
  exports.onBeforeRender = onBeforeRender;
1953
1972
  exports.onBeforeRenderKey = onBeforeRenderKey;
1973
+ exports.onCameraReady = onCameraReady;
1974
+ exports.onRendererReady = onRendererReady;
1975
+ exports.onSceneReady = onSceneReady;
1954
1976
  exports.onStart = onStart;
1955
- exports.removeInteractable = removeInteractable;
1956
- exports.renderer = renderer;
1957
- exports.scene = scene;
1958
1977
  exports.setCustomRender = setCustomRender;
1959
1978
  exports.setCustomRenderKey = setCustomRenderKey;
1960
1979
  exports.startCallbackKey = startCallbackKey;
1961
1980
  exports.update = update;
1962
- exports.updateAllObjectProps = updateAllObjectProps;
1963
1981
  exports.updateGlobals = updateGlobals;
1964
1982
  exports.updateGlobalsInjectionKey = updateGlobalsInjectionKey;
1965
1983
  exports.updateObjectProp = updateObjectProp;
@@ -1971,8 +1989,8 @@
1971
1989
  exports.useCancelUpdateSource = useCancelUpdateSource;
1972
1990
  exports.useCustomRender = useCustomRender;
1973
1991
  exports.useGlobals = useGlobals;
1992
+ exports.useLunchboxInteractables = useLunchboxInteractables;
1974
1993
  exports.useRenderer = useRenderer;
1975
- exports.useRootNode = useRootNode;
1976
1994
  exports.useScene = useScene;
1977
1995
  exports.useStartCallbacks = useStartCallbacks;
1978
1996
  exports.useUpdateGlobals = useUpdateGlobals;