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.
@@ -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, inject, toRaw, defineComponent, createVNode, resolveComponent, ref, watch, onBeforeUnmount, reactive, onMounted, Fragment, mergeProps, h, createRenderer, computed } from 'vue';
2
2
  import * as THREE from 'three';
3
3
  import { get, isNumber, set } from 'lodash';
4
4
 
@@ -119,6 +119,7 @@ const appKey = Symbol();
119
119
  const appRenderersKey = Symbol();
120
120
  const appSceneKey = Symbol();
121
121
  const appCameraKey = Symbol();
122
+ const lunchboxInteractables = Symbol();
122
123
  const startCallbackKey = Symbol();
123
124
 
124
125
  const ensuredCamera = () => inject(appCameraKey); // ENSURE RENDERER
@@ -129,215 +130,11 @@ const ensureRenderer = () => inject(appRenderersKey); // ENSURE SCENE
129
130
 
130
131
  const ensuredScene = () => inject(appSceneKey);
131
132
 
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
133
  /** Add an event listener to the given node. Also creates the event teardown function and any necessary raycaster/interaction dictionary updates. */
337
-
338
134
  function addEventListener({
339
135
  node,
340
136
  key,
137
+ interactables,
341
138
  value
342
139
  }) {
343
140
  // create new records for this key if needed
@@ -353,37 +150,18 @@ function addEventListener({
353
150
  node.eventListeners[key].push(value); // if we need it, let's get/create the main raycaster
354
151
 
355
152
  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
- });
153
+ if (node.instance && !interactables.value.includes(node)) {
154
+ // add to interactables
155
+ interactables.value.push(node);
156
+ node.eventListenerRemoveFunctions[key].push(() => {
157
+ // remove from interactables
158
+ const idx = interactables.value.indexOf(node);
159
+
160
+ if (idx !== -1) {
161
+ interactables.value.splice(idx, 1);
383
162
  }
384
- }
385
- });
386
- node.eventListenerRemoveFunctions[key].push(stop);
163
+ });
164
+ }
387
165
  }
388
166
 
389
167
  return node;
@@ -477,6 +255,195 @@ const LunchboxScene = defineComponent({
477
255
 
478
256
  });
479
257
 
