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.
@@ -1,4 +1,4 @@
1
- import { isRef, isVNode, inject, ref, watch, toRaw, defineComponent, createVNode, resolveComponent, reactive, onMounted, onBeforeUnmount, Fragment, mergeProps, h, createRenderer, computed } from 'vue';
1
+ import { isRef, isVNode, toRaw, defineComponent, createVNode, resolveComponent, ref, onBeforeUnmount, watch, reactive, onMounted, computed, Fragment, mergeProps, h, inject, createRenderer } from 'vue';
2
2
  import * as THREE from 'three';
3
3
  import { get, isNumber, set } from 'lodash';
4
4
 
@@ -102,242 +102,11 @@ function createNode(options = {}, props = {}) {
102
102
  return node;
103
103
  }
104
104
 
105
- const globalsInjectionKey = Symbol();
106
- const updateGlobalsInjectionKey = Symbol();
107
- const setCustomRenderKey = Symbol();
108
- const clearCustomRenderKey = Symbol();
109
- const beforeRenderKey = Symbol();
110
- const onBeforeRenderKey = Symbol();
111
- const offBeforeRenderKey = Symbol();
112
- const afterRenderKey = Symbol();
113
- const onAfterRenderKey = Symbol();
114
- const offAfterRenderKey = Symbol();
115
- const frameIdKey = Symbol();
116
- const watchStopHandleKey = Symbol();
117
- const appRootNodeKey = Symbol();
118
- const appKey = Symbol();
119
- const appRenderersKey = Symbol();
120
- const appSceneKey = Symbol();
121
- const appCameraKey = Symbol();
122
- const startCallbackKey = Symbol();
123
-
124
- const ensuredCamera = () => inject(appCameraKey); // ENSURE RENDERER
125
- // ====================
126
-
127
- const ensureRenderer = () => inject(appRenderersKey); // ENSURE SCENE
128
- // ====================
129
-
130
- const ensuredScene = () => inject(appSceneKey);
131
-
132
- const interactables = [];
133
- const addInteractable = target => {
134
- interactables.push(target);
135
- };
136
- const removeInteractable = target => {
137
- const idx = interactables.indexOf(target);
138
-
139
- if (idx !== -1) {
140
- interactables.splice(idx, 1);
141
- }
142
- };
143
-
144
- /** Mouse is down, touch is pressed, etc */
145
-
146
- const inputActive = ref(false);
147
-
148
- // let mouseDownListener: (event: MouseEvent) => void
149
- // let mouseUpListener: (event: MouseEvent) => void
150
-
151
- const mousePos = ref({
152
- x: Infinity,
153
- y: Infinity
154
- }); // let autoRaycasterEventsInitialized = false
155
- // let frameID: number
156
- // export const setupAutoRaycaster = (node: Lunch.Node<THREE.Raycaster>) => {
157
- // const instance = node.instance
158
- // if (!instance) return
159
- // // TODO: inject doesn't work here. replace this raycaster with a component so we can
160
- // // `inject` in `setup`?
161
- // const appLevelGlobals = { dpr: window.devicePixelRatio } //useGlobals()
162
- // // add mouse events once renderer is ready
163
- // let stopWatcher: WatchStopHandle | null = null
164
- // stopWatcher = watch(
165
- // () => ensureRenderer.value,
166
- // (renderer) => {
167
- // // make sure renderer exists
168
- // if (!renderer?.instance) return
169
- // // cancel early if autoraycaster exists
170
- // if (autoRaycasterEventsInitialized) {
171
- // if (stopWatcher) stopWatcher()
172
- // return
173
- // }
174
- // // create mouse events
175
- // mouseMoveListener = (evt) => {
176
- // const screenWidth =
177
- // (renderer.instance!.domElement.width ?? 1) /
178
- // appLevelGlobals.dpr
179
- // const screenHeight =
180
- // (renderer.instance!.domElement.height ?? 1) /
181
- // appLevelGlobals.dpr
182
- // mousePos.value.x = (evt.offsetX / screenWidth) * 2 - 1
183
- // mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1
184
- // }
185
- // mouseDownListener = () => (inputActive.value = true)
186
- // mouseUpListener = () => (inputActive.value = false)
187
- // // add mouse events
188
- // renderer.instance.domElement.addEventListener(
189
- // 'mousemove',
190
- // mouseMoveListener
191
- // )
192
- // renderer.instance.domElement.addEventListener(
193
- // 'mousedown',
194
- // mouseDownListener
195
- // )
196
- // renderer.instance.domElement.addEventListener(
197
- // 'mouseup',
198
- // mouseUpListener
199
- // )
200
- // // TODO: add touch events
201
- // // process mouse events asynchronously, whenever the mouse state changes
202
- // watch(
203
- // () => [inputActive.value, mousePos.value.x, mousePos.value.y],
204
- // () => {
205
- // if (frameID) cancelAnimationFrame(frameID)
206
- // frameID = requestAnimationFrame(() => {
207
- // autoRaycasterBeforeRender()
208
- // })
209
- // }
210
- // )
211
- // // mark complete
212
- // autoRaycasterEventsInitialized = true
213
- // // cancel setup watcher
214
- // if (stopWatcher) {
215
- // stopWatcher()
216
- // }
217
- // },
218
- // { immediate: true }
219
- // )
220
- // }
221
- // AUTO-RAYCASTER CALLBACK
222
- // ====================
223
-
224
- let currentIntersections = []; // const autoRaycasterBeforeRender = () => {
225
- // // setup
226
- // const raycaster = ensuredRaycaster.value?.instance
227
- // const camera = ensuredCamera.value?.instance
228
- // if (!raycaster || !camera) return
229
- // raycaster.setFromCamera(globals.mousePos.value, camera)
230
- // const intersections = raycaster.intersectObjects(
231
- // interactables.map((v) => v.instance as any as THREE.Object3D)
232
- // )
233
- // let enterValues: Array<Intersection<THREE.Object3D>> = [],
234
- // sameValues: Array<Intersection<THREE.Object3D>> = [],
235
- // leaveValues: Array<Intersection<THREE.Object3D>> = [],
236
- // entering: Array<{
237
- // element: Lunch.Node
238
- // intersection: Intersection<THREE.Object3D>
239
- // }> = [],
240
- // staying: Array<{
241
- // element: Lunch.Node
242
- // intersection: Intersection<THREE.Object3D>
243
- // }> = []
244
- // // intersection arrays
245
- // leaveValues = currentIntersections.map((v) => v.intersection)
246
- // // element arrays
247
- // intersections?.forEach((intersection) => {
248
- // const currentIdx = currentIntersections.findIndex(
249
- // (v) => v.intersection.object === intersection.object
250
- // )
251
- // if (currentIdx === -1) {
252
- // // new intersection
253
- // enterValues.push(intersection)
254
- // const found = interactables.find(
255
- // (v) => v.instance?.uuid === intersection.object.uuid
256
- // )
257
- // if (found) {
258
- // entering.push({ element: found, intersection })
259
- // }
260
- // } else {
261
- // // existing intersection
262
- // sameValues.push(intersection)
263
- // const found = interactables.find(
264
- // (v) => v.instance?.uuid === intersection.object.uuid
265
- // )
266
- // if (found) {
267
- // staying.push({ element: found, intersection })
268
- // }
269
- // }
270
- // // this is a current intersection, so it won't be in our `leave` array
271
- // const leaveIdx = leaveValues.findIndex(
272
- // (v) => v.object.uuid === intersection.object.uuid
273
- // )
274
- // if (leaveIdx !== -1) {
275
- // leaveValues.splice(leaveIdx, 1)
276
- // }
277
- // })
278
- // const leaving: Array<{
279
- // element: Lunch.Node
280
- // intersection: Intersection<THREE.Object3D>
281
- // }> = leaveValues.map((intersection) => {
282
- // return {
283
- // element: interactables.find(
284
- // (interactable) =>
285
- // interactable.instance?.uuid === intersection.object.uuid
286
- // ) as any as Lunch.Node,
287
- // intersection,
288
- // }
289
- // })
290
- // // new interactions
291
- // entering.forEach(({ element, intersection }) => {
292
- // fireEventsFromIntersections({
293
- // element,
294
- // eventKeys: ['onPointerEnter'],
295
- // intersection,
296
- // })
297
- // })
298
- // // unchanged interactions
299
- // staying.forEach(({ element, intersection }) => {
300
- // const eventKeys: Array<Lunch.EventKey> = [
301
- // 'onPointerOver',
302
- // 'onPointerMove',
303
- // ]
304
- // fireEventsFromIntersections({ element, eventKeys, intersection })
305
- // })
306
- // // exited interactions
307
- // leaving.forEach(({ element, intersection }) => {
308
- // const eventKeys: Array<Lunch.EventKey> = [
309
- // 'onPointerLeave',
310
- // 'onPointerOut',
311
- // ]
312
- // fireEventsFromIntersections({ element, eventKeys, intersection })
313
- // })
314
- // currentIntersections = ([] as any).concat(entering, staying)
315
- // }
316
- // utility function for firing multiple callbacks and multiple events on a Lunchbox.Element
317
- // const fireEventsFromIntersections = ({
318
- // element,
319
- // eventKeys,
320
- // intersection,
321
- // }: {
322
- // element: Lunch.Node
323
- // eventKeys: Array<Lunch.EventKey>
324
- // intersection: Intersection<THREE.Object3D>
325
- // }) => {
326
- // if (!element) return
327
- // eventKeys.forEach((eventKey) => {
328
- // if (element.eventListeners[eventKey]) {
329
- // element.eventListeners[eventKey].forEach((cb) => {
330
- // cb({ intersection })
331
- // })
332
- // }
333
- // })
334
- // }
335
-
336
105
  /** Add an event listener to the given node. Also creates the event teardown function and any necessary raycaster/interaction dictionary updates. */