258
+ const LunchboxEventHandlers = defineComponent({
259
+ name: 'LunchboxEventHandlers',
260
+
261
+ setup() {
262
+ const interactables = useLunchboxInteractables();
263
+ const camera = useCamera();
264
+ const renderer = useRenderer();
265
+ const globals = useGlobals();
266
+ const mousePos = ref({
267
+ x: Infinity,
268
+ y: Infinity
269
+ });
270
+ const inputActive = ref(false);
271
+ let currentIntersections = [];
272
+ const raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, 0, -1));
273
+
274
+ const fireEventsFromIntersections = ({
275
+ element,
276
+ eventKeys,
277
+ intersection
278
+ }) => {
279
+ if (!element) return;
280
+ eventKeys.forEach(eventKey => {
281
+ if (element.eventListeners[eventKey]) {
282
+ element.eventListeners[eventKey].forEach(cb => {
283
+ cb({
284
+ intersection
285
+ });
286
+ });
287
+ }
288
+ });
289
+ }; // add mouse listener to renderer DOM element when the element is ready
290
+
291
+
292
+ const stopWatch = watch(renderer, v => {
293
+ if (!v?.domElement) return; // we have a DOM element, so let's add mouse listeners
294
+
295
+ const {
296
+ domElement
297
+ } = v;
298
+
299
+ const mouseMoveListener = evt => {
300
+ const screenWidth = (domElement.width ?? 1) / globals.dpr;
301
+ const screenHeight = (domElement.height ?? 1) / globals.dpr;
302
+ mousePos.value.x = evt.offsetX / screenWidth * 2 - 1;
303
+ mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1;
304
+ };
305
+
306
+ const mouseDownListener = () => inputActive.value = true;
307
+
308
+ const mouseUpListener = () => inputActive.value = false; // add mouse events
309
+
310
+
311
+ domElement.addEventListener('pointermove', mouseMoveListener);
312
+ domElement.addEventListener('pointerdown', mouseDownListener);
313
+ domElement.addEventListener('pointerup', mouseUpListener); // stop the watcher
314
+
315
+ stopWatch();
316
+ }, {
317
+ immediate: true
318
+ });
319
+
320
+ const update = () => {
321
+ const c = camera.value;
322
+ if (!c) return;
323
+ raycaster.setFromCamera(mousePos.value, c);
324
+ const intersections = raycaster.intersectObjects(interactables?.value.map(v => v.instance) ?? []);
325
+ let leaveValues = [],
326
+ entering = [],
327
+ staying = []; // intersection arrays
328
+
329
+ leaveValues = currentIntersections.map(v => v.intersection); // element arrays
330
+
331
+ intersections?.forEach(intersection => {
332
+ const currentIdx = currentIntersections.findIndex(v => v.intersection.object === intersection.object);
333
+
334
+ if (currentIdx === -1) {
335
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
336
+
337
+ if (found) {
338
+ entering.push({
339
+ element: found,
340
+ intersection
341
+ });
342
+ }
343
+ } else {
344
+ const found = interactables?.value.find(v => v.instance?.uuid === intersection.object.uuid);
345
+
346
+ if (found) {
347
+ staying.push({
348
+ element: found,
349
+ intersection
350
+ });
351
+ }
352
+ } // this is a current intersection, so it won't be in our `leave` array
353
+
354
+
355
+ const leaveIdx = leaveValues.findIndex(v => v.object.uuid === intersection.object.uuid);
356
+
357
+ if (leaveIdx !== -1) {
358
+ leaveValues.splice(leaveIdx, 1);
359
+ }
360
+ });
361
+ const leaving = leaveValues.map(intersection => {
362
+ return {
363
+ element: interactables?.value.find(interactable => interactable.instance?.uuid === intersection.object.uuid),
364
+ intersection
365
+ };
366
+ }); // new interactions
367
+
368
+ entering.forEach(({
369
+ element,
370
+ intersection
371
+ }) => {
372
+ fireEventsFromIntersections({
373
+ element,
374
+ eventKeys: ['onPointerEnter'],
375
+ intersection
376
+ });
377
+ }); // unchanged interactions
378
+
379
+ staying.forEach(({
380
+ element,
381
+ intersection
382
+ }) => {
383
+ const eventKeys = ['onPointerOver', 'onPointerMove'];
384
+ fireEventsFromIntersections({
385
+ element,
386
+ eventKeys,
387
+ intersection
388
+ });
389
+ }); // exited interactions
390
+
391
+ leaving.forEach(({
392
+ element,
393
+ intersection
394
+ }) => {
395
+ const eventKeys = ['onPointerLeave', 'onPointerOut'];
396
+ fireEventsFromIntersections({
397
+ element,
398
+ eventKeys,
399
+ intersection
400
+ });
401
+ });
402
+ currentIntersections = [].concat(entering, staying);
403
+ }; // update function
404
+
405
+
406
+ onBeforeRender(update);
407
+
408
+ const teardown = () => offBeforeRender(update);
409
+
410
+ onBeforeUnmount(teardown);
411
+ const clickEventKeys = ['onClick', 'onPointerDown', 'onPointerUp'];
412
+ watch(inputActive, isDown => {
413
+ // meshes with multiple intersections receive multiple callbacks by default -
414
+ // let's make it so they only receive one callback of each type per frame.
415
+ // (ie usually when you click on a mesh, you expect only one click event to fire, even
416
+ // if there are technically multiple intersections with that mesh)
417
+ const uuidsInteractedWithThisFrame = [];
418
+ currentIntersections.forEach(v => {
419
+ clickEventKeys.forEach(key => {
420
+ const id = v.element.uuid + key;
421
+
422
+ if (isDown && (key === 'onClick' || key === 'onPointerDown')) {
423
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
424
+ v.element.eventListeners[key]?.forEach(cb => cb({
425
+ intersection: v.intersection
426
+ }));
427
+ uuidsInteractedWithThisFrame.push(id);
428
+ }
429
+ } else if (!isDown && key === 'onPointerUp') {
430
+ if (!uuidsInteractedWithThisFrame.includes(id)) {
431
+ v.element.eventListeners[key]?.forEach(cb => cb({
432
+ intersection: v.intersection
433
+ }));
434
+ uuidsInteractedWithThisFrame.push(id);
435
+ }
436
+ }
437
+ });
438
+ });
439
+ }); // return arbitrary object to ensure instantiation
440
+ // TODO: why can't we return a <raycaster/> here?
441
+
442
+ return () => createVNode(resolveComponent("object3D"), null, null);
443
+ }
444
+
445
+ });
446
+
480
447
  /** fixed & fill styling for container */