337
-
338
106
  function addEventListener({
339
107
  node,
340
108
  key,
109
+ interactables,
341
110
  value
342
111
  }) {
343
112
  // create new records for this key if needed
@@ -353,37 +122,18 @@ function addEventListener({
353
122
  node.eventListeners[key].push(value); // if we need it, let's get/create the main raycaster
354
123
 
355
124
  if (interactionsRequiringRaycaster.includes(key)) {
356
- // we're not using `v` here, we're just making sure the raycaster has been created
357
- // TODO: is this necessary?
358
- // const v = ensuredRaycaster.value
359
- if (node.instance && !interactables.includes(node)) {
360
- addInteractable(node);
361
- node.eventListenerRemoveFunctions[key].push(() => removeInteractable(node));
362
- }
363
- } // register click, pointerdown, pointerup
364
-
365
-
366
- if (key === 'onClick' || key === 'onPointerDown' || key === 'onPointerUp') {
367
- const stop = watch(() => inputActive.value, isDown => {
368
- const idx = currentIntersections.map(v => v.element).findIndex(v => v.instance && v.instance.uuid === node.instance?.uuid);
369
-
370
- if (idx !== -1) {
371
- if (isDown && (key === 'onClick' || key === 'onPointerDown')) {
372
- node.eventListeners[key].forEach(func => {
373
- func({
374
- intersection: currentIntersections[idx].intersection
375
- });
376
- });
377
- } else if (!isDown && key === 'onPointerUp') {
378
- node.eventListeners[key].forEach(func => {
379
- func({
380
- intersection: currentIntersections[idx].intersection
381
- });
382
- });
125
+ if (node.instance && !interactables.value.includes(node)) {
126
+ // add to interactables
127
+ interactables.value.push(node);
128
+ node.eventListenerRemoveFunctions[key].push(() => {
129
+ // remove from interactables
130
+ const idx = interactables.value.indexOf(node);
131
+
132
+ if (idx !== -1) {
133
+ interactables.value.splice(idx, 1);
383
134
  }
384
- }
385
- });
386
- node.eventListenerRemoveFunctions[key].push(stop);
135
+ });
136
+ }
387
137
  }
388
138
 
389
139
  return node;
@@ -477,6 +227,194 @@ const LunchboxScene = defineComponent({
477
227
 
478
228
  });
479
229
 
230
+ const LunchboxEventHandlers = defineComponent({
231
+ name: 'LunchboxEventHandlers',
232
+
233
+ setup() {
234
+ const interactables = useLunchboxInteractables();
235
+ const globals = useGlobals();
236
+ const mousePos = ref({
237
+ x: Infinity,
238
+ y: Infinity
239
+ });
240
+ const inputActive = ref(false);
241
+ let currentIntersections = [];
242
+ const raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, 0, -1));
243
+
244
+ const fireEventsFromIntersections = ({
245
+ element,
246
+ eventKeys,
247
+ intersection
248
+ }) => {
249
+ if (!element) return;
250
+ eventKeys.forEach(eventKey => {
251
+ if (element.eventListeners[eventKey]) {
252
+ element.eventListeners[eventKey].forEach(cb => {
253
+ cb({
254
+ intersection
255
+ });
256
+ });
257
+ }
258
+ });
259
+ }; // add mouse listener to renderer DOM element when the element is ready
260
+
261
+
262
+ onRendererReady(v => {
263
+ if (!v?.domElement) return; // we have a DOM element, so let's add mouse listeners
264
+
265
+ const {
266
+ domElement
267
+ } = v;
268
+
269
+ const mouseMoveListener = evt => {
270
+ const screenWidth = (domElement.width ?? 1) / globals.dpr;
271
+ const screenHeight = (domElement.height ?? 1) / globals.dpr;
272
+ mousePos.value.x = evt.offsetX / screenWidth * 2 - 1;
273
+ mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1;
274
+ };
275
+
276
+ const mouseDownListener = () => inputActive.value = true;
277
+
278
+ const mouseUpListener = () => inputActive.value = false; // add mouse events
279
+
280
+
281
+ domElement.addEventListener('pointermove', mouseMoveListener);
282
+ domElement.addEventListener('pointerdown', mouseDownListener);
283
+ domElement.addEventListener('pointerup', mouseUpListener);
284
+ });
285
+ const camera = useCamera();
286
+
287
+ const update = () => {
288
+ const c = camera.value;
289
+ if (!c) return; // console.log(camera.value)
290
+
291
+ raycaster.setFromCamera(mousePos.value, c);
292
+ const intersections = raycaster.intersectObjects(interactables?.value.map(v => v.instance) ?? []);
293
+ let leaveValues = [],
294
+ entering = [],
295
+ staying = []; // intersection arrays
296
+
297
+ leaveValues = currentIntersections.map(v => v.intersection); // element arrays
298
+
299
+ intersections?.forEach(intersection => {
300
+ const currentIdx = currentIntersections.findIndex(v => v.intersection.object === intersection.object);
301
+
302
+ if (currentIdx === -1) {
303
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
304
+
305
+ if (found) {
306
+ entering.push({
307
+ element: found,
308
+ intersection
309
+ });
310
+ }
311
+ } else {
312
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
313
+
314
+ if (found) {
315
+ staying.push({
316
+ element: found,
317
+ intersection
318
+ });
319
+ }
320
+ } // this is a current intersection, so it won't be in our `leave` array
321
+
322
+
323
+ const leaveIdx = leaveValues.findIndex(v => v.object.uuid === intersection.object.uuid);
324
+
325
+ if (leaveIdx !== -1) {
326
+ leaveValues.splice(leaveIdx, 1);
327
+ }
328
+ });
329
+ const leaving = leaveValues.map(intersection => {
330
+ return {
331
+ element: interactables?.value.find(interactable => interactable.instance?.uuid === intersection.object.uuid),
332
+ intersection
333
+ };
334
+ }); // new interactions
335
+
336
+ entering.forEach(({
337
+ element,
338
+ intersection
339
+ }) => {
340
+ fireEventsFromIntersections({
341
+ element,
342
+ eventKeys: ['onPointerEnter'],
343
+ intersection
344
+ });
345
+ }); // unchanged interactions
346
+
347
+ staying.forEach(({
348
+ element,
349
+ intersection
350
+ }) => {
351
+ const eventKeys = ['onPointerOver', 'onPointerMove'];
352
+ fireEventsFromIntersections({
353
+ element,
354
+ eventKeys,
355
+ intersection
356
+ });
357
+ }); // exited interactions
358
+
359
+ leaving.forEach(({
360
+ element,
361
+ intersection
362
+ }) => {
363
+ const eventKeys = ['onPointerLeave', 'onPointerOut'];
364
+ fireEventsFromIntersections({
365
+ element,
366
+ eventKeys,
367
+ intersection
368
+ });
369
+ });
370
+ currentIntersections = [].concat(entering, staying);
371
+ }; // update function
372
+
373
+
374
+ onBeforeRender(update);
375
+
376
+ const teardown = () => offBeforeRender(update);
377
+
378
+ onBeforeUnmount(teardown);
379
+ const clickEventKeys = ['onClick', 'onPointerDown', 'onPointerUp'];
380
+ watch(inputActive, isDown => {
381
+ // run raycaster on click (necessary when `update` is not automatically called,
382
+ // for example in `updateSource` functions)
383
+ update(); // meshes with multiple intersections receive multiple callbacks by default -
384
+ // let's make it so they only receive one callback of each type per frame.
385
+ // (ie usually when you click on a mesh, you expect only one click event to fire, even
386
+ // if there are technically multiple intersections with that mesh)
387
+
388
+ const uuidsInteractedWithThisFrame = [];
389
+ currentIntersections.forEach(v => {
390
+ clickEventKeys.forEach(key => {
391
+ const id = v.element.uuid + key;
392
+
393
+ if (isDown && (key === 'onClick' || key === 'onPointerDown')) {
394
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
395
+ v.element.eventListeners[key]?.forEach(cb => cb({
396
+ intersection: v.intersection
397
+ }));
398
+ uuidsInteractedWithThisFrame.push(id);
399
+ }
400
+ } else if (!isDown && key === 'onPointerUp') {
401
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
402
+ v.element.eventListeners[key]?.forEach(cb => cb({
403
+ intersection: v.intersection
404
+ }));
405
+ uuidsInteractedWithThisFrame.push(id);
406
+ }
407
+ }
408
+ });
409
+ });
410
+ }); // return arbitrary object to ensure instantiation
411
+ // TODO: why can't we return a <raycaster/> here?
412
+
413
+ return () => createVNode(resolveComponent("object3D"), null, null);
414
+ }
415
+
416
+ });
417
+
480
418
  /** fixed & fill styling for container */
481
419
 
482
420
  const fillStyle = position => {
@@ -529,9 +467,10 @@ const LunchboxWrapper = defineComponent({
529
467
 
530
468
  if (props.r3f && THREE?.ColorManagement) {
531
469
  THREE.ColorManagement.legacyMode = false;
532
- } // MOUNT
533
- // ====================
470
+ }
534
471
 
472
+ const interactables = useLunchboxInteractables(); // MOUNT
473
+ // ====================
535
474
 
536
475
  onMounted(async () => {
537
476
  // canvas needs to exist (or user needs to handle it on their own)
@@ -567,23 +506,17 @@ const LunchboxWrapper = defineComponent({
567
506
  updateGlobals?.({
568
507
  dpr
569
508
  });
570
- console.log(1);
571
509
 
572
510
  while (!renderer.value?.$el?.instance && // TODO: remove `as any`
573
511
  !renderer.value?.component?.ctx.$el?.instance) {
574
- console.log(2);
575
512
  await new Promise(r => requestAnimationFrame(r));
576
513
  }
577
514
 
578
- console.log(3);
579
-
580
515
  while (!scene.value?.$el?.instance && // TODO: remove `as any`
581
516
  !scene.value?.component?.ctx.$el?.instance) {
582
- console.log(4);
583
517
  await new Promise(r => requestAnimationFrame(r));
584
518
  }
585
519
 
586
- console.log(5);
587
520
  const normalizedRenderer = renderer.value?.$el?.instance ?? renderer.value?.component?.ctx.$el?.instance;
588
521
  normalizedRenderer.setPixelRatio(globals.dpr);
589
522
  const normalizedScene = scene.value?.$el?.instance ?? scene.value?.component?.ctx.$el?.instance;
@@ -653,7 +586,37 @@ const LunchboxWrapper = defineComponent({
653
586
  // ====================
654
587
 
655
588
  const containerFillStyle = props.sizePolicy === 'container' ? 'static' : 'absolute';
656
- const canvasFillStyle = props.sizePolicy === 'container' ? 'static' : 'fixed';
589
+ const canvasFillStyle = props.sizePolicy === 'container' ? 'static' : 'fixed'; // REACTIVE CUSTOM CAMERAS
590
+ // ====================
591
+ // find first camera with `type.name` property
592
+ // (which indicates a Lunch.Node)
593
+
594
+ const activeCamera = computed(() => {
595
+ const output = context.slots?.camera?.().find(c => c.type?.name);
596
+
597
+ if (output) {
598
+ return output;
599
+ }
600
+
601
+ return output;
602
+ }); // TODO: make custom cameras reactive
603
+
604
+ watch(activeCamera, async (newVal, oldVal) => {
605
+ // console.log('got camera', newVal)
606
+ if (newVal && newVal?.props?.key !== oldVal?.props?.key) {
607
+ // TODO: remove cast
608
+ camera.value = newVal; // TODO: why isn't this updating app camera?
609
+ // const el = await waitFor(() => newVal.el)
610
+ // console.log(el)
611
+ // camera.value = el
612
+ // console.log(newVal.uuid)
613
+ // updateGlobals?.({ camera: el })
614
+ }
615
+ }, {
616
+ immediate: true
617
+ }); // RENDER FUNCTION
618
+ // ====================
619
+
657
620
  return () => createVNode(Fragment, null, [context.slots?.renderer?.()?.length ? // TODO: remove `as any` cast
658
621
  renderer.value = context.slots?.renderer?.()[0] : // ...otherwise, add canvas...
659
622
  createVNode(Fragment, null, [createVNode("div", {
@@ -683,13 +646,13 @@ const LunchboxWrapper = defineComponent({
683
646
  }, {
684
647
  default: () => [context.slots?.default?.()]
685
648
  }), context.slots?.camera?.()?.length ? // TODO: remove `any` cast
686
- camera.value = context.slots?.camera?.()[0] : props.ortho || props.orthographic ? createVNode(resolveComponent("orthographicCamera"), mergeProps({
649
+ camera.value : props.ortho || props.orthographic ? createVNode(resolveComponent("orthographicCamera"), mergeProps({
687
650
  "ref": camera,
688
651
  "args": props.cameraArgs ?? []
689
652
  }, consolidatedCameraProperties), null) : createVNode(resolveComponent("perspectiveCamera"), mergeProps({
690
653
  "ref": camera,
691
654
  "args": props.cameraArgs ?? [props.r3f ? 75 : 45, 0.5625, 1, 1000]
692
- }, consolidatedCameraProperties), null)]);
655
+ }, consolidatedCameraProperties), null), interactables?.value.length && createVNode(LunchboxEventHandlers, null, null)]);
693
656
  }
694
657
 
695
658
  });
@@ -704,7 +667,7 @@ const autoGeneratedComponents = [// ThreeJS basics
704
667
  'light', 'spotLightShadow', 'spotLight', 'pointLight', 'rectAreaLight', 'hemisphereLight', 'directionalLightShadow', 'directionalLight', 'ambientLight', 'lightShadow', 'ambientLightProbe', 'hemisphereLightProbe', 'lightProbe', // textures
705
668
  'texture', 'videoTexture', 'dataTexture', 'dataTexture3D', 'compressedTexture', 'cubeTexture', 'canvasTexture', 'depthTexture', // Texture loaders
706
669
  'textureLoader', // misc
707
- 'group', 'catmullRomCurve3', 'points', // helpers
670
+ 'group', 'catmullRomCurve3', 'points', 'raycaster', // helpers
708
671
  'cameraHelper', // cameras
709
672
  'camera', 'perspectiveCamera', 'orthographicCamera', 'cubeCamera', 'arrayCamera', // renderers
710
673
  'webGLRenderer'
@@ -739,7 +702,6 @@ planeHelper: PlaneHelperProps
739
702
  arrowHelper: ArrowHelperProps
740
703
  axesHelper: AxesHelperProps
741
704
  // misc
742
- raycaster: RaycasterProps
743
705
  vector2: Vector2Props
744
706
  vector3: Vector3Props
745
707
  vector4: Vector4Props
@@ -756,7 +718,7 @@ shape: ShapeProps
756
718
  */
757
719
  ];
758
720
 
759
- const catalogue = {};
721
+ const catalogue = {}; // component creation utility
760
722
 
761
723
  const createComponent$1 = tag => defineComponent({
762
724
  inheritAttrs: false,
@@ -1152,9 +1114,25 @@ function isMinidomNode(item) {
1152
1114
  return item?.minidomType === 'RendererNode';
1153
1115
  }
1154
1116
 
1155
- // let watchStopHandle: WatchStopHandle
1156
- // export const beforeRender = [] as Lunch.UpdateCallback[]
1157
- // export const afterRender = [] as Lunch.UpdateCallback[]
1117
+ const globalsInjectionKey = Symbol();
1118
+ const updateGlobalsInjectionKey = Symbol();
1119
+ const setCustomRenderKey = Symbol();
1120
+ const clearCustomRenderKey = Symbol();
1121
+ const beforeRenderKey = Symbol();
1122
+ const onBeforeRenderKey = Symbol();
1123
+ const offBeforeRenderKey = Symbol();
1124
+ const afterRenderKey = Symbol();
1125
+ const onAfterRenderKey = Symbol();
1126
+ const offAfterRenderKey = Symbol();
1127
+ const frameIdKey = Symbol();
1128
+ const watchStopHandleKey = Symbol();
1129
+ const appRootNodeKey = Symbol();
1130
+ const appKey = Symbol();
1131
+ const appRenderersKey = Symbol();
1132
+ const appSceneKey = Symbol();
1133
+ const appCameraKey = Symbol();
1134
+ const lunchboxInteractables = Symbol();
1135
+ const startCallbackKey = Symbol();
1158
1136
 
1159
1137
  const requestUpdate = opts => {
1160
1138
  if (typeof opts.app.config.globalProperties.lunchbox.frameId === 'number') {
@@ -1189,20 +1167,19 @@ const update = opts => {
1189
1167
  const {
1190
1168
  app,
1191
1169
  renderer,
1192
- scene,
1193
- camera
1170
+ scene
1194
1171
  } = opts; // BEFORE RENDER
1195
1172
 
1196
1173
  app.config.globalProperties.lunchbox.beforeRender.forEach(cb => {
1197
1174
  cb?.(opts);
1198
1175
  }); // RENDER
1199
1176
 
1200
- if (renderer && scene && camera) {
1177
+ if (renderer && scene && opts.app.config.globalProperties.lunchbox.camera) {
1201
1178
  if (app.customRender) {
1202
1179
  app.customRender(opts);
1203
1180
  } else {
1204
- renderer.render(toRaw(scene), // opts.app.config.globalProperties.lunchbox.camera!
1205
- toRaw(camera));
1181
+ renderer.render(toRaw(scene), opts.app.config.globalProperties.lunchbox.camera // toRaw(camera)
1182
+ );
1206
1183
  }
1207
1184
  } // AFTER RENDER
1208
1185
 
@@ -1212,97 +1189,92 @@ const update = opts => {
1212
1189
  });
1213
1190
  }; // before render
1214
1191
  // ====================
1215
- // TODO: document
1192
+
1193
+ /** Obtain callback methods for `onBeforeRender` and `offBeforeRender`. Usually used internally by Lunchbox. */
1216
1194
 
1217
1195
  const useBeforeRender = () => {
1218
1196
  return {
1219
1197
  onBeforeRender: inject(onBeforeRenderKey),
1220
1198
  offBeforeRender: inject(offBeforeRenderKey)
1221
1199
  };
1222
- }; // TODO: document
1200
+ };
1201
+ /** Run a function before every render.
1202
+ *
1203
+ * Note that if `updateSource` is set in the Lunchbox wrapper component, this will **only** run
1204
+ * before a render triggered by that `updateSource`. Normally, the function should run every frame.
1205
+ */
1223
1206
 
1224
1207
  const onBeforeRender = (cb, index = Infinity) => {
1225
1208
  useBeforeRender().onBeforeRender?.(cb, index);
1226
- }; // TODO: document
1209
+ };
1210
+ /** Remove a function from the `beforeRender` callback list. Useful for tearing down functions added
1211
+ * by `onBeforeRender`.
1212
+ */
1227
1213
 
1228
1214
  const offBeforeRender = cb => {
1229
1215
  useBeforeRender().offBeforeRender?.(cb);
1230
1216
  }; // after render
1231
1217
  // ====================
1232
- // TODO: document
1218
+
1219
+ /** Obtain callback methods for `onAfterRender` and `offAfterRender`. Usually used internally by Lunchbox. */
1233
1220
 
1234
1221
  const useAfterRender = () => {
1235
1222
  return {
1236
1223
  onAfterRender: inject(onBeforeRenderKey),
1237
1224
  offAfterRender: inject(offBeforeRenderKey)
1238
1225
  };
1239
- }; // TODO: document
1226
+ };
1227
+ /** Run a function after every render.
1228
+ *
1229
+ * Note that if `updateSource` is set in the Lunchbox wrapper component, this will **only** run
1230
+ * after a render triggered by that `updateSource`. Normally, the function should run every frame.
1231
+ */
1240
1232
 
1241
1233
  const onAfterRender = (cb, index = Infinity) => {
1242
1234
  useBeforeRender().onBeforeRender?.(cb, index);
1243
- }; // TODO: document
1235
+ };
1236
+ /** Remove a function from the `afterRender` callback list. Useful for tearing down functions added
1237
+ * by `onAfterRender`.
1238
+ */
1244
1239
 
1245
1240
  const offAfterRender = cb => {
1246
1241
  useBeforeRender().offBeforeRender?.(cb);
1247
- }; // export const onAfterRender = (cb: Lunch.UpdateCallback, index = Infinity) => {
1248
- // if (index === Infinity) {
1249
- // afterRender.push(cb)
1250
- // } else {
1251
- // afterRender.splice(index, 0, cb)
1252
- // }
1253
- // }
1254
- // export const offAfterRender = (cb: Lunch.UpdateCallback | number) => {
1255
- // if (isFinite(cb as number)) {
1256
- // afterRender.splice(cb as number, 1)
1257
- // } else {
1258
- // const idx = afterRender.findIndex((v) => v == cb)
1259
- // afterRender.splice(idx, 1)
1260
- // }
1261
- // }
1262
- // TODO: document
1242
+ };
1243
+ /** Obtain a function used to cancel the current update frame. Use `cancelUpdate` if you wish
1244
+ * to immediately invoke the cancellation function. Usually used internally by Lunchbox.
1245
+ */
1263
1246
 
1264
1247
  const useCancelUpdate = () => {
1265
1248
  const frameId = inject(frameIdKey);
1266
1249
  return () => {
1267
1250
  if (frameId !== undefined) cancelAnimationFrame(frameId);
1268
1251
  };
1269
- }; // TODO: document
1252
+ };
1253
+ /** Cancel the current update frame. Usually used internally by Lunchbox. */
1270
1254
 
1271
1255
  const cancelUpdate = () => {
1272
1256
  useCancelUpdate()?.();
1273
- }; // TODO: document
1257
+ };
1258
+ /** Obtain a function used to cancel an update source. Use `cancelUpdateSource` if you wish to
1259
+ * immediately invoke the cancellation function. Usually used internally by Lunchbox.
1260
+ */
1274
1261
 
1275
1262
  const useCancelUpdateSource = () => {
1276
1263
  const cancel = inject(watchStopHandleKey);
1277
1264
  return () => cancel?.();
1278
- }; // TODO: document
1265
+ };
1266
+ /** Cancel an update source. Usually used internally by Lunchbox. */
1279
1267
 
1280
1268
  const cancelUpdateSource = () => {
1281
1269
  useCancelUpdateSource()?.();
1282
1270
  };
1283
1271
 
1284
- /** Update the given node so all of its props are current. */
1285
-
1286
- function updateAllObjectProps({
1287
- node
1288
- }) {
1289
- // set props
1290
- const props = node.props || {};
1291
- let output = node;
1292
- Object.keys(props).forEach(key => {
1293
- output = updateObjectProp({
1294
- node,
1295
- key,
1296
- value: props[key]
1297
- });
1298
- });
1299
- return output;
1300
- }
1301
1272
  /** Update a single prop on a given node. */
1302
1273
 
1303
1274
  function updateObjectProp({
1304
1275
  node,
1305
1276
  key,
1277
+ interactables,
1306
1278
  value
1307
1279
  }) {
1308
1280
  // handle and return early if prop is an event
@@ -1311,6 +1283,7 @@ function updateObjectProp({
1311
1283
  return addEventListener({
1312
1284
  node,
1313
1285
  key,
1286
+ interactables,
1314
1287
  value
1315
1288
  });
1316
1289
  } // update THREE property
@@ -1341,6 +1314,7 @@ function updateObjectProp({
1341
1314
  const fullPath = [nestedProperty, finalKey].filter(Boolean).join('.');
1342
1315
  liveProperty = liveProperty = get(target, fullPath);
1343
1316
  } // change property
1317
+ // first, save as array in case we need to spread it
1344
1318
 
1345
1319
 
1346
1320
  if (liveProperty && isNumber(value) && liveProperty.setScalar) {
@@ -1351,14 +1325,24 @@ function updateObjectProp({
1351
1325
  const nextValueAsArray = Array.isArray(value) ? value : [value];
1352
1326
  target[finalKey].set(...nextValueAsArray);
1353
1327
  } else if (typeof liveProperty === 'function') {
1354
- // if property is a function, let's try calling it
1355
- liveProperty.bind(node.instance)(...value); // pass the result to the parent
1328
+ // some function properties are set rather than called, so let's handle them
1329
+ if (finalKey.toLowerCase() === 'onbeforerender' || finalKey.toLowerCase() === 'onafterrender') {
1330
+ target[finalKey] = value;
1331
+ } else {
1332
+ if (!Array.isArray(value)) {
1333
+ 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" />');
1334
+ } // if property is a function, let's try calling it
1335
+
1336
+
1337
+ liveProperty.bind(node.instance)(...value);
1338
+ } // pass the result to the parent
1356
1339
  // const parent = node.parentNode
1357
1340
  // if (parent) {
1358
1341
  // const parentAsLunchboxNode = parent as Lunchbox.Node
1359
1342
  // parentAsLunchboxNode.attached[finalKey] = result
1360
1343
  // ; (parentAsLunchboxNode.instance as any)[finalKey] = result
1361
1344
  // }
1345
+
1362
1346
  } else if (get(target, finalKey, undefined) !== undefined) {
1363
1347
  // blank strings evaluate to `true`
1364
1348
  // <mesh castShadow receiveShadow /> will work the same as
@@ -1566,101 +1550,133 @@ const remove = node => {
1566
1550
  Elements are `create`d from the outside in, then `insert`ed from the inside out.
1567
1551
  */
1568
1552
 
1569
- const nodeOps = {
1570
- createElement,
1571
-
1572
- createText(text) {
1573
- return createTextNode({
1574
- text
1575
- });
1576
- },
1577
-
1578
- createComment(text) {
1579
- return createCommentNode({
1580
- text
1581
- });
1582
- },
1583
-
1584
- insert,
1553
+ const createNodeOps = () => {
1554
+ // APP-LEVEL GLOBALS
1555
+ // ====================
1556
+ // These need to exist at the app level in a place where the node ops can access them.
1557
+ // It'd be better to set these via `app.provide` at app creation, but the node ops need access
1558
+ // to these values before the app is instantiated, so this is the next-best place for them to exist.
1559
+ const interactables = ref([]); // NODE OPS
1560
+ // ====================
1585
1561
 
1586
- nextSibling(node) {
1587
- const result = node.nextSibling; // console.log('found', result)
1562
+ const nodeOps = {
1563
+ createElement,
1588
1564
 
1589
- if (!result) return null;
1590
- return result;
1591
- },
1592
-
1593
- parentNode(node) {
1594
- const result = node.parentNode;
1595
- if (!result) return null;
1596
- return result;
1597
- },
1565
+ createText(text) {
1566
+ return createTextNode({
1567
+ text
1568
+ });
1569
+ },
1598
1570
 
1599
- patchProp(node, key, prevValue, nextValue) {
1600
- if (isLunchboxDomComponent(node)) {
1601
- // handle DOM node
1602
- if (key === 'style') {
1603
- // special handling for style
1604
- Object.keys(nextValue).forEach(k => {
1605
- node.domElement.style[k] = nextValue[k];
1606
- });
1607
- } else {
1608
- node.domElement.setAttribute(key, nextValue);
1609
- }
1571
+ createComment(text) {
1572
+ return createCommentNode({
1573
+ text
1574
+ });
1575
+ },
1576
+
1577
+ insert,
1578
+
1579
+ nextSibling(node) {
1580
+ const result = node.nextSibling;
1581
+ if (!result) return null;
1582
+ return result;
1583
+ },
1584
+
1585
+ parentNode(node) {
1586
+ const result = node.parentNode;
1587
+ if (!result) return null;
1588
+ return result;
1589
+ },
1590
+
1591
+ patchProp(node, key, prevValue, nextValue) {
1592
+ if (isLunchboxDomComponent(node)) {
1593
+ // handle DOM node
1594
+ if (key === 'style') {
1595
+ // special handling for style
1596
+ Object.keys(nextValue).forEach(k => {
1597
+ node.domElement.style[k] = nextValue[k];
1598
+ });
1599
+ } else {
1600
+ node.domElement.setAttribute(key, nextValue);
1601
+ }
1610
1602
 
1611
- return;
1612
- } // ignore if root node, or Lunchbox internal prop
1603
+ return;
1604
+ } // ignore if root node, or Lunchbox internal prop
1613
1605
 
1614
1606
 
1615
- if (isLunchboxRootNode(node) || key.startsWith('$')) {
1616
- return;
1617
- } // otherwise, update prop
1607
+ if (isLunchboxRootNode(node) || key.startsWith('$')) {
1608
+ return;
1609
+ } // otherwise, update prop
1618
1610
 
1619
1611
 
1620
- updateObjectProp({
1621
- node: node,
1622
- key,
1623
- value: nextValue
1624
- });
1625
- },
1612
+ updateObjectProp({
1613
+ node: node,
1614
+ key,
1615
+ interactables,
1616
+ value: nextValue
1617
+ });
1618
+ },
1626
1619
 
1627
- remove,
1620
+ remove,
1628
1621
 
1629
- setElementText() {// noop
1630
- },
1622
+ setElementText() {// noop
1623
+ },
1631
1624
 
1632
- setText() {// noop
1633
- }
1625
+ setText() {// noop
1626
+ }
1634
1627
 
1628
+ };
1629
+ return {
1630
+ nodeOps,
1631
+ interactables
1632
+ };
1635
1633
  };
1636
1634
 
1637
- /** The current camera. Often easier to use `useCamera` instead of this. */
1638
- // TODO: update docs
1635
+ /** The current camera as a computed value. */
1639
1636
 
1640
- const camera = ensuredCamera; // TODO: update docs
1637
+ const useCamera = () => inject(appCameraKey);
1638
+ /** Run a function using the current camera when it's present. */
1641
1639
 
1642
- const useCamera = () => ensuredCamera();
1643
- /** The current renderer as a computed value. Often easier to use `useRenderer` instead of this. */
1640
+ const onCameraReady = cb => {
1641
+ const stopWatch = watch(useCamera(), newVal => {
1642
+ if (newVal) {
1643
+ cb(newVal);
1644
+ stopWatch();
1645
+ }
1646
+ }, {
1647
+ immediate: true
1648
+ });
1649
+ };
1650
+ /** The current renderer as a computed value. */
1644
1651
 
1645
- const renderer = ensureRenderer;
1652
+ const useRenderer = () => inject(appRenderersKey);
1646
1653
  /** Run a function using the current renderer when it's present. */
1647
1654
 
1648
- const useRenderer = () => ensureRenderer();
1649
- /** The current scene. Often easier to use `useScene` instead of this. */
1650
- // TODO: update docs
1655
+ const onRendererReady = cb => {
1656
+ const stopWatch = watch(useRenderer(), newVal => {
1657
+ if (newVal) {
1658
+ cb(newVal);
1659
+ stopWatch();
1660
+ }
1661
+ }, {
1662
+ immediate: true
1663
+ });
1664
+ };
1665
+ /** The current scene as a computed value. */
1651
1666
 
1652
- const scene = ensuredScene;
1667
+ const useScene = () => inject(appSceneKey);
1653
1668
  /** Run a function using the current scene when it's present. */
1654
- // TODO: update docs
1655
1669
 
1656
- function useScene(callback) {
1657
- return watch(scene, newVal => {
1658
- if (!newVal) return;
1659
- callback(newVal.value);
1670
+ const onSceneReady = cb => {
1671
+ const stopWatch = watch(useScene(), newVal => {
1672
+ if (newVal) {
1673
+ cb(newVal);
1674
+ stopWatch();
1675
+ }
1660
1676
  }, {
1661
1677
  immediate: true
1662
1678
  });
1663
- } // CUSTOM RENDER SUPPORT
1679
+ }; // CUSTOM RENDER SUPPORT
1664
1680
  // ====================
1665
1681
 
1666
1682
  /** Set a custom render function, overriding the Lunchbox app's default render function.
@@ -1717,14 +1733,15 @@ const useUpdateGlobals = () => inject(updateGlobalsInjectionKey);
1717
1733
 
1718
1734
  const updateGlobals = newValue => {
1719
1735
  useUpdateGlobals()?.(newValue);
1720
- }; // TODO: document
1721
-
1722
- const useRootNode = () => inject(appRootNodeKey); // TODO: document
1736
+ };
1737
+ /** Use the current Lunchbox app. Usually used internally by Lunchbox. */
1723
1738
 
1724
- const useApp = () => inject(appKey); // TODO: document
1739
+ const useApp = () => inject(appKey);
1740
+ /** Obtain a list of the start callback functions. Usually used internally by Lunchbox. */
1725
1741
 
1726
- const useStartCallbacks = () => inject(startCallbackKey); //[] as Lunch.UpdateCallback[]
1727
- // TODO: document
1742
+ const useStartCallbacks = () => inject(startCallbackKey);
1743
+ /** Run a given callback once when the Lunchbox app starts. Include an index to
1744
+ * splice the callback at that index in the callback queue. */
1728
1745
 
1729
1746
  const onStart = (cb, index = Infinity) => {
1730
1747
  const callbacks = useStartCallbacks();
@@ -1734,11 +1751,21 @@ const onStart = (cb, index = Infinity) => {
1734
1751
  } else {
1735
1752
  callbacks?.splice(index, 0, cb);
1736
1753
  }
1737
- }; // CREATE APP
1754
+ };
1755
+ /** Obtain a list of interactable objects (registered via onClick, onHover, etc events). Usually used internally by Lunchbox. */
1756
+
1757
+ const useLunchboxInteractables = () => inject(lunchboxInteractables); // CREATE APP
1738
1758
  // ====================
1739
1759
 
1740
1760
  const createApp = root => {
1741
- const app = createRenderer(nodeOps).createApp(root); // register all components
1761
+ const {
1762
+ nodeOps,
1763
+ interactables
1764
+ } = createNodeOps();
1765
+ const app = createRenderer(nodeOps).createApp(root); // provide Lunchbox interaction handlers flag (modified when user references events via
1766
+ // @click, etc)
1767
+
1768
+ app.provide(lunchboxInteractables, interactables); // register all components
1742
1769
  // ====================
1743
1770
 
1744
1771
  Object.keys(components).forEach(key => {
@@ -1888,4 +1915,4 @@ const createApp = root => {
1888
1915
  return app;
1889
1916
  };
1890
1917
 
1891
- export { MiniDom, addEventListener, addInteractable, afterRenderKey, appCameraKey, appKey, appRenderersKey, appRootNodeKey, appSceneKey, beforeRenderKey, camera, cancelUpdate, cancelUpdateSource, clearCustomRender, clearCustomRenderKey, createApp, createCommentNode, createDomNode, createNode, createTextNode, currentIntersections, ensureRenderer, ensuredCamera, ensuredScene, extend, find, frameIdKey, globalsInjectionKey, inputActive, instantiateThreeObject, interactables, isMinidomNode, mousePos, nestedPropertiesToCheck, offAfterRender, offAfterRenderKey, offBeforeRender, offBeforeRenderKey, onAfterRender, onAfterRenderKey, onBeforeRender, onBeforeRenderKey, onStart, removeInteractable, renderer, scene, setCustomRender, setCustomRenderKey, startCallbackKey, update, updateAllObjectProps, updateGlobals, updateGlobalsInjectionKey, updateObjectProp, useAfterRender, useApp, useBeforeRender, useCamera, useCancelUpdate, useCancelUpdateSource, useCustomRender, useGlobals, useRenderer, useRootNode, useScene, useStartCallbacks, useUpdateGlobals, watchStopHandleKey };
1918
+ export { MiniDom, addEventListener, afterRenderKey, appCameraKey, appKey, appRenderersKey, appRootNodeKey, appSceneKey, beforeRenderKey, cancelUpdate, cancelUpdateSource, clearCustomRender, clearCustomRenderKey, createApp, createCommentNode, createDomNode, createNode, createTextNode, extend, find, frameIdKey, globalsInjectionKey, instantiateThreeObject, isMinidomNode, lunchboxInteractables, nestedPropertiesToCheck, offAfterRender, offAfterRenderKey, offBeforeRender, offBeforeRenderKey, onAfterRender, onAfterRenderKey, onBeforeRender, onBeforeRenderKey, onCameraReady, onRendererReady, onSceneReady, onStart, setCustomRender, setCustomRenderKey, startCallbackKey, update, updateGlobals, updateGlobalsInjectionKey, updateObjectProp, useAfterRender, useApp, useBeforeRender, useCamera, useCancelUpdate, useCancelUpdateSource, useCustomRender, useGlobals, useLunchboxInteractables, useRenderer, useScene, useStartCallbacks, useUpdateGlobals, watchStopHandleKey };