481
448
 
482
449
  const fillStyle = position => {
@@ -529,9 +496,10 @@ const LunchboxWrapper = defineComponent({
529
496
 
530
497
  if (props.r3f && THREE?.ColorManagement) {
531
498
  THREE.ColorManagement.legacyMode = false;
532
- } // MOUNT
533
- // ====================
499
+ }
534
500
 
501
+ const interactables = useLunchboxInteractables(); // MOUNT
502
+ // ====================
535
503
 
536
504
  onMounted(async () => {
537
505
  // canvas needs to exist (or user needs to handle it on their own)
@@ -567,23 +535,17 @@ const LunchboxWrapper = defineComponent({
567
535
  updateGlobals?.({
568
536
  dpr
569
537
  });
570
- console.log(1);
571
538
 
572
539
  while (!renderer.value?.$el?.instance && // TODO: remove `as any`
573
540
  !renderer.value?.component?.ctx.$el?.instance) {
574
- console.log(2);
575
541
  await new Promise(r => requestAnimationFrame(r));
576
542
  }
577
543
 
578
- console.log(3);
579
-
580
544
  while (!scene.value?.$el?.instance && // TODO: remove `as any`
581
545
  !scene.value?.component?.ctx.$el?.instance) {
582
- console.log(4);
583
546
  await new Promise(r => requestAnimationFrame(r));
584
547
  }
585
548
 
586
- console.log(5);
587
549
  const normalizedRenderer = renderer.value?.$el?.instance ?? renderer.value?.component?.ctx.$el?.instance;
588
550
  normalizedRenderer.setPixelRatio(globals.dpr);
589
551
  const normalizedScene = scene.value?.$el?.instance ?? scene.value?.component?.ctx.$el?.instance;
@@ -689,7 +651,7 @@ const LunchboxWrapper = defineComponent({
689
651
  }, consolidatedCameraProperties), null) : createVNode(resolveComponent("perspectiveCamera"), mergeProps({
690
652
  "ref": camera,
691
653
  "args": props.cameraArgs ?? [props.r3f ? 75 : 45, 0.5625, 1, 1000]
692
- }, consolidatedCameraProperties), null)]);
654
+ }, consolidatedCameraProperties), null), interactables?.value.length && createVNode(LunchboxEventHandlers, null, null)]);
693
655
  }
694
656
 
695
657
  });
@@ -704,7 +666,7 @@ const autoGeneratedComponents = [// ThreeJS basics
704
666
  'light', 'spotLightShadow', 'spotLight', 'pointLight', 'rectAreaLight', 'hemisphereLight', 'directionalLightShadow', 'directionalLight', 'ambientLight', 'lightShadow', 'ambientLightProbe', 'hemisphereLightProbe', 'lightProbe', // textures
705
667
  'texture', 'videoTexture', 'dataTexture', 'dataTexture3D', 'compressedTexture', 'cubeTexture', 'canvasTexture', 'depthTexture', // Texture loaders
706
668
  'textureLoader', // misc
707
- 'group', 'catmullRomCurve3', 'points', // helpers
669
+ 'group', 'catmullRomCurve3', 'points', 'raycaster', // helpers
708
670
  'cameraHelper', // cameras
709
671
  'camera', 'perspectiveCamera', 'orthographicCamera', 'cubeCamera', 'arrayCamera', // renderers
710
672
  'webGLRenderer'
@@ -739,7 +701,6 @@ planeHelper: PlaneHelperProps
739
701
  arrowHelper: ArrowHelperProps
740
702
  axesHelper: AxesHelperProps
741
703
  // misc
742
- raycaster: RaycasterProps
743
704
  vector2: Vector2Props
744
705
  vector3: Vector3Props
745
706
  vector4: Vector4Props
@@ -756,7 +717,7 @@ shape: ShapeProps
756
717
  */
757
718
  ];
758
719
 
759
- const catalogue = {};
720
+ const catalogue = {}; // component creation utility
760
721
 
761
722
  const createComponent$1 = tag => defineComponent({
762
723
  inheritAttrs: false,
@@ -1152,10 +1113,6 @@ function isMinidomNode(item) {
1152
1113
  return item?.minidomType === 'RendererNode';
1153
1114
  }
1154
1115
 
1155
- // let watchStopHandle: WatchStopHandle
1156
- // export const beforeRender = [] as Lunch.UpdateCallback[]
1157
- // export const afterRender = [] as Lunch.UpdateCallback[]
1158
-
1159
1116
  const requestUpdate = opts => {
1160
1117
  if (typeof opts.app.config.globalProperties.lunchbox.frameId === 'number') {
1161
1118
  cancelAnimationFrame(opts.app.config.globalProperties.lunchbox.frameId);
@@ -1244,22 +1201,7 @@ const onAfterRender = (cb, index = Infinity) => {
1244
1201
 
1245
1202
  const offAfterRender = cb => {
1246
1203
  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
1204
+ }; // TODO: document
1263
1205
 
1264
1206
  const useCancelUpdate = () => {
1265
1207
  const frameId = inject(frameIdKey);
@@ -1281,28 +1223,12 @@ const cancelUpdateSource = () => {
1281
1223
  useCancelUpdateSource()?.();
1282
1224
  };
1283
1225
 
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
1226
  /** Update a single prop on a given node. */
1302
1227
 
1303
1228
  function updateObjectProp({
1304
1229
  node,
1305
1230
  key,
1231
+ interactables,
1306
1232
  value
1307
1233
  }) {
1308
1234
  // handle and return early if prop is an event
@@ -1311,6 +1237,7 @@ function updateObjectProp({
1311
1237
  return addEventListener({
1312
1238
  node,
1313
1239
  key,
1240
+ interactables,
1314
1241
  value
1315
1242
  });
1316
1243
  } // update THREE property
@@ -1566,72 +1493,86 @@ const remove = node => {
1566
1493
  Elements are `create`d from the outside in, then `insert`ed from the inside out.
1567
1494
  */
1568
1495
 
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,
1585
-
1586
- nextSibling(node) {
1587
- const result = node.nextSibling; // console.log('found', result)
1496
+ const createNodeOps = () => {
1497
+ // APP-LEVEL GLOBALS
1498
+ // ====================
1499
+ // These need to exist at the app level in a place where the node ops can access them.
1500
+ // It'd be better to set these via `app.provide` at app creation, but the node ops need access
1501
+ // to these values before the app is instantiated, so this is the next-best place for them to exist.
1502
+ const interactables = ref([]); // NODE OPS
1503
+ // ====================
1588
1504
 
1589
- if (!result) return null;
1590
- return result;
1591
- },
1505
+ const nodeOps = {
1506
+ createElement,
1592
1507
 
1593
- parentNode(node) {
1594
- const result = node.parentNode;
1595
- if (!result) return null;
1596
- return result;
1597
- },
1508
+ createText(text) {
1509
+ return createTextNode({
1510
+ text
1511
+ });
1512
+ },
1598
1513
 
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
- }
1514
+ createComment(text) {
1515
+ return createCommentNode({
1516
+ text
1517
+ });
1518
+ },
1519
+
1520
+ insert,
1521
+
1522
+ nextSibling(node) {
1523
+ const result = node.nextSibling;
1524
+ if (!result) return null;
1525
+ return result;
1526
+ },
1527
+
1528
+ parentNode(node) {
1529
+ const result = node.parentNode;
1530
+ if (!result) return null;
1531
+ return result;
1532
+ },
1533
+
1534
+ patchProp(node, key, prevValue, nextValue) {
1535
+ if (isLunchboxDomComponent(node)) {
1536
+ // handle DOM node
1537
+ if (key === 'style') {
1538
+ // special handling for style
1539
+ Object.keys(nextValue).forEach(k => {
1540
+ node.domElement.style[k] = nextValue[k];
1541
+ });
1542
+ } else {
1543
+ node.domElement.setAttribute(key, nextValue);
1544
+ }
1610
1545
 
1611
- return;
1612
- } // ignore if root node, or Lunchbox internal prop
1546
+ return;
1547
+ } // ignore if root node, or Lunchbox internal prop
1613
1548
 
1614
1549
 
1615
- if (isLunchboxRootNode(node) || key.startsWith('$')) {
1616
- return;
1617
- } // otherwise, update prop
1550
+ if (isLunchboxRootNode(node) || key.startsWith('$')) {
1551
+ return;
1552
+ } // otherwise, update prop
1618
1553
 
1619
1554
 
1620
- updateObjectProp({
1621
- node: node,
1622
- key,
1623
- value: nextValue
1624
- });
1625
- },
1555
+ updateObjectProp({
1556
+ node: node,
1557
+ key,
1558
+ interactables,
1559
+ value: nextValue
1560
+ });
1561
+ },
1626
1562
 
1627
- remove,
1563
+ remove,
1628
1564
 
1629
- setElementText() {// noop
1630
- },
1565
+ setElementText() {// noop
1566
+ },
1631
1567
 
1632
- setText() {// noop
1633
- }
1568
+ setText() {// noop
1569
+ }
1634
1570
 
1571
+ };
1572
+ return {
1573
+ nodeOps,
1574
+ interactables
1575
+ };
1635
1576
  };
1636
1577
 
1637
1578
  /** The current camera. Often easier to use `useCamera` instead of this. */
@@ -1734,11 +1675,20 @@ const onStart = (cb, index = Infinity) => {
1734
1675
  } else {
1735
1676
  callbacks?.splice(index, 0, cb);
1736
1677
  }
1737
- }; // CREATE APP
1678
+ }; // TODO: document
1679
+
1680
+ const useLunchboxInteractables = () => inject(lunchboxInteractables); // CREATE APP
1738
1681
  // ====================
1739
1682
 
1740
1683
  const createApp = root => {
1741
- const app = createRenderer(nodeOps).createApp(root); // register all components
1684
+ const {
1685
+ nodeOps,
1686
+ interactables
1687
+ } = createNodeOps();
1688
+ const app = createRenderer(nodeOps).createApp(root); // provide Lunchbox interaction handlers flag (modified when user references events via
1689
+ // @click, etc)
1690
+
1691
+ app.provide(lunchboxInteractables, interactables); // register all components
1742
1692
  // ====================
1743
1693
 
1744
1694
  Object.keys(components).forEach(key => {
@@ -1888,4 +1838,4 @@ const createApp = root => {
1888
1838
  return app;
1889
1839
  };
1890
1840
 
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 };
1841
+ export { MiniDom, addEventListener, afterRenderKey, appCameraKey, appKey, appRenderersKey, appRootNodeKey, appSceneKey, beforeRenderKey, camera, cancelUpdate, cancelUpdateSource, clearCustomRender, clearCustomRenderKey, createApp, createCommentNode, createDomNode, createNode, createTextNode, ensureRenderer, ensuredCamera, ensuredScene, extend, find, frameIdKey, globalsInjectionKey, instantiateThreeObject, isMinidomNode, lunchboxInteractables, nestedPropertiesToCheck, offAfterRender, offAfterRenderKey, offBeforeRender, offBeforeRenderKey, onAfterRender, onAfterRenderKey, onBeforeRender, onBeforeRenderKey, onStart, renderer, scene, setCustomRender, setCustomRenderKey, startCallbackKey, update, updateGlobals, updateGlobalsInjectionKey, updateObjectProp, useAfterRender, useApp, useBeforeRender, useCamera, useCancelUpdate, useCancelUpdateSource, useCustomRender, useGlobals, useLunchboxInteractables, useRenderer, useRootNode, useScene, useStartCallbacks, useUpdateGlobals, watchStopHandleKey };