lunchboxjs 0.1.4018 → 0.2.1001-beta.2

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.
Files changed (41) hide show
  1. package/dist/lunchboxjs.js +1739 -1670
  2. package/dist/lunchboxjs.min.js +1 -1
  3. package/dist/lunchboxjs.module.js +1694 -1667
  4. package/extras/OrbitControlsWrapper.vue +5 -7
  5. package/package.json +15 -4
  6. package/src/components/LunchboxEventHandlers.tsx +237 -0
  7. package/src/components/LunchboxWrapper/LunchboxScene.tsx +8 -0
  8. package/src/components/LunchboxWrapper/LunchboxWrapper.tsx +341 -0
  9. package/src/components/LunchboxWrapper/prepCanvas.ts +27 -21
  10. package/src/components/LunchboxWrapper/resizeCanvas.ts +13 -12
  11. package/src/components/autoGeneratedComponents.ts +1 -1
  12. package/src/components/index.ts +2 -4
  13. package/src/core/createNode.ts +2 -18
  14. package/src/core/extend.ts +1 -1
  15. package/src/core/index.ts +0 -3
  16. package/src/core/instantiateThreeObject/index.ts +7 -2
  17. package/src/core/instantiateThreeObject/processProps.ts +1 -1
  18. package/src/core/interaction.ts +55 -0
  19. package/src/core/minidom.ts +5 -9
  20. package/src/core/update.ts +92 -53
  21. package/src/core/updateObjectProp.ts +5 -14
  22. package/src/index.ts +270 -76
  23. package/src/keys.ts +25 -0
  24. package/src/nodeOps/createElement.ts +2 -5
  25. package/src/nodeOps/index.ts +70 -57
  26. package/src/nodeOps/insert.ts +11 -32
  27. package/src/nodeOps/remove.ts +1 -17
  28. package/src/types.ts +34 -10
  29. package/src/utils/index.ts +11 -4
  30. package/dist/.DS_Store +0 -0
  31. package/src/.DS_Store +0 -0
  32. package/src/components/LunchboxWrapper/LunchboxWrapper.ts +0 -312
  33. package/src/components/catalogue.ts +0 -3
  34. package/src/core/.DS_Store +0 -0
  35. package/src/core/allNodes.ts +0 -4
  36. package/src/core/ensure.ts +0 -203
  37. package/src/core/interaction/index.ts +0 -102
  38. package/src/core/interaction/input.ts +0 -4
  39. package/src/core/interaction/interactables.ts +0 -14
  40. package/src/core/interaction/setupAutoRaycaster.ts +0 -224
  41. package/src/core/start.ts +0 -11
@@ -1,7 +1,7 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue'), require('three'), require('lodash')) :
3
3
  typeof define === 'function' && define.amd ? define(['exports', 'vue', 'three', 'lodash'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LunchboxRenderer = {}, global.vue, global.three, global.lodash));
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Lunchbox = {}, global.vue, global.three, global.lodash));
5
5
  })(this, (function (exports, vue, THREE, lodash) { 'use strict';
6
6
 
7
7
  function _interopNamespace(e) {
@@ -24,1063 +24,819 @@
24
24
 
25
25
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
26
26
 
27
- // this needs to be in a separate file to ensure it's created immediately
28
- const allNodes = [];
27
+ function find(target) {
28
+ target = vue.isRef(target) ? target.value : target; // handle standard lunchbox node
29
29
 
30
- const resizeCanvas = (width, height) => {
31
- const renderer = ensureRenderer.value?.instance;
32
- const scene = ensuredScene.value.instance;
33
- const camera = ensuredCamera.value;
34
- // ignore if no element
35
- if (!renderer?.domElement || !scene || !camera)
36
- return;
37
- width = width ?? window.innerWidth;
38
- height = height ?? window.innerHeight;
39
- // update camera
40
- const aspect = width / height;
41
- if (camera.type?.toLowerCase() === 'perspectivecamera') {
42
- const perspectiveCamera = camera.instance;
43
- perspectiveCamera.aspect = aspect;
44
- perspectiveCamera.updateProjectionMatrix();
45
- }
46
- else if (camera.type?.toLowerCase() === 'orthographiccamera') {
47
- // console.log('TODO: ortho camera update')
48
- const orthoCamera = camera.instance;
49
- const heightInTermsOfWidth = height / width;
50
- orthoCamera.top = heightInTermsOfWidth * 10;
51
- orthoCamera.bottom = -heightInTermsOfWidth * 10;
52
- orthoCamera.right = 10;
53
- orthoCamera.left = -10;
54
- orthoCamera.updateProjectionMatrix();
55
- }
56
- else {
57
- console.log('TODO: non-ortho or perspective camera');
58
- }
59
- // update canvas
60
- renderer.setSize(width, height);
61
- // render immediately so there's no flicker
62
- if (scene && camera.instance) {
63
- renderer.render(vue.toRaw(scene), vue.toRaw(camera.instance));
64
- }
65
- };
30
+ if (isLunchboxStandardNode(target)) {
31
+ return target?.instance;
32
+ } // handle component
66
33
 
67
- const getInnerDimensions = (node) => {
68
- const computedStyle = getComputedStyle(node);
69
- const width = node.clientWidth - parseFloat(computedStyle.paddingLeft) - parseFloat(computedStyle.paddingRight);
70
- const height = node.clientHeight - parseFloat(computedStyle.paddingTop) - parseFloat(computedStyle.paddingBottom);
71
- return { width, height };
72
- };
73
- const prepCanvas = (container, canvasElement, onBeforeUnmount, sizePolicy) => {
74
- const containerElement = container.value?.domElement;
75
- if (!containerElement)
76
- throw new Error('missing container');
77
- // save...
78
- // ...and size element
79
- const resizeCanvasByPolicy = () => {
80
- if (sizePolicy === "container") {
81
- const dims = getInnerDimensions(containerElement);
82
- resizeCanvas(dims.width, dims.height);
83
- }
84
- else
85
- resizeCanvas();
86
- };
87
- resizeCanvasByPolicy();
88
- // attach listeners
89
- const observer = new ResizeObserver(([canvas]) => {
90
- resizeCanvasByPolicy();
91
- });
92
- // window.addEventListener('resize', resizeCanvas)
93
- if (containerElement) {
94
- observer.observe(containerElement);
95
- }
96
- // cleanup
97
- onBeforeUnmount(() => {
98
- if (canvasElement) {
99
- observer.unobserve(canvasElement);
100
- }
101
- });
102
- };
103
-
104
- // TODO:
105
- // Continue r3f prop - what else (besides camera fov) makes r3f look good?
106
- /** fixed & fill styling for container */
107
- const fillStyle = (position) => {
108
- return {
109
- position,
110
- top: 0,
111
- right: 0,
112
- bottom: 0,
113
- left: 0,
114
- width: '100%',
115
- height: '100%',
116
- display: 'block',
117
- };
118
- };
119
- const LunchboxWrapper = {
120
- name: 'Lunchbox',
121
- props: {
122
- // These should match the Lunchbox.WrapperProps interface
123
- background: String,
124
- cameraArgs: Array,
125
- cameraLook: Array,
126
- cameraLookAt: Array,
127
- cameraPosition: Array,
128
- dpr: Number,
129
- ortho: Boolean,
130
- orthographic: Boolean,
131
- r3f: Boolean,
132
- rendererArguments: Object,
133
- rendererProperties: Object,
134
- sizePolicy: String,
135
- shadow: [Boolean, Object],
136
- transparent: Boolean,
137
- zoom: Number,
138
- updateSource: Object,
139
- },
140
- setup(props, context) {
141
- const canvas = vue.ref();
142
- const useFallbackRenderer = vue.ref(true);
143
- const dpr = vue.ref(props.dpr ?? -1);
144
- const container = vue.ref();
145
- let renderer;
146
- let camera;
147
- let scene;
148
- // https://threejs.org/docs/index.html#manual/en/introduction/Color-management
149
- if (props.r3f && THREE__namespace?.ColorManagement) {
150
- THREE__namespace.ColorManagement.legacyMode = false;
151
- }
152
- // MOUNT
153
- // ====================
154
- vue.onMounted(() => {
155
- // canvas needs to exist
156
- if (!canvas.value)
157
- throw new Error('missing canvas');
158
- // RENDERER
159
- // ====================
160
- // is there already a renderer?
161
- // TODO: allow other renderer types
162
- renderer = tryGetNodeWithInstanceType([
163
- 'WebGLRenderer',
164
- ]);
165
- // if renderer is missing, initialize with options
166
- if (!renderer) {
167
- // build renderer args
168
- const rendererArgs = {
169
- alpha: props.transparent,
170
- antialias: true,
171
- canvas: canvas.value.domElement,
172
- powerPreference: !!props.r3f
173
- ? 'high-performance'
174
- : 'default',
175
- ...(props.rendererArguments ?? {}),
176
- };
177
- // create new renderer
178
- ensureRenderer.value = createNode({
179
- type: 'WebGLRenderer',
180
- uuid: fallbackRendererUuid,
181
- props: {
182
- args: [rendererArgs],
183
- },
184
- });
185
- // we've initialized the renderer, so anything depending on it can execute now
186
- rendererReady.value = true;
187
- const rendererAsWebGlRenderer = ensureRenderer;
188
- // apply r3f settings if desired
189
- if (props.r3f) {
190
- if (rendererAsWebGlRenderer.value.instance) {
191
- rendererAsWebGlRenderer.value.instance.outputEncoding =
192
- THREE__namespace.sRGBEncoding;
193
- rendererAsWebGlRenderer.value.instance.toneMapping =
194
- THREE__namespace.ACESFilmicToneMapping;
195
- }
196
- }
197
- // update render sugar
198
- const sugar = {
199
- shadow: props.shadow,
200
- };
201
- if (rendererAsWebGlRenderer.value.instance && sugar?.shadow) {
202
- rendererAsWebGlRenderer.value.instance.shadowMap.enabled =
203
- true;
204
- if (typeof sugar.shadow === 'object') {
205
- rendererAsWebGlRenderer.value.instance.shadowMap.type =
206
- sugar.shadow.type;
207
- }
208
- }
209
- // set renderer props if needed
210
- if (props.rendererProperties) {
211
- Object.keys(props.rendererProperties).forEach((key) => {
212
- lodash.set(rendererAsWebGlRenderer.value, key, props.rendererProperties[key]);
213
- });
214
- }
215
- // update using created renderer
216
- renderer = rendererAsWebGlRenderer.value;
217
- }
218
- else {
219
- useFallbackRenderer.value = false;
220
- // the user has initialized the renderer, so anything depending
221
- // on the renderer can execute
222
- rendererReady.value = true;
223
- }
224
- // CAMERA
225
- // ====================
226
- // is there already a camera?
227
- camera = tryGetNodeWithInstanceType([
228
- 'PerspectiveCamera',
229
- 'OrthographicCamera',
230
- ]);
231
- // if not, let's create one
232
- if (!camera) {
233
- // create ortho camera
234
- if (props.ortho || props.orthographic) {
235
- ensuredCamera.value = createNode({
236
- props: { args: props.cameraArgs ?? [] },
237
- type: 'OrthographicCamera',
238
- uuid: fallbackCameraUuid,
239
- });
240
- }
241
- else {
242
- ensuredCamera.value = createNode({
243
- props: {
244
- args: props.cameraArgs ?? [
245
- props.r3f ? 75 : 45,
246
- 0.5625,
247
- 1,
248
- 1000,
249
- ],
250
- },
251
- type: 'PerspectiveCamera',
252
- uuid: fallbackCameraUuid,
253
- });
254
- }
255
- cameraReady.value = true;
256
- camera = ensuredCamera.value;
257
- }
258
- else {
259
- cameraReady.value = true;
260
- }
261
- if (!camera.instance) {
262
- throw new Error('Error creating camera.');
263
- }
264
- // move camera if needed
265
- if (camera && props.cameraPosition) {
266
- camera.instance.position.set(...props.cameraPosition);
267
- }
268
- // angle camera if needed
269
- if (camera && (props.cameraLookAt || props.cameraLook)) {
270
- const source = (props.cameraLookAt || props.cameraLook);
271
- camera.instance.lookAt(...source);
272
- }
273
- // zoom camera if needed
274
- if (camera && props.zoom !== undefined) {
275
- camera.instance.zoom = props.zoom;
276
- }
277
- // SCENE
278
- // ====================
279
- scene = ensuredScene.value;
280
- // set background color
281
- if (scene && scene.instance && props.background) {
282
- scene.instance.background = new THREE__namespace.Color(props.background);
283
- }
284
- // MISC PROPERTIES
285
- // ====================
286
- if (dpr.value === -1) {
287
- dpr.value = window.devicePixelRatio;
288
- }
289
- if (renderer?.instance) {
290
- renderer.instance.setPixelRatio(dpr.value);
291
- globals.dpr.value = dpr.value;
292
- // prep canvas (sizing, observe, unmount, etc)
293
- prepCanvas(container, renderer.instance.domElement, vue.onBeforeUnmount, props.sizePolicy);
294
- }
295
- else {
296
- throw new Error('missing renderer');
297
- }
298
- // CALLBACK PREP
299
- // ====================
300
- const app = vue.getCurrentInstance().appContext.app;
301
- // START
302
- // ====================
303
- for (let startCallback of startCallbacks) {
304
- startCallback({
305
- app,
306
- camera: camera.instance,
307
- renderer: renderer.instance,
308
- scene: scene.instance,
309
- });
310
- }
311
- // KICK UPDATE
312
- // ====================
313
- update({
314
- app,
315
- camera: camera.instance,
316
- renderer: renderer.instance,
317
- scene: scene.instance,
318
- updateSource: props.updateSource,
319
- });
320
- });
321
- // UNMOUNT
322
- // ====================
323
- vue.onBeforeUnmount(() => {
324
- cancelUpdate();
325
- cancelUpdateSource();
326
- });
327
- // RENDER FUNCTION
328
- // ====================
329
- const containerFillStyle = props.sizePolicy === 'container' ? 'static' : 'absolute';
330
- const canvasFillStyle = props.sizePolicy === 'container' ? 'static' : 'fixed';
331
- return () => [
332
- context.slots.default?.() ?? null,
333
- vue.h('div', {
334
- style: fillStyle(containerFillStyle),
335
- ref: container,
336
- }, [
337
- useFallbackRenderer.value
338
- ? vue.h('canvas', {
339
- style: fillStyle(canvasFillStyle),
340
- class: 'lunchbox-canvas',
341
- ref: canvas,
342
- })
343
- : null,
344
- ]),
345
- ];
346
- },
347
- };
348
34
 
349
- // list of all components to register out of the box
350
- const autoGeneratedComponents = [
351
- // ThreeJS basics
352
- 'mesh',
353
- 'instancedMesh',
354
- 'scene',
355
- 'sprite',
356
- 'object3D',
357
- // geometry
358
- 'instancedBufferGeometry',
359
- 'bufferGeometry',
360
- 'boxBufferGeometry',
361
- 'circleBufferGeometry',
362
- 'coneBufferGeometry',
363
- 'cylinderBufferGeometry',
364
- 'dodecahedronBufferGeometry',
365
- 'extrudeBufferGeometry',
366
- 'icosahedronBufferGeometry',
367
- 'latheBufferGeometry',
368
- 'octahedronBufferGeometry',
369
- 'parametricBufferGeometry',
370
- 'planeBufferGeometry',
371
- 'polyhedronBufferGeometry',
372
- 'ringBufferGeometry',
373
- 'shapeBufferGeometry',
374
- 'sphereBufferGeometry',
375
- 'tetrahedronBufferGeometry',
376
- 'textBufferGeometry',
377
- 'torusBufferGeometry',
378
- 'torusKnotBufferGeometry',
379
- 'tubeBufferGeometry',
380
- 'wireframeGeometry',
381
- 'parametricGeometry',
382
- 'tetrahedronGeometry',
383
- 'octahedronGeometry',
384
- 'icosahedronGeometry',
385
- 'dodecahedronGeometry',
386
- 'polyhedronGeometry',
387
- 'tubeGeometry',
388
- 'torusKnotGeometry',
389
- 'torusGeometry',
390
- // textgeometry has been moved to /examples/jsm/geometries/TextGeometry
391
- // 'textGeometry',
392
- 'sphereGeometry',
393
- 'ringGeometry',
394
- 'planeGeometry',
395
- 'latheGeometry',
396
- 'shapeGeometry',
397
- 'extrudeGeometry',
398
- 'edgesGeometry',
399
- 'coneGeometry',
400
- 'cylinderGeometry',
401
- 'circleGeometry',
402
- 'boxGeometry',
403
- // materials
404
- 'material',
405
- 'shadowMaterial',
406
- 'spriteMaterial',
407
- 'rawShaderMaterial',
408
- 'shaderMaterial',
409
- 'pointsMaterial',
410
- 'meshPhysicalMaterial',
411
- 'meshStandardMaterial',
412
- 'meshPhongMaterial',
413
- 'meshToonMaterial',
414
- 'meshNormalMaterial',
415
- 'meshLambertMaterial',
416
- 'meshDepthMaterial',
417
- 'meshDistanceMaterial',
418
- 'meshBasicMaterial',
419
- 'meshMatcapMaterial',
420
- 'lineDashedMaterial',
421
- 'lineBasicMaterial',
422
- // lights
423
- 'light',
424
- 'spotLightShadow',
425
- 'spotLight',
426
- 'pointLight',
427
- 'rectAreaLight',
428
- 'hemisphereLight',
429
- 'directionalLightShadow',
430
- 'directionalLight',
431
- 'ambientLight',
432
- 'lightShadow',
433
- 'ambientLightProbe',
434
- 'hemisphereLightProbe',
435
- 'lightProbe',
436
- // textures
437
- 'texture',
438
- 'videoTexture',
439
- 'dataTexture',
440
- 'dataTexture3D',
441
- 'compressedTexture',
442
- 'cubeTexture',
443
- 'canvasTexture',
444
- 'depthTexture',
445
- // Texture loaders
446
- 'textureLoader',
447
- // misc
448
- 'group',
449
- 'catmullRomCurve3',
450
- 'points',
451
- // helpers
452
- 'cameraHelper',
453
- // cameras
454
- 'camera',
455
- 'perspectiveCamera',
456
- 'orthographicCamera',
457
- 'cubeCamera',
458
- 'arrayCamera',
459
- // renderers
460
- 'webGLRenderer',
461
- /*
462
- // List copied from r3f:
463
- // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
464
-
465
- // NOT IMPLEMENTED (can be added via Extend - docs.lunchboxjs.com/components/extend/):
466
- audioListener: AudioListenerProps
467
- positionalAudio: PositionalAudioProps
468
-
469
- lOD: LODProps
470
- skinnedMesh: SkinnedMeshProps
471
- skeleton: SkeletonProps
472
- bone: BoneProps
473
- lineSegments: LineSegmentsProps
474
- lineLoop: LineLoopProps
475
- // see `audio`
476
- // line: LineProps
477
- immediateRenderObject: ImmediateRenderObjectProps
478
-
479
- // primitive
480
- primitive: PrimitiveProps
481
-
482
- // helpers
483
- spotLightHelper: SpotLightHelperProps
484
- skeletonHelper: SkeletonHelperProps
485
- pointLightHelper: PointLightHelperProps
486
- hemisphereLightHelper: HemisphereLightHelperProps
487
- gridHelper: GridHelperProps
488
- polarGridHelper: PolarGridHelperProps
489
- directionalLightHelper: DirectionalLightHelperProps
490
- boxHelper: BoxHelperProps
491
- box3Helper: Box3HelperProps
492
- planeHelper: PlaneHelperProps
493
- arrowHelper: ArrowHelperProps
494
- axesHelper: AxesHelperProps
495
-
496
-
497
- // misc
498
- raycaster: RaycasterProps
499
- vector2: Vector2Props
500
- vector3: Vector3Props
501
- vector4: Vector4Props
502
- euler: EulerProps
503
- matrix3: Matrix3Props
504
- matrix4: Matrix4Props
505
- quaternion: QuaternionProps
506
- bufferAttribute: BufferAttributeProps
507
- instancedBufferAttribute: InstancedBufferAttributeProps
508
- color: ColorProps
509
- fog: FogProps
510
- fogExp2: FogExp2Props
511
- shape: ShapeProps
512
- */
513
- ];
35
+ if (isLunchboxComponent(target)) {
36
+ return target?.$el?.instance;
37
+ } // handle vnode
514
38
 
515
- const catalogue = {};
516
39
 
517
- const lunchboxDomComponentNames = ['canvas', 'div', 'LunchboxWrapper'];
518
- // component creation utility
519
- const createComponent$1 = (tag) => vue.defineComponent({
520
- inheritAttrs: false,
521
- name: tag,
522
- setup(props, context) {
523
- return () => {
524
- return vue.h(tag, context.attrs, context.slots?.default?.() || []);
525
- };
526
- },
527
- });
528
- // turn components into registered map
529
- const processed = autoGeneratedComponents
530
- .map(createComponent$1)
531
- .reduce((acc, curr) => {
532
- acc[curr.name] = curr;
533
- return acc;
534
- }, {});
535
- const components = {
536
- ...processed,
537
- Lunchbox: LunchboxWrapper,
538
- };
40
+ if (vue.isVNode(target)) {
41
+ return target.el?.instance;
42
+ }
539
43
 
540
- function find(target) {
541
- target = vue.isRef(target) ? target.value : target;
542
- // handle standard lunchbox node
543
- if (isLunchboxStandardNode(target)) {
544
- return target?.instance;
545
- }
546
- // handle component
547
- if (isLunchboxComponent(target)) {
548
- return target?.$el?.instance;
549
- }
550
- // handle vnode
551
- if (vue.isVNode(target)) {
552
- return target.el?.instance;
553
- }
554
- return null;
44
+ return null;
555
45
  }
556
46
 
557
- // MAKE SURE THESE MATCH VALUES IN types.EventKey
558
47
  /** Type check on whether target is a Lunchbox.EventKey */
559
- const isEventKey = (target) => {
560
- return [
561
- 'onClick',
562
- 'onContextMenu',
563
- 'onDoubleClick',
564
- 'onPointerUp',
565
- 'onPointerDown',
566
- 'onPointerOver',
567
- 'onPointerOut',
568
- 'onPointerEnter',
569
- 'onPointerLeave',
570
- 'onPointerMove',
571
- // 'onPointerMissed',
572
- // 'onUpdate',
573
- 'onWheel',
574
- ].includes(target);
48
+
49
+ const isEventKey = target => {
50
+ return ['onClick', 'onContextMenu', 'onDoubleClick', 'onPointerUp', 'onPointerDown', 'onPointerOver', 'onPointerOut', 'onPointerEnter', 'onPointerLeave', 'onPointerMove', // 'onPointerMissed',
51
+ // 'onUpdate',
52
+ 'onWheel'].includes(target);
575
53
  };
576
- const isLunchboxComponent = (node) => {
577
- return node?.$el && node?.$el?.hasOwnProperty?.('instance');
54
+ const isLunchboxComponent = node => {
55
+ return node?.$el && node?.$el?.hasOwnProperty?.('instance');
578
56
  };
579
- const isLunchboxDomComponent = (node) => {
580
- if (node?.metaType === 'domMeta')
581
- return true;
582
- const typeToCheck = typeof node === 'string' ? node : node?.type;
583
- return lunchboxDomComponentNames.includes(typeToCheck ?? '');
57
+ const isLunchboxDomComponent = node => {
58
+ if (node?.metaType === 'domMeta') return true;
59
+ return node?.props?.['data-lunchbox'];
584
60
  };
585
- const isLunchboxStandardNode = (node) => {
586
- return node?.metaType === 'standardMeta';
61
+ const isLunchboxStandardNode = node => {
62
+ return node?.metaType === 'standardMeta';
587
63
  };
588
- const isLunchboxRootNode = (node) => {
589
- return node.isLunchboxRootNode;
64
+ const isLunchboxRootNode = node => {
65
+ return node.isLunchboxRootNode;
590
66
  };
591
67
 
592
- const interactables = [];
593
- const addInteractable = (target) => {
594
- interactables.push(target);
595
- };
596
- const removeInteractable = (target) => {
597
- const idx = interactables.indexOf(target);
598
- if (idx !== -1) {
599
- interactables.splice(idx, 1);
600
- }
601
- };
68
+ /** Create a new Lunchbox comment node. */
69
+
70
+ function createCommentNode(options = {}) {
71
+ const defaults = {
72
+ text: options.text ?? ''
73
+ };
74
+ return new exports.MiniDom.RendererCommentNode({ ...defaults,
75
+ ...options,
76
+ metaType: 'commentMeta'
77
+ });
78
+ }
79
+ /** Create a new DOM node. */
602
80
 
603
- /** Mouse is down, touch is pressed, etc */
604
- const inputActive = vue.ref(false);
81
+ function createDomNode(options = {}) {
82
+ const domElement = document.createElement(options.type ?? '');
83
+ const defaults = {
84
+ domElement
85
+ };
86
+ const node = new exports.MiniDom.RendererDomNode({ ...defaults,
87
+ ...options,
88
+ metaType: 'domMeta'
89
+ });
90
+ return node;
91
+ }
92
+ /** Create a new Lunchbox text node. */
93
+
94
+ function createTextNode(options = {}) {
95
+ const defaults = {
96
+ text: options.text ?? ''
97
+ };
98
+ return new exports.MiniDom.RendererTextNode({ ...options,
99
+ ...defaults,
100
+ metaType: 'textMeta'
101
+ });
102
+ }
103
+ /** Create a new Lunchbox standard node. */
104
+
105
+ function createNode(options = {}, props = {}) {
106
+ const defaults = {
107
+ attached: options.attached ?? [],
108
+ attachedArray: options.attachedArray ?? {},
109
+ instance: options.instance ?? null
110
+ };
111
+ const node = new exports.MiniDom.RendererStandardNode({ ...options,
112
+ ...defaults,
113
+ metaType: 'standardMeta'
114
+ });
115
+
116
+ if (node.type && !isLunchboxRootNode(node) && !node.instance) {
117
+ node.instance = instantiateThreeObject({ ...node,
118
+ props: { ...node.props,
119
+ ...props
120
+ }
121
+ });
122
+ }
123
+
124
+ return node;
125
+ }
605
126
 
606
127
  /** Add an event listener to the given node. Also creates the event teardown function and any necessary raycaster/interaction dictionary updates. */
607
- function addEventListener({ node, key, value, }) {
608
- // create new records for this key if needed
609
- if (!node.eventListeners[key]) {
610
- node.eventListeners[key] = [];
611
- }
612
- if (!node.eventListenerRemoveFunctions[key]) {
613
- node.eventListenerRemoveFunctions[key] = [];
614
- }
615
- // add event listener
616
- node.eventListeners[key].push(value);
617
- // if we need it, let's get/create the main raycaster
618
- if (interactionsRequiringRaycaster.includes(key)) {
619
- // we're not using `v` here, we're just making sure the raycaster has been created
620
- // TODO: is this necessary?
621
- ensuredRaycaster.value;
622
- if (node.instance && !interactables.includes(node)) {
623
- addInteractable(node);
624
- node.eventListenerRemoveFunctions[key].push(() => removeInteractable(node));
128
+ function addEventListener({
129
+ node,
130
+ key,
131
+ interactables,
132
+ value
133
+ }) {
134
+ // create new records for this key if needed
135
+ if (!node.eventListeners[key]) {
136
+ node.eventListeners[key] = [];
137
+ }
138
+
139
+ if (!node.eventListenerRemoveFunctions[key]) {
140
+ node.eventListenerRemoveFunctions[key] = [];
141
+ } // add event listener
142
+
143
+
144
+ node.eventListeners[key].push(value); // if we need it, let's get/create the main raycaster
145
+
146
+ if (interactionsRequiringRaycaster.includes(key)) {
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);
625
156
  }
157
+ });
626
158
  }
627
- // register click, pointerdown, pointerup
628
- if (key === 'onClick' || key === 'onPointerDown' || key === 'onPointerUp') {
629
- const stop = vue.watch(() => inputActive.value, (isDown) => {
630
- const idx = currentIntersections
631
- .map((v) => v.element)
632
- .findIndex((v) => v.instance &&
633
- v.instance.uuid === node.instance?.uuid);
634
- if (idx !== -1) {
635
- if (isDown &&
636
- (key === 'onClick' || key === 'onPointerDown')) {
637
- node.eventListeners[key].forEach((func) => {
638
- func({
639
- intersection: currentIntersections[idx].intersection,
640
- });
641
- });
642
- }
643
- else if (!isDown && key === 'onPointerUp') {
644
- node.eventListeners[key].forEach((func) => {
645
- func({
646
- intersection: currentIntersections[idx].intersection,
647
- });
648
- });
649
- }
650
- }
651
- });
652
- node.eventListenerRemoveFunctions[key].push(stop);
653
- }
654
- return node;
159
+ }
160
+
161
+ return node;
655
162
  }
656
- const interactionsRequiringRaycaster = [
657
- 'onClick',
658
- 'onPointerUp',
659
- 'onPointerDown',
660
- 'onPointerOver',
661
- 'onPointerOut',
662
- 'onPointerEnter',
663
- 'onPointerLeave',
664
- 'onPointerMove',
665
- // 'onPointerMissed',
163
+ const interactionsRequiringRaycaster = ['onClick', 'onPointerUp', 'onPointerDown', 'onPointerOver', 'onPointerOut', 'onPointerEnter', 'onPointerLeave', 'onPointerMove' // 'onPointerMissed',
666
164
  ];
667
165
 
668
- let mouseMoveListener;
669
- let mouseDownListener;
670
- let mouseUpListener;
671
- const mousePos = vue.ref({ x: Infinity, y: Infinity });
672
- let autoRaycasterEventsInitialized = false;
673
- let frameID$1;
674
- const setupAutoRaycaster = (node) => {
675
- const instance = node.instance;
676
- if (!instance)
677
- return;
678
- // add mouse events once renderer is ready
679
- let stopWatcher = null;
680
- stopWatcher = vue.watch(() => ensureRenderer.value, (renderer) => {
681
- // make sure renderer exists
682
- if (!renderer?.instance)
683
- return;
684
- // cancel early if autoraycaster exists
685
- if (autoRaycasterEventsInitialized) {
686
- if (stopWatcher)
687
- stopWatcher();
688
- return;
689
- }
690
- // create mouse events
691
- mouseMoveListener = (evt) => {
692
- const screenWidth = (renderer.instance.domElement.width ?? 1) /
693
- globals.dpr.value;
694
- const screenHeight = (renderer.instance.domElement.height ?? 1) /
695
- globals.dpr.value;
696
- mousePos.value.x = (evt.offsetX / screenWidth) * 2 - 1;
697
- mousePos.value.y = -(evt.offsetY / screenHeight) * 2 + 1;
698
- };
699
- mouseDownListener = () => (inputActive.value = true);
700
- mouseUpListener = () => (inputActive.value = false);
701
- // add mouse events
702
- renderer.instance.domElement.addEventListener('mousemove', mouseMoveListener);
703
- renderer.instance.domElement.addEventListener('mousedown', mouseDownListener);
704
- renderer.instance.domElement.addEventListener('mouseup', mouseUpListener);
705
- // TODO: add touch events
706
- // process mouse events asynchronously, whenever the mouse state changes
707
- vue.watch(() => [inputActive.value, mousePos.value.x, mousePos.value.y], () => {
708
- if (frameID$1)
709
- cancelAnimationFrame(frameID$1);
710
- frameID$1 = requestAnimationFrame(() => {
711
- autoRaycasterBeforeRender();
166
+ const resizeCanvas = (camera, renderer, scene, width, height) => {
167
+ // ignore if no element
168
+ if (!renderer?.domElement || !scene || !camera) return;
169
+ width = width ?? window.innerWidth;
170
+ height = height ?? window.innerHeight; // update camera
171
+
172
+ const aspect = width / height;
173
+
174
+ if (camera.type?.toLowerCase() === 'perspectivecamera') {
175
+ const perspectiveCamera = camera;
176
+ perspectiveCamera.aspect = aspect;
177
+ perspectiveCamera.updateProjectionMatrix();
178
+ } else if (camera.type?.toLowerCase() === 'orthographiccamera') {
179
+ // TODO: ortho camera update
180
+ const orthoCamera = camera;
181
+ const heightInTermsOfWidth = height / width;
182
+ orthoCamera.top = heightInTermsOfWidth * 10;
183
+ orthoCamera.bottom = -heightInTermsOfWidth * 10;
184
+ orthoCamera.right = 10;
185
+ orthoCamera.left = -10;
186
+ orthoCamera.updateProjectionMatrix();
187
+ } else ; // update canvas
188
+
189
+
190
+ renderer.setSize(width, height); // render immediately so there's no flicker
191
+
192
+ if (scene && camera) {
193
+ renderer.render(vue.toRaw(scene), vue.toRaw(camera));
194
+ }
195
+ };
196
+
197
+ const getInnerDimensions = node => {
198
+ const computedStyle = getComputedStyle(node);
199
+ const width = node.clientWidth - parseFloat(computedStyle.paddingLeft) - parseFloat(computedStyle.paddingRight);
200
+ const height = node.clientHeight - parseFloat(computedStyle.paddingTop) - parseFloat(computedStyle.paddingBottom);
201
+ return {
202
+ width,
203
+ height
204
+ };
205
+ };
206
+
207
+ const prepCanvas = (container, camera, renderer, scene, sizePolicy) => {
208
+ const containerElement = container.value?.domElement;
209
+ if (!containerElement) throw new Error('missing container'); // save and size element
210
+
211
+ const resizeCanvasByPolicy = () => {
212
+ if (sizePolicy === 'container') {
213
+ const dims = getInnerDimensions(containerElement);
214
+ resizeCanvas(camera, renderer, scene, dims.width, dims.height);
215
+ } else resizeCanvas(camera, renderer, scene);
216
+ };
217
+
218
+ resizeCanvasByPolicy(); // attach listeners
219
+
220
+ let observer = new ResizeObserver(() => {
221
+ resizeCanvasByPolicy();
222
+ }); // window.addEventListener('resize', resizeCanvas)
223
+
224
+ if (containerElement) {
225
+ observer.observe(containerElement);
226
+ } // cleanup
227
+
228
+
229
+ return {
230
+ dispose() {
231
+ if (containerElement) {
232
+ observer.unobserve(containerElement);
233
+ }
234
+ }
235
+
236
+ };
237
+ };
238
+
239
+ const LunchboxScene = vue.defineComponent({
240
+ name: 'LunchboxScene',
241
+
242
+ setup(props, {
243
+ slots
244
+ }) {
245
+ return () => vue.createVNode(vue.resolveComponent("scene"), null, {
246
+ default: () => [slots.default?.()]
247
+ });
248
+ }
249
+
250
+ });
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
712
277
  });
713
- });
714
- // mark complete
715
- autoRaycasterEventsInitialized = true;
716
- // cancel setup watcher
717
- if (stopWatcher) {
718
- stopWatcher();
278
+ });
719
279
  }
720
- }, { immediate: true });
721
- };
722
- // AUTO-RAYCASTER CALLBACK
723
- // ====================
724
- let currentIntersections = [];
725
- const autoRaycasterBeforeRender = () => {
726
- // setup
727
- const raycaster = ensuredRaycaster.value?.instance;
728
- const camera = ensuredCamera.value?.instance;
729
- if (!raycaster || !camera)
730
- return;
731
- raycaster.setFromCamera(globals.mousePos.value, camera);
732
- const intersections = raycaster.intersectObjects(interactables.map((v) => v.instance));
733
- let leaveValues = [], entering = [], staying = [];
734
- // intersection arrays
735
- leaveValues = currentIntersections.map((v) => v.intersection);
736
- // element arrays
737
- intersections?.forEach((intersection) => {
738
- const currentIdx = currentIntersections.findIndex((v) => v.intersection.object === intersection.object);
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
+
739
324
  if (currentIdx === -1) {
740
- const found = interactables.find((v) => v.instance?.uuid === intersection.object.uuid);
741
- if (found) {
742
- entering.push({ element: found, intersection });
743
- }
744
- }
745
- else {
746
- const found = interactables.find((v) => v.instance?.uuid === intersection.object.uuid);
747
- if (found) {
748
- staying.push({ element: found, intersection });
749
- }
750
- }
751
- // this is a current intersection, so it won't be in our `leave` array
752
- const leaveIdx = leaveValues.findIndex((v) => v.object.uuid === intersection.object.uuid);
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
+
753
347
  if (leaveIdx !== -1) {
754
- leaveValues.splice(leaveIdx, 1);
348
+ leaveValues.splice(leaveIdx, 1);
755
349
  }
756
- });
757
- const leaving = leaveValues.map((intersection) => {
350
+ });
351
+ const leaving = leaveValues.map(intersection => {
758
352
  return {
759
- element: interactables.find((interactable) => interactable.instance?.uuid === intersection.object.uuid),
760
- intersection,
353
+ element: interactables?.value.find(interactable => interactable.instance?.uuid === intersection.object.uuid),
354
+ intersection
761
355
  };
762
- });
763
- // new interactions
764
- entering.forEach(({ element, intersection }) => {
356
+ }); // new interactions
357
+
358
+ entering.forEach(({
359
+ element,
360
+ intersection
361
+ }) => {
765
362
  fireEventsFromIntersections({
766
- element,
767
- eventKeys: ['onPointerEnter'],
768
- intersection,
363
+ element,
364
+ eventKeys: ['onPointerEnter'],
365
+ intersection
769
366
  });
770
- });
771
- // unchanged interactions
772
- staying.forEach(({ element, intersection }) => {
773
- const eventKeys = [
774
- 'onPointerOver',
775
- 'onPointerMove',
776
- ];
777
- fireEventsFromIntersections({ element, eventKeys, intersection });
778
- });
779
- // exited interactions
780
- leaving.forEach(({ element, intersection }) => {
781
- const eventKeys = [
782
- 'onPointerLeave',
783
- 'onPointerOut',
784
- ];
785
- fireEventsFromIntersections({ element, eventKeys, intersection });
786
- });
787
- currentIntersections = [].concat(entering, staying);
788
- };
789
- // utility function for firing multiple callbacks and multiple events on a Lunchbox.Element
790
- const fireEventsFromIntersections = ({ element, eventKeys, intersection, }) => {
791
- if (!element)
792
- return;
793
- eventKeys.forEach((eventKey) => {
794
- if (element.eventListeners[eventKey]) {
795
- element.eventListeners[eventKey].forEach((cb) => {
796
- cb({ intersection });
797
- });
798
- }
799
- });
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
+
440
+ /** fixed & fill styling for container */
441
+
442
+ const fillStyle = position => {
443
+ return {
444
+ position,
445
+ top: 0,
446
+ right: 0,
447
+ bottom: 0,
448
+ left: 0,
449
+ width: '100%',
450
+ height: '100%',
451
+ display: 'block'
452
+ };
800
453
  };
801
454
 
802
- // ENSURE ROOT
803
- // ====================
804
- const rootUuid = 'LUNCHBOX_ROOT';
805
- exports.lunchboxTree = void 0;
806
- function ensureRootNode(options = {}) {
807
- if (!exports.lunchboxTree) {
808
- exports.lunchboxTree = new MiniDom.RendererRootNode(options);
809
- }
810
- return exports.lunchboxTree;
811
- }
812
- // This is used in `buildEnsured` below and `LunchboxWrapper`
813
- /** Search the overrides record and the node tree for a node in the given types */
814
- function tryGetNodeWithInstanceType(pascalCaseTypes) {
815
- if (!Array.isArray(pascalCaseTypes)) {
816
- pascalCaseTypes = [pascalCaseTypes];
817
- }
818
- // default to override if we have one
819
- for (let singleType of pascalCaseTypes) {
820
- if (overrides[singleType])
821
- return overrides[singleType];
455
+ const LunchboxWrapper = vue.defineComponent({
456
+ name: 'Lunchbox',
457
+ props: {
458
+ // These should match the Lunchbox.WrapperProps interface
459
+ background: String,
460
+ cameraArgs: Array,
461
+ cameraLook: Array,
462
+ cameraLookAt: Array,
463
+ cameraPosition: Array,
464
+ dpr: Number,
465
+ ortho: Boolean,
466
+ orthographic: Boolean,
467
+ r3f: Boolean,
468
+ rendererArguments: Object,
469
+ rendererProperties: Object,
470
+ sizePolicy: String,
471
+ shadow: [Boolean, Object],
472
+ transparent: Boolean,
473
+ zoom: Number,
474
+ updateSource: Object
475
+ },
476
+
477
+ setup(props, context) {
478
+ const canvas = vue.ref();
479
+ let dpr = props.dpr ?? -1;
480
+ const container = vue.ref();
481
+ const renderer = vue.ref();
482
+ const camera = vue.ref();
483
+ const scene = vue.ref();
484
+ const globals = useGlobals();
485
+ const updateGlobals = useUpdateGlobals();
486
+ const app = useApp();
487
+ const consolidatedCameraProperties = vue.reactive({});
488
+ const startCallbacks = useStartCallbacks(); // https://threejs.org/docs/index.html#manual/en/introduction/Color-management
489
+
490
+ if (props.r3f && THREE__namespace?.ColorManagement) {
491
+ THREE__namespace.ColorManagement.legacyMode = false;
822
492
  }
823
- // look for auto-created node
824
- for (let singleType of pascalCaseTypes) {
825
- const found = autoCreated[singleType] ||
826
- allNodes.find((node) => node.type?.toLowerCase() ===
827
- singleType.toLowerCase());
828
- // cancel if found example is marked !isDefault
829
- if (isMinidomNode(found) &&
830
- (found.props['is-default'] === false ||
831
- !found.props['isDefault'] === false)) {
832
- return null;
493
+
494
+ const interactables = useLunchboxInteractables(); // MOUNT
495
+ // ====================
496
+
497
+ vue.onMounted(async () => {
498
+ // canvas needs to exist (or user needs to handle it on their own)
499
+ if (!canvas.value && !context.slots?.renderer?.()?.length) throw new Error('missing canvas'); // no camera provided, so let's create one
500
+
501
+ if (!context.slots?.camera?.()?.length) {
502
+ if (props.cameraPosition) {
503
+ consolidatedCameraProperties.position = props.cameraPosition;
833
504
  }
834
- // if we have one, save and return
835
- if (found) {
836
- const createdAsNode = found;
837
- autoCreated[singleType] = createdAsNode;
838
- return createdAsNode;
505
+
506
+ if (props.cameraLook || props.cameraLookAt) {
507
+ consolidatedCameraProperties.lookAt = props.cameraLook || props.cameraLookAt;
839
508
  }
840
- }
841
- return null;
842
- }
843
- // GENERIC ENSURE FUNCTION
844
- // ====================
845
- // Problem:
846
- // I want to make sure an object of type Xyz exists in my Lunchbox app.
847
- // If it doesn't exist, I want to create it and add it to the root node.
848
- //
849
- // Solution:
850
- // export const ensuredXyz = buildEnsured<Xyz>('Xyz', 'FALLBACK_XYZ')
851
- //
852
- // Now in other components, you can do both:
853
- // import { ensuredXyz }
854
- // ensuredXyz.value (...)
855
- // and:
856
- // ensuredXyz.value = ...
857
- const autoCreated = vue.reactive({});
858
- const overrides = vue.reactive({});
859
- /**
860
- * Build a computed ensured value with a getter and setter.
861
- * @param pascalCaseTypes List of types this can be. Will autocreate first type if array provided.
862
- * @param fallbackUuid Fallback UUID to use.
863
- * @param props Props to pass to autocreated element
864
- * @returns Computed getter/setter for ensured object.
865
- */
866
- function buildEnsured(pascalCaseTypes, fallbackUuid, props = {}, callback = null) {
867
- // make sure we've got an array
868
- if (!Array.isArray(pascalCaseTypes)) {
869
- pascalCaseTypes = [pascalCaseTypes];
870
- }
871
- // add type for autoCreated and overrides
872
- for (let singleType of pascalCaseTypes) {
873
- if (!autoCreated[singleType]) {
874
- autoCreated[singleType] = null;
509
+
510
+ if (props.zoom !== undefined) {
511
+ consolidatedCameraProperties.zoom = props.zoom;
875
512
  }
876
- if (!overrides[singleType]) {
877
- overrides[singleType] = null;
513
+ } // SCENE
514
+ // ====================
515
+ // set background color
516
+
517
+
518
+ if (scene.value?.$el?.instance && props.background) {
519
+ scene.value.$el.instance.background = new THREE__namespace.Color(props.background);
520
+ } // MISC PROPERTIES
521
+ // ====================
522
+
523
+
524
+ if (dpr === -1) {
525
+ dpr = window.devicePixelRatio;
526
+ }
527
+
528
+ updateGlobals?.({
529
+ dpr
530
+ });
531
+
532
+ while (!renderer.value?.$el?.instance && // TODO: remove `as any`
533
+ !renderer.value?.component?.ctx.$el?.instance) {
534
+ await new Promise(r => requestAnimationFrame(r));
535
+ }
536
+
537
+ while (!scene.value?.$el?.instance && // TODO: remove `as any`
538
+ !scene.value?.component?.ctx.$el?.instance) {
539
+ await new Promise(r => requestAnimationFrame(r));
540
+ }
541
+
542
+ const normalizedRenderer = renderer.value?.$el?.instance ?? renderer.value?.component?.ctx.$el?.instance;
543
+ normalizedRenderer.setPixelRatio(globals.dpr);
544
+ const normalizedScene = scene.value?.$el?.instance ?? scene.value?.component?.ctx.$el?.instance;
545
+ const normalizedCamera = camera.value?.$el?.instance ?? camera.value?.component?.ctx.$el?.instance; // TODO: update DPR on monitor switch
546
+ // prep canvas (sizing, observe, unmount, etc)
547
+ // (only run if no custom renderer)
548
+
549
+ if (!context.slots?.renderer?.()?.length) {
550
+ // TODO: use dispose
551
+ prepCanvas(container, normalizedCamera, normalizedRenderer, normalizedScene, props.sizePolicy);
552
+
553
+ if (props.r3f) {
554
+ normalizedRenderer.outputEncoding = THREE__namespace.sRGBEncoding;
555
+ normalizedRenderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
556
+ } // update render sugar
557
+
558
+
559
+ const sugar = {
560
+ shadow: props.shadow
561
+ };
562
+
563
+ if (sugar?.shadow) {
564
+ normalizedRenderer.shadowMap.enabled = true;
565
+
566
+ if (typeof sugar.shadow === 'object') {
567
+ normalizedRenderer.shadowMap.type = sugar.shadow.type;
568
+ }
878
569
  }
879
- }
880
- return vue.computed({
881
- get() {
882
- // try to get existing type
883
- const existing = tryGetNodeWithInstanceType(pascalCaseTypes);
884
- if (existing)
885
- return existing;
886
- // otherwise, create a new node
887
- const root = ensureRootNode();
888
- const node = createNode({
889
- type: pascalCaseTypes[0],
890
- uuid: fallbackUuid,
891
- props,
892
- });
893
- root.addChild(node);
894
- autoCreated[pascalCaseTypes[0]] = node;
895
- if (callback) {
896
- callback(node);
897
- }
898
- return node;
899
- },
900
- set(val) {
901
- const t = val.type ?? '';
902
- const pascalType = t[0].toUpperCase() + t.slice(1);
903
- overrides[pascalType] = val;
904
- },
905
- });
906
- }
907
- // ENSURE CAMERA
908
- // ====================
909
- const fallbackCameraUuid = 'FALLBACK_CAMERA';
910
- const defaultCamera = buildEnsured(['PerspectiveCamera', 'OrthographicCamera'], fallbackCameraUuid, { args: [45, 0.5625, 1, 1000] });
911
- /** Special value to be changed ONLY in `LunchboxWrapper`.
912
- * Functions waiting for a Camera need to wait for this to be true. */
913
- const cameraReady = vue.ref(false);
914
- const ensuredCamera = vue.computed({
915
- get() {
916
- return (cameraReady.value ? defaultCamera.value : null);
917
- },
918
- set(val) {
919
- const t = val.type ?? '';
920
- const pascalType = t[0].toUpperCase() + t.slice(1);
921
- overrides[pascalType] = val;
922
- },
923
- });
924
- // ENSURE RENDERER
925
- // ====================
926
- const fallbackRendererUuid = 'FALLBACK_RENDERER';
927
- const ensuredRenderer = buildEnsured(
928
- // TODO: ensure support for css/svg renderers
929
- ['WebGLRenderer'], //, 'CSS2DRenderer', 'CSS3DRenderer', 'SVGRenderer'],
930
- fallbackRendererUuid, {});
931
- /** Special value to be changed ONLY in `LunchboxWrapper`.
932
- * Functions waiting for a Renderer need to wait for this to be true. */
933
- const rendererReady = vue.ref(false);
934
- const ensureRenderer = vue.computed({
935
- get() {
936
- return (rendererReady.value ? ensuredRenderer.value : null);
937
- },
938
- set(val) {
939
- const t = val.type ?? '';
940
- const pascalType = t[0].toUpperCase() + t.slice(1);
941
- overrides[pascalType] = val;
942
- },
570
+ } // START
571
+ // ====================
572
+
573
+
574
+ if (!app) {
575
+ throw new Error('error creating app');
576
+ } // save renderer, scene, camera
577
+
578
+
579
+ app.config.globalProperties.lunchbox.camera = normalizedCamera;
580
+ app.config.globalProperties.lunchbox.renderer = normalizedRenderer;
581
+ app.config.globalProperties.lunchbox.scene = normalizedScene;
582
+
583
+ for (let startCallback of startCallbacks ?? []) {
584
+ startCallback({
585
+ app,
586
+ camera: normalizedCamera,
587
+ renderer: normalizedRenderer,
588
+ scene: normalizedScene
589
+ });
590
+ } // KICK UPDATE
591
+ // ====================
592
+
593
+
594
+ update({
595
+ app,
596
+ camera: normalizedCamera,
597
+ renderer: normalizedRenderer,
598
+ scene: normalizedScene,
599
+ updateSource: props.updateSource
600
+ });
601
+ }); // UNMOUNT
602
+ // ====================
603
+
604
+ vue.onBeforeUnmount(() => {
605
+ cancelUpdate();
606
+ cancelUpdateSource();
607
+ }); // RENDER FUNCTION
608
+ // ====================
609
+
610
+ const containerFillStyle = props.sizePolicy === 'container' ? 'static' : 'absolute';
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
+
642
+ return () => vue.createVNode(vue.Fragment, null, [context.slots?.renderer?.()?.length ? // TODO: remove `as any` cast
643
+ renderer.value = context.slots?.renderer?.()[0] : // ...otherwise, add canvas...
644
+ vue.createVNode(vue.Fragment, null, [vue.createVNode("div", {
645
+ "class": "lunchbox-container",
646
+ "style": fillStyle(containerFillStyle),
647
+ "ref": container,
648
+ "data-lunchbox": "true"
649
+ }, [vue.createVNode("canvas", {
650
+ "ref": canvas,
651
+ "class": "lunchbox-canvas",
652
+ "style": fillStyle(canvasFillStyle),
653
+ "data-lunchbox": "true"
654
+ }, null)]), canvas.value?.domElement && vue.createVNode(vue.resolveComponent("webGLRenderer"), vue.mergeProps(props.rendererProperties ?? {}, {
655
+ "ref": renderer,
656
+ "args": [{
657
+ alpha: props.transparent,
658
+ antialias: true,
659
+ canvas: canvas.value?.domElement,
660
+ powerPreference: !!props.r3f ? 'high-performance' : 'default',
661
+ ...(props.rendererArguments ?? {})
662
+ }]
663
+ }), null)]), context.slots?.scene?.()?.length ? // TODO: remove `as any` cast
664
+ scene.value = context.slots?.scene?.()[0] : // ...otherwise, add default scene
665
+ // TODO: why does this need to be a separate component? <scene> throws an error
666
+ vue.createVNode(LunchboxScene, {
667
+ "ref": scene
668
+ }, {
669
+ default: () => [context.slots?.default?.()]
670
+ }), context.slots?.camera?.()?.length ? // TODO: remove `any` cast
671
+ camera.value : props.ortho || props.orthographic ? vue.createVNode(vue.resolveComponent("orthographicCamera"), vue.mergeProps({
672
+ "ref": camera,
673
+ "args": props.cameraArgs ?? []
674
+ }, consolidatedCameraProperties), null) : vue.createVNode(vue.resolveComponent("perspectiveCamera"), vue.mergeProps({
675
+ "ref": camera,
676
+ "args": props.cameraArgs ?? [props.r3f ? 75 : 45, 0.5625, 1, 1000]
677
+ }, consolidatedCameraProperties), null), interactables?.value.length && vue.createVNode(LunchboxEventHandlers, null, null)]);
678
+ }
679
+
943
680
  });
944
- // ENSURE SCENE
945
- // ====================
946
- const fallbackSceneUuid = 'FALLBACK_SCENE';
947
- const ensuredScene = buildEnsured('Scene', fallbackSceneUuid);
948
- // ENSURE AUTO-RAYCASTER
949
- const autoRaycasterUuid = 'AUTO_RAYCASTER';
950
- // `unknown` is intentional here - we need to typecast the node since Raycaster isn't an Object3D
951
- const ensuredRaycaster = buildEnsured('Raycaster', autoRaycasterUuid, {}, (node) => setupAutoRaycaster(node));
952
681
 
953
- /** Create a new Lunchbox comment node. */
954
- function createCommentNode(options = {}) {
955
- const defaults = {
956
- text: options.text ?? '',
957
- };
958
- return new MiniDom.RendererCommentNode({
959
- ...defaults,
960
- ...options,
961
- metaType: 'commentMeta',
962
- });
963
- }
964
- /** Create a new DOM node. */
965
- function createDomNode(options = {}) {
966
- const domElement = document.createElement(options.type ?? '');
967
- const defaults = {
968
- domElement,
969
- };
970
- const node = new MiniDom.RendererDomNode({
971
- ...defaults,
972
- ...options,
973
- metaType: 'domMeta',
974
- });
975
- return node;
976
- }
977
- /** Create a new Lunchbox text node. */
978
- function createTextNode(options = {}) {
979
- const defaults = {
980
- text: options.text ?? '',
981
- };
982
- return new MiniDom.RendererTextNode({
983
- ...options,
984
- ...defaults,
985
- metaType: 'textMeta',
986
- });
987
- }
988
- /** Create a new Lunchbox standard node. */
989
- function createNode(options = {}, props = {}) {
990
- const defaults = {
991
- attached: options.attached ?? [],
992
- attachedArray: options.attachedArray ?? {},
993
- instance: options.instance ?? null,
682
+ // list of all components to register out of the box
683
+ const autoGeneratedComponents = [// ThreeJS basics
684
+ 'mesh', 'instancedMesh', 'scene', 'sprite', 'object3D', // geometry
685
+ 'instancedBufferGeometry', 'bufferGeometry', 'boxBufferGeometry', 'circleBufferGeometry', 'coneBufferGeometry', 'cylinderBufferGeometry', 'dodecahedronBufferGeometry', 'extrudeBufferGeometry', 'icosahedronBufferGeometry', 'latheBufferGeometry', 'octahedronBufferGeometry', 'parametricBufferGeometry', 'planeBufferGeometry', 'polyhedronBufferGeometry', 'ringBufferGeometry', 'shapeBufferGeometry', 'sphereBufferGeometry', 'tetrahedronBufferGeometry', 'textBufferGeometry', 'torusBufferGeometry', 'torusKnotBufferGeometry', 'tubeBufferGeometry', 'wireframeGeometry', 'parametricGeometry', 'tetrahedronGeometry', 'octahedronGeometry', 'icosahedronGeometry', 'dodecahedronGeometry', 'polyhedronGeometry', 'tubeGeometry', 'torusKnotGeometry', 'torusGeometry', // textgeometry has been moved to /examples/jsm/geometries/TextGeometry
686
+ // 'textGeometry',
687
+ 'sphereGeometry', 'ringGeometry', 'planeGeometry', 'latheGeometry', 'shapeGeometry', 'extrudeGeometry', 'edgesGeometry', 'coneGeometry', 'cylinderGeometry', 'circleGeometry', 'boxGeometry', // materials
688
+ 'material', 'shadowMaterial', 'spriteMaterial', 'rawShaderMaterial', 'shaderMaterial', 'pointsMaterial', 'meshPhysicalMaterial', 'meshStandardMaterial', 'meshPhongMaterial', 'meshToonMaterial', 'meshNormalMaterial', 'meshLambertMaterial', 'meshDepthMaterial', 'meshDistanceMaterial', 'meshBasicMaterial', 'meshMatcapMaterial', 'lineDashedMaterial', 'lineBasicMaterial', // lights
689
+ 'light', 'spotLightShadow', 'spotLight', 'pointLight', 'rectAreaLight', 'hemisphereLight', 'directionalLightShadow', 'directionalLight', 'ambientLight', 'lightShadow', 'ambientLightProbe', 'hemisphereLightProbe', 'lightProbe', // textures
690
+ 'texture', 'videoTexture', 'dataTexture', 'dataTexture3D', 'compressedTexture', 'cubeTexture', 'canvasTexture', 'depthTexture', // Texture loaders
691
+ 'textureLoader', // misc
692
+ 'group', 'catmullRomCurve3', 'points', 'raycaster', // helpers
693
+ 'cameraHelper', // cameras
694
+ 'camera', 'perspectiveCamera', 'orthographicCamera', 'cubeCamera', 'arrayCamera', // renderers
695
+ 'webGLRenderer'
696
+ /*
697
+ // List copied from r3f:
698
+ // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
699
+ // NOT IMPLEMENTED (can be added via Extend - docs.lunchboxjs.com/components/extend/):
700
+ audioListener: AudioListenerProps
701
+ positionalAudio: PositionalAudioProps
702
+ lOD: LODProps
703
+ skinnedMesh: SkinnedMeshProps
704
+ skeleton: SkeletonProps
705
+ bone: BoneProps
706
+ lineSegments: LineSegmentsProps
707
+ lineLoop: LineLoopProps
708
+ // see `audio`
709
+ // line: LineProps
710
+ immediateRenderObject: ImmediateRenderObjectProps
711
+ // primitive
712
+ primitive: PrimitiveProps
713
+ // helpers
714
+ spotLightHelper: SpotLightHelperProps
715
+ skeletonHelper: SkeletonHelperProps
716
+ pointLightHelper: PointLightHelperProps
717
+ hemisphereLightHelper: HemisphereLightHelperProps
718
+ gridHelper: GridHelperProps
719
+ polarGridHelper: PolarGridHelperProps
720
+ directionalLightHelper: DirectionalLightHelperProps
721
+ boxHelper: BoxHelperProps
722
+ box3Helper: Box3HelperProps
723
+ planeHelper: PlaneHelperProps
724
+ arrowHelper: ArrowHelperProps
725
+ axesHelper: AxesHelperProps
726
+ // misc
727
+ vector2: Vector2Props
728
+ vector3: Vector3Props
729
+ vector4: Vector4Props
730
+ euler: EulerProps
731
+ matrix3: Matrix3Props
732
+ matrix4: Matrix4Props
733
+ quaternion: QuaternionProps
734
+ bufferAttribute: BufferAttributeProps
735
+ instancedBufferAttribute: InstancedBufferAttributeProps
736
+ color: ColorProps
737
+ fog: FogProps
738
+ fogExp2: FogExp2Props
739
+ shape: ShapeProps
740
+ */
741
+ ];
742
+
743
+ const catalogue = {}; // component creation utility
744
+
745
+ const createComponent$1 = tag => vue.defineComponent({
746
+ inheritAttrs: false,
747
+ name: tag,
748
+
749
+ setup(props, context) {
750
+ return () => {
751
+ return vue.h(tag, context.attrs, context.slots?.default?.() || []);
994
752
  };
995
- const node = new MiniDom.RendererStandardNode({
996
- ...options,
997
- ...defaults,
998
- metaType: 'standardMeta',
999
- });
1000
- if (node.type && !isLunchboxRootNode(node) && !node.instance) {
1001
- // if (node.type.includes('Camera')) {
1002
- // console.log(node.type, {
1003
- // ...node.props,
1004
- // ...props,
1005
- // })
1006
- // console.trace()
1007
- // }
1008
- node.instance = instantiateThreeObject({
1009
- ...node,
1010
- props: {
1011
- ...node.props,
1012
- ...props,
1013
- },
1014
- });
1015
- }
1016
- // TODO: these manual overrides are a bit brittle - replace?
1017
- if (node.type?.toLowerCase() === 'scene') {
1018
- // manually set scene override
1019
- ensuredScene.value = node;
1020
- }
1021
- else if (node.type?.toLowerCase().endsWith('camera')) {
1022
- ensuredCamera.value = node;
1023
- }
1024
- return node;
1025
- }
753
+ }
754
+
755
+ }); // turn components into registered map
756
+
757
+
758
+ const processed = autoGeneratedComponents.map(createComponent$1).reduce((acc, curr) => {
759
+ acc[curr.name] = curr;
760
+ return acc;
761
+ }, {});
762
+ const components = { ...processed,
763
+ Lunchbox: LunchboxWrapper
764
+ };
765
+
766
+ const createComponent = tag => vue.defineComponent({
767
+ inheritAttrs: false,
768
+ name: tag,
769
+
770
+ render() {
771
+ return vue.h(tag, this.$attrs, this.$slots?.default?.() || []);
772
+ }
1026
773
 
1027
- const createComponent = (tag) => vue.defineComponent({
1028
- inheritAttrs: false,
1029
- name: tag,
1030
- render() {
1031
- return vue.h(tag, this.$attrs, this.$slots?.default?.() || []);
1032
- },
1033
774
  });
1034
- const extend = ({ app, ...targets }) => {
1035
- Object.keys(targets).forEach((key) => {
1036
- app.component(key, createComponent(key));
1037
- catalogue[key] = targets[key];
1038
- });
775
+
776
+ const extend = ({
777
+ app,
778
+ ...targets
779
+ }) => {
780
+ Object.keys(targets).forEach(key => {
781
+ app.component(key, createComponent(key));
782
+ catalogue[key] = targets[key];
783
+ });
1039
784
  };
1040
785
 
1041
786
  /** Process props into either themselves or the $attached value */
1042
- function processProp({ node, prop, }) {
1043
- // return $attachedArray value if needed
1044
- if (typeof prop === 'string' && prop.startsWith('$attachedArray')) {
1045
- return node.attachedArray[prop.replace('$attachedArray.', '')];
1046
- }
1047
- // return $attached value if needed
1048
- if (typeof prop === 'string' && prop.startsWith('$attached')) {
1049
- return node.attached[prop.replace('$attached.', '')];
1050
- }
1051
- // otherwise, return plain value
1052
- return prop;
787
+ function processProp({
788
+ node,
789
+ prop
790
+ }) {
791
+ // return $attachedArray value if needed
792
+ if (typeof prop === 'string' && prop.startsWith('$attachedArray')) {
793
+ return node.attachedArray[prop.replace('$attachedArray.', '')];
794
+ } // return $attached value if needed
795
+
796
+
797
+ if (typeof prop === 'string' && prop.startsWith('$attached')) {
798
+ return node.attached[prop.replace('$attached.', '')];
799
+ } // otherwise, return plain value
800
+
801
+
802
+ return prop;
1053
803
  }
1054
- function processPropAsArray({ node, prop, }) {
1055
- const isAttachedArray = typeof prop === 'string' && prop.startsWith('$attachedArray');
1056
- const output = processProp({ node, prop });
1057
- return Array.isArray(output) && isAttachedArray
1058
- ? output
1059
- : [output];
804
+ function processPropAsArray({
805
+ node,
806
+ prop
807
+ }) {
808
+ const isAttachedArray = typeof prop === 'string' && prop.startsWith('$attachedArray');
809
+ const output = processProp({
810
+ node,
811
+ prop
812
+ });
813
+ return Array.isArray(output) && isAttachedArray ? output : [output];
1060
814
  }
1061
815
 
1062
816
  function instantiateThreeObject(node) {
1063
- if (!node.type)
1064
- return null;
1065
- // what class will we be instantiating?
1066
- const uppercaseType = node.type[0].toUpperCase() + node.type.slice(1);
1067
- const targetClass = catalogue[node.type] || THREE__namespace[uppercaseType];
1068
- if (!targetClass)
1069
- throw `${uppercaseType} is not part of the THREE namespace! Did you forget to extend? import {extend} from 'lunchbox'; extend({app, YourComponent, ...})`;
1070
- // what args have we been provided?
1071
- const args = node.props.args ?? [];
1072
- // replace $attached values with their instances
1073
- // we need to guarantee everything comes back as an array so we can spread $attachedArrays,
1074
- // so we'll use processPropAsArray
1075
- const argsWrappedInArrays = args.map((arg) => {
1076
- return processPropAsArray({ node, prop: arg });
1077
- });
1078
- let processedArgs = [];
1079
- argsWrappedInArrays.forEach((arr) => {
1080
- processedArgs = processedArgs.concat(arr);
817
+ if (!node.type) return null; // what class will we be instantiating?
818
+
819
+ const uppercaseType = node.type[0].toUpperCase() + node.type.slice(1);
820
+ const translatedType = uppercaseType.replace(/Lunchbox$/, '');
821
+ const targetClass = catalogue[node.type] || THREE__namespace[uppercaseType] || catalogue[translatedType] || THREE__namespace[translatedType];
822
+ if (!targetClass) throw `${uppercaseType} is not part of the THREE namespace! Did you forget to extend? import {extend} from 'lunchbox'; extend({app, YourComponent, ...})`; // what args have we been provided?
823
+
824
+ const args = node.props.args ?? []; // replace $attached values with their instances
825
+ // we need to guarantee everything comes back as an array so we can spread $attachedArrays,
826
+ // so we'll use processPropAsArray
827
+
828
+ const argsWrappedInArrays = args.map(arg => {
829
+ return processPropAsArray({
830
+ node,
831
+ prop: arg
1081
832
  });
1082
- const instance = new targetClass(...processedArgs);
1083
- return instance;
833
+ });
834
+ let processedArgs = [];
835
+ argsWrappedInArrays.forEach(arr => {
836
+ processedArgs = processedArgs.concat(arr);
837
+ });
838
+ const instance = new targetClass(...processedArgs);
839
+ return instance;
1084
840
  }
1085
841
 
1086
842
  // Unique ID creation requires a high quality random # generator. In the browser we therefore
@@ -1121,9 +877,9 @@
1121
877
  }
1122
878
 
1123
879
  function stringify(arr) {
1124
- var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
1125
- // Note: Be careful editing this code! It's been tuned for performance
880
+ var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; // Note: Be careful editing this code! It's been tuned for performance
1126
881
  // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
882
+
1127
883
  var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one
1128
884
  // of the following:
1129
885
  // - One or more input array values don't map to a hex octet (leading to
@@ -1157,775 +913,1088 @@
1157
913
  return stringify(rnds);
1158
914
  }
1159
915
 
1160
- // MiniDom recreates DOM node properties and methods.
1161
916
  // Since Vue 3 is a DOM-first framework, many of its nodeOps depend on
1162
917
  // properties and methods the DOM naturally contains. MiniDom recreates
1163
918
  // those properties (as well as a few from the tree-model npm package)
1164
919
  // to make a DOM-like but otherwise agnostic hierarchy structure.
1165
- var MiniDom;
920
+
921
+ exports.MiniDom = void 0;
922
+
1166
923
  (function (MiniDom) {
1167
- class BaseNode {
1168
- constructor(options = {}, parent) {
1169
- this.parentNode = options?.parentNode ?? parent ?? null;
1170
- this.minidomType = 'MinidomBaseNode';
1171
- this.uuid = options?.uuid ?? v4();
1172
- allNodes.push(this);
1173
- }
1174
- uuid;
1175
- // DOM FEATURES
1176
- // ====================
1177
- parentNode;
1178
- get nextSibling() {
1179
- if (!this.parentNode)
1180
- return null;
1181
- const idx = this.parentNode.children.findIndex((n) => n.uuid === this.uuid);
1182
- // return next sibling if we're present and not the last child of the parent
1183
- if (idx !== -1 && idx < this.parentNode.children.length - 1) {
1184
- return this.parentNode.children[idx + 1];
1185
- }
1186
- return null;
1187
- }
1188
- insertBefore(child, anchor) {
1189
- child.removeAsChildFromAnyParents();
1190
- child.parentNode = this;
1191
- const anchorIdx = this.children.findIndex((n) => n.uuid === anchor?.uuid);
1192
- if (anchorIdx !== -1) {
1193
- this.children.splice(anchorIdx, 0, child);
1194
- }
1195
- else {
1196
- this.children.push(child);
1197
- }
1198
- }
1199
- removeChild(child) {
1200
- const idx = this.children.findIndex((n) => n?.uuid === child?.uuid);
1201
- if (idx !== -1) {
1202
- this.children.splice(idx, 1);
1203
- }
1204
- }
1205
- // TREE FEATURES
1206
- // ====================
1207
- children = [];
1208
- addChild(child) {
1209
- if (child) {
1210
- // remove child from any other parents
1211
- child.removeAsChildFromAnyParents();
1212
- // add to this node
1213
- child.parentNode = this;
1214
- this.insertBefore(child, null);
1215
- }
1216
- return this;
1217
- }
1218
- /** Get the array of Nodes representing the path from the root to this Node (inclusive). */
1219
- getPath() {
1220
- const output = [];
1221
- let current = this;
1222
- while (current) {
1223
- output.unshift(current);
1224
- current = current.parentNode;
1225
- }
1226
- return output;
1227
- }
1228
- /** Drop this node. Removes parent's knowledge of this node
1229
- * and resets this node's internal parent. */
1230
- drop() {
1231
- // remove parent
1232
- this.parentNode = null;
1233
- // remove as child
1234
- this.removeAsChildFromAnyParents();
1235
- }
1236
- /** Walk over the entire subtree. Return falsey value in callback to end early. */
1237
- // TODO: depth-first vs breadth-first
1238
- walk(callback) {
1239
- const queue = [this, ...this.children];
1240
- const traversed = [];
1241
- let canContinue = true;
1242
- while (queue.length && canContinue) {
1243
- const current = queue.shift();
1244
- if (current) {
1245
- if (traversed.includes(current))
1246
- continue;
1247
- traversed.push(current);
1248
- queue.push(...current.children.filter((child) => !traversed.includes(child)));
1249
- canContinue = callback(current);
1250
- }
1251
- else {
1252
- canContinue = false;
1253
- }
1254
- }
1255
- }
1256
- // INTERNAL FEATURES
1257
- // ====================
1258
- minidomType;
1259
- removeAsChildFromAnyParents() {
1260
- allNodes.forEach((node) => node.removeChild(this));
1261
- }
924
+ class BaseNode {
925
+ constructor(options = {}, parent) {
926
+ this.parentNode = options?.parentNode ?? parent ?? null;
927
+ this.minidomType = 'MinidomBaseNode';
928
+ this.uuid = options?.uuid ?? v4(); // allNodes.push(this)
1262
929
  }
1263
- MiniDom.BaseNode = BaseNode;
1264
- class RendererBaseNode extends MiniDom.BaseNode {
1265
- constructor(options = {}, parent) {
1266
- super(options, parent);
1267
- this.minidomType = 'RendererNode';
1268
- this.eventListeners = {};
1269
- this.eventListenerRemoveFunctions = {};
1270
- this.name = options.name ?? '';
1271
- this.metaType = options.metaType ?? 'standardMeta';
1272
- this.props = options.props ?? [];
1273
- this.type = options.type ?? '';
1274
- }
1275
- eventListeners;
1276
- eventListenerRemoveFunctions;
1277
- name;
1278
- metaType;
1279
- props;
1280
- type;
1281
- drop() {
1282
- super.drop();
1283
- // handle remove functions
1284
- Object.keys(this.eventListenerRemoveFunctions).forEach((key) => {
1285
- this.eventListenerRemoveFunctions[key].forEach((func) => func());
1286
- });
1287
- }
1288
- }
1289
- MiniDom.RendererBaseNode = RendererBaseNode;
930
+
931
+ uuid; // DOM FEATURES
1290
932
  // ====================
1291
- // SPECIFIC RENDERER NODES BELOW
933
+
934
+ parentNode;
935
+
936
+ get nextSibling() {
937
+ if (!this.parentNode) return null;
938
+ const idx = this.parentNode.children.findIndex(n => n.uuid === this.uuid); // return next sibling if we're present and not the last child of the parent
939
+
940
+ if (idx !== -1 && idx < this.parentNode.children.length - 1) {
941
+ return this.parentNode.children[idx + 1];
942
+ }
943
+
944
+ return null;
945
+ }
946
+
947
+ insertBefore(child, anchor) {
948
+ child.removeAsChildFromAnyParents();
949
+ child.parentNode = this;
950
+ const anchorIdx = this.children.findIndex(n => n.uuid === anchor?.uuid);
951
+
952
+ if (anchorIdx !== -1) {
953
+ this.children.splice(anchorIdx, 0, child);
954
+ } else {
955
+ this.children.push(child);
956
+ }
957
+ }
958
+
959
+ removeChild(child) {
960
+ const idx = this.children.findIndex(n => n?.uuid === child?.uuid);
961
+
962
+ if (idx !== -1) {
963
+ this.children.splice(idx, 1);
964
+ }
965
+ } // TREE FEATURES
1292
966
  // ====================
1293
- class RendererRootNode extends MiniDom.RendererBaseNode {
1294
- constructor(options = {}, parent) {
1295
- super(options, parent);
1296
- this.domElement =
1297
- options.domElement ?? document.createElement('div');
1298
- }
1299
- domElement;
1300
- isLunchboxRootNode = true;
967
+
968
+
969
+ children = [];
970
+
971
+ addChild(child) {
972
+ if (child) {
973
+ // remove child from any other parents
974
+ child.removeAsChildFromAnyParents(); // add to this node
975
+
976
+ child.parentNode = this;
977
+ this.insertBefore(child, null);
978
+ }
979
+
980
+ return this;
1301
981
  }
1302
- MiniDom.RendererRootNode = RendererRootNode;
1303
- class RendererCommentNode extends MiniDom.RendererBaseNode {
1304
- constructor(options = {}, parent) {
1305
- super(options, parent);
1306
- this.text = options.text ?? '';
1307
- }
1308
- text;
982
+ /** Get the array of Nodes representing the path from the root to this Node (inclusive). */
983
+
984
+
985
+ getPath() {
986
+ const output = [];
987
+ let current = this;
988
+
989
+ while (current) {
990
+ output.unshift(current);
991
+ current = current.parentNode;
992
+ }
993
+
994
+ return output;
1309
995
  }
1310
- MiniDom.RendererCommentNode = RendererCommentNode;
1311
- class RendererDomNode extends MiniDom.RendererBaseNode {
1312
- constructor(options = {}, parent) {
1313
- super(options, parent);
1314
- this.domElement =
1315
- options.domElement ?? document.createElement('div');
1316
- }
1317
- domElement;
996
+ /** Drop this node. Removes parent's knowledge of this node
997
+ * and resets this node's internal parent. */
998
+
999
+
1000
+ drop() {
1001
+ // remove as child
1002
+ this.removeAsChildFromAnyParents(); // remove parent
1003
+
1004
+ this.parentNode = null;
1318
1005
  }
1319
- MiniDom.RendererDomNode = RendererDomNode;
1320
- class RendererTextNode extends MiniDom.RendererBaseNode {
1321
- constructor(options = {}, parent) {
1322
- super(options, parent);
1323
- this.text = options.text ?? '';
1006
+ /** Walk over the entire subtree. Return falsey value in callback to end early. */
1007
+ // TODO: depth-first vs breadth-first
1008
+
1009
+
1010
+ walk(callback) {
1011
+ const queue = [this, ...this.children];
1012
+ const traversed = [];
1013
+ let canContinue = true;
1014
+
1015
+ while (queue.length && canContinue) {
1016
+ const current = queue.shift();
1017
+
1018
+ if (current) {
1019
+ if (traversed.includes(current)) continue;
1020
+ traversed.push(current);
1021
+ queue.push(...current.children.filter(child => !traversed.includes(child)));
1022
+ canContinue = callback(current);
1023
+ } else {
1024
+ canContinue = false;
1324
1025
  }
1325
- text;
1026
+ }
1027
+ } // INTERNAL FEATURES
1028
+ // ====================
1029
+
1030
+
1031
+ minidomType;
1032
+
1033
+ removeAsChildFromAnyParents() {
1034
+ this.parentNode?.removeChild(this);
1326
1035
  }
1327
- MiniDom.RendererTextNode = RendererTextNode;
1328
- class RendererStandardNode extends MiniDom.RendererBaseNode {
1329
- constructor(options = {}, parent) {
1330
- super(options, parent);
1331
- this.attached = options.attached ?? [];
1332
- this.attachedArray = options.attachedArray ?? {};
1333
- this.instance = options.instance ?? null;
1334
- }
1335
- attached;
1336
- attachedArray;
1337
- instance;
1036
+
1037
+ }
1038
+
1039
+ MiniDom.BaseNode = BaseNode;
1040
+
1041
+ class RendererBaseNode extends MiniDom.BaseNode {
1042
+ constructor(options = {}, parent) {
1043
+ super(options, parent);
1044
+ this.minidomType = 'RendererNode';
1045
+ this.eventListeners = {};
1046
+ this.eventListenerRemoveFunctions = {};
1047
+ this.name = options.name ?? '';
1048
+ this.metaType = options.metaType ?? 'standardMeta';
1049
+ this.props = options.props ?? [];
1050
+ this.type = options.type ?? '';
1338
1051
  }
1339
- MiniDom.RendererStandardNode = RendererStandardNode;
1340
- })(MiniDom || (MiniDom = {}));
1341
- function isMinidomNode(item) {
1342
- return item?.minidomType === 'RendererNode';
1343
- }
1344
- const rootNode = new MiniDom.RendererRootNode();
1345
- rootNode.minidomType = 'RootNode';
1346
1052
 
1347
- const startCallbacks = [];
1348
- const onStart = (cb, index = Infinity) => {
1349
- if (index === Infinity) {
1350
- startCallbacks.push(cb);
1053
+ eventListeners;
1054
+ eventListenerRemoveFunctions;
1055
+ name;
1056
+ metaType;
1057
+ props;
1058
+ type;
1059
+
1060
+ drop() {
1061
+ super.drop(); // handle remove functions
1062
+
1063
+ Object.keys(this.eventListenerRemoveFunctions).forEach(key => {
1064
+ this.eventListenerRemoveFunctions[key].forEach(func => func());
1065
+ });
1351
1066
  }
1352
- else {
1353
- startCallbacks.splice(index, 0, cb);
1067
+
1068
+ }
1069
+
1070
+ MiniDom.RendererBaseNode = RendererBaseNode; // ====================
1071
+ // SPECIFIC RENDERER NODES BELOW
1072
+ // ====================
1073
+
1074
+ class RendererRootNode extends MiniDom.RendererBaseNode {
1075
+ constructor(options = {}, parent) {
1076
+ super(options, parent);
1077
+ this.domElement = options.domElement ?? document.createElement('div');
1354
1078
  }
1355
- };
1356
1079
 
1357
- let frameID;
1358
- let watchStopHandle;
1359
- const beforeRender = [];
1360
- const afterRender = [];
1361
- const requestUpdate = (opts) => {
1362
- cancelUpdate();
1363
- frameID = requestAnimationFrame(() => update({
1364
- app: opts.app,
1365
- renderer: ensureRenderer.value?.instance,
1366
- scene: ensuredScene.value.instance,
1367
- camera: ensuredCamera.value?.instance,
1368
- updateSource: opts.updateSource,
1369
- }));
1370
- };
1371
- const update = (opts) => {
1372
- if (opts.updateSource) {
1373
- if (!watchStopHandle) {
1374
- // request next frame only when state changes
1375
- watchStopHandle = vue.watch(opts.updateSource, () => {
1376
- requestUpdate(opts);
1377
- }, {
1378
- deep: true,
1379
- });
1380
- }
1080
+ domElement;
1081
+ isLunchboxRootNode = true;
1082
+ }
1083
+
1084
+ MiniDom.RendererRootNode = RendererRootNode;
1085
+
1086
+ class RendererCommentNode extends MiniDom.RendererBaseNode {
1087
+ constructor(options = {}, parent) {
1088
+ super(options, parent);
1089
+ this.text = options.text ?? '';
1381
1090
  }
1382
- else {
1383
- // request next frame on a continuous loop
1384
- requestUpdate(opts);
1091
+
1092
+ text;
1093
+ }
1094
+
1095
+ MiniDom.RendererCommentNode = RendererCommentNode;
1096
+
1097
+ class RendererDomNode extends MiniDom.RendererBaseNode {
1098
+ constructor(options = {}, parent) {
1099
+ super(options, parent);
1100
+ this.domElement = options.domElement ?? document.createElement('div');
1385
1101
  }
1386
- // prep options
1387
- const { app, renderer, scene, camera } = opts;
1388
- // BEFORE RENDER
1389
- beforeRender.forEach((cb) => {
1390
- if (cb) {
1391
- cb(opts);
1392
- }
1393
- });
1394
- // RENDER
1395
- if (renderer && scene && camera) {
1396
- if (app.customRender) {
1397
- app.customRender(opts);
1398
- }
1399
- else {
1400
- renderer.render(vue.toRaw(scene), vue.toRaw(camera));
1401
- }
1102
+
1103
+ domElement;
1104
+ }
1105
+
1106
+ MiniDom.RendererDomNode = RendererDomNode;
1107
+
1108
+ class RendererTextNode extends MiniDom.RendererBaseNode {
1109
+ constructor(options = {}, parent) {
1110
+ super(options, parent);
1111
+ this.text = options.text ?? '';
1402
1112
  }
1403
- // AFTER RENDER
1404
- afterRender.forEach((cb) => {
1405
- if (cb) {
1406
- cb(opts);
1407
- }
1408
- });
1113
+
1114
+ text;
1115
+ }
1116
+
1117
+ MiniDom.RendererTextNode = RendererTextNode;
1118
+
1119
+ class RendererStandardNode extends MiniDom.RendererBaseNode {
1120
+ constructor(options = {}, parent) {
1121
+ super(options, parent);
1122
+ this.attached = options.attached ?? [];
1123
+ this.attachedArray = options.attachedArray ?? {};
1124
+ this.instance = options.instance ?? null;
1125
+ }
1126
+
1127
+ attached;
1128
+ attachedArray;
1129
+ instance;
1130
+ }
1131
+
1132
+ MiniDom.RendererStandardNode = RendererStandardNode;
1133
+ })(exports.MiniDom || (exports.MiniDom = {}));
1134
+
1135
+ function isMinidomNode(item) {
1136
+ return item?.minidomType === 'RendererNode';
1137
+ }
1138
+
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();
1158
+
1159
+ const requestUpdate = opts => {
1160
+ if (typeof opts.app.config.globalProperties.lunchbox.frameId === 'number') {
1161
+ cancelAnimationFrame(opts.app.config.globalProperties.lunchbox.frameId);
1162
+ }
1163
+
1164
+ opts.app.config.globalProperties.lunchbox.frameId = requestAnimationFrame(() => update({
1165
+ app: opts.app,
1166
+ renderer: opts.renderer,
1167
+ scene: opts.scene,
1168
+ camera: opts.camera,
1169
+ updateSource: opts.updateSource
1170
+ }));
1171
+ };
1172
+
1173
+ const update = opts => {
1174
+ if (opts.updateSource) {
1175
+ if (!opts.app.config.globalProperties.lunchbox.watchStopHandle) {
1176
+ // request next frame only when state changes
1177
+ opts.app.config.globalProperties.lunchbox.watchStopHandle = vue.watch(opts.updateSource, () => {
1178
+ requestUpdate(opts);
1179
+ }, {
1180
+ deep: true
1181
+ });
1182
+ }
1183
+ } else {
1184
+ // request next frame on a continuous loop
1185
+ requestUpdate(opts);
1186
+ } // prep options
1187
+
1188
+
1189
+ const {
1190
+ app,
1191
+ renderer,
1192
+ scene
1193
+ } = opts; // BEFORE RENDER
1194
+
1195
+ app.config.globalProperties.lunchbox.beforeRender.forEach(cb => {
1196
+ cb?.(opts);
1197
+ }); // RENDER
1198
+
1199
+ if (renderer && scene && opts.app.config.globalProperties.lunchbox.camera) {
1200
+ if (app.customRender) {
1201
+ app.customRender(opts);
1202
+ } else {
1203
+ renderer.render(vue.toRaw(scene), opts.app.config.globalProperties.lunchbox.camera // toRaw(camera)
1204
+ );
1205
+ }
1206
+ } // AFTER RENDER
1207
+
1208
+
1209
+ app.config.globalProperties.lunchbox.afterRender.forEach(cb => {
1210
+ cb?.(opts);
1211
+ });
1212
+ }; // before render
1213
+ // ====================
1214
+
1215
+ /** Obtain callback methods for `onBeforeRender` and `offBeforeRender`. Usually used internally by Lunchbox. */
1216
+
1217
+ const useBeforeRender = () => {
1218
+ return {
1219
+ onBeforeRender: vue.inject(onBeforeRenderKey),
1220
+ offBeforeRender: vue.inject(offBeforeRenderKey)
1221
+ };
1409
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
+ */
1228
+
1410
1229
  const onBeforeRender = (cb, index = Infinity) => {
1411
- if (index === Infinity) {
1412
- beforeRender.push(cb);
1413
- }
1414
- else {
1415
- beforeRender.splice(index, 0, cb);
1416
- }
1230
+ useBeforeRender().onBeforeRender?.(cb, index);
1417
1231
  };
1418
- const offBeforeRender = (cb) => {
1419
- if (isFinite(cb)) {
1420
- beforeRender.splice(cb, 1);
1421
- }
1422
- else {
1423
- const idx = beforeRender.findIndex((v) => v == cb);
1424
- beforeRender.splice(idx, 1);
1425
- }
1232
+ /** Remove a function from the `beforeRender` callback list. Useful for tearing down functions added
1233
+ * by `onBeforeRender`.
1234
+ */
1235
+
1236
+ const offBeforeRender = cb => {
1237
+ useBeforeRender().offBeforeRender?.(cb);
1238
+ }; // after render
1239
+ // ====================
1240
+
1241
+ /** Obtain callback methods for `onAfterRender` and `offAfterRender`. Usually used internally by Lunchbox. */
1242
+
1243
+ const useAfterRender = () => {
1244
+ return {
1245
+ onAfterRender: vue.inject(onBeforeRenderKey),
1246
+ offAfterRender: vue.inject(offBeforeRenderKey)
1247
+ };
1426
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
+ */
1254
+
1427
1255
  const onAfterRender = (cb, index = Infinity) => {
1428
- if (index === Infinity) {
1429
- afterRender.push(cb);
1430
- }
1431
- else {
1432
- afterRender.splice(index, 0, cb);
1433
- }
1256
+ useBeforeRender().onBeforeRender?.(cb, index);
1434
1257
  };
1435
- const offAfterRender = (cb) => {
1436
- if (isFinite(cb)) {
1437
- afterRender.splice(cb, 1);
1438
- }
1439
- else {
1440
- const idx = afterRender.findIndex((v) => v == cb);
1441
- afterRender.splice(idx, 1);
1442
- }
1258
+ /** Remove a function from the `afterRender` callback list. Useful for tearing down functions added
1259
+ * by `onAfterRender`.
1260
+ */
1261
+
1262
+ const offAfterRender = cb => {
1263
+ useBeforeRender().offBeforeRender?.(cb);
1443
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
+ */
1268
+
1269
+ const useCancelUpdate = () => {
1270
+ const frameId = vue.inject(frameIdKey);
1271
+ return () => {
1272
+ if (frameId !== undefined) cancelAnimationFrame(frameId);
1273
+ };
1274
+ };
1275
+ /** Cancel the current update frame. Usually used internally by Lunchbox. */
1276
+
1444
1277
  const cancelUpdate = () => {
1445
- if (frameID)
1446
- cancelAnimationFrame(frameID);
1278
+ useCancelUpdate()?.();
1447
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
+ */
1283
+
1284
+ const useCancelUpdateSource = () => {
1285
+ const cancel = vue.inject(watchStopHandleKey);
1286
+ return () => cancel?.();
1287
+ };
1288
+ /** Cancel an update source. Usually used internally by Lunchbox. */
1289
+
1448
1290
  const cancelUpdateSource = () => {
1449
- if (watchStopHandle)
1450
- watchStopHandle();
1291
+ useCancelUpdateSource()?.();
1451
1292
  };
1452
1293
 
1453
1294
  /** Update a single prop on a given node. */
1454
- function updateObjectProp({ node, key, value, }) {
1455
- // handle and return early if prop is an event
1456
- // (event list from react-three-fiber)
1457
- if (isEventKey(key)) {
1458
- return addEventListener({ node, key, value });
1459
- }
1460
- // update THREE property
1461
- // get final key
1462
- const camelKey = key.replace(/-/g, '.');
1463
- const finalKey = propertyShortcuts[camelKey] || camelKey;
1464
- // handle and return early if prop is specific to Vue/Lunchbox
1465
- if (internalLunchboxVueKeys.includes(key) ||
1466
- internalLunchboxVueKeys.includes(finalKey))
1467
- return node;
1468
- // everything else should be Three-specific, so let's cancel if this isn't a standard node
1469
- if (!isLunchboxStandardNode(node))
1470
- return node;
1471
- // parse $attached values
1472
- if (typeof value === 'string' && value.startsWith('$attached')) {
1473
- const attachedName = value.replace('$attached.', '');
1474
- value = lodash.get(node.attached, attachedName, null);
1475
- }
1476
- // save instance
1477
- const target = node.instance;
1478
- // cancel if no target
1479
- if (!target)
1480
- return node;
1481
- // burrow down until we get property to change
1482
- let liveProperty;
1483
- for (let i = 0; i < nestedPropertiesToCheck.length && !liveProperty; i++) {
1484
- const nestedProperty = nestedPropertiesToCheck[i];
1485
- const fullPath = [nestedProperty, finalKey].filter(Boolean).join('.');
1486
- liveProperty = liveProperty = lodash.get(target, fullPath);
1487
- }
1488
- // change property
1489
- // first, save as array in case we need to spread it
1490
- if (liveProperty && lodash.isNumber(value) && liveProperty.setScalar) {
1491
- // if value is a number and the property has a `setScalar` method, use that
1492
- liveProperty.setScalar(value);
1493
- }
1494
- else if (liveProperty && liveProperty.set) {
1495
- // if property has `set` method, use that (https://github.com/pmndrs/react-three-fiber/blob/master/markdown/api.md#shortcuts)
1496
- const nextValueAsArray = Array.isArray(value) ? value : [value];
1497
- target[finalKey].set(...nextValueAsArray);
1498
- }
1499
- else if (typeof liveProperty === 'function') {
1500
- // some function properties are set rather than called, so let's handle them
1501
- if (finalKey.toLowerCase() === 'onbeforerender' ||
1502
- finalKey.toLowerCase() === 'onafterrender') {
1503
- target[finalKey] = value;
1504
- }
1505
- else {
1506
- if (!Array.isArray(value)) {
1507
- 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" />');
1508
- }
1509
- // if property is a function, let's try calling it
1510
- liveProperty.bind(node.instance)(...value);
1511
- }
1512
- // pass the result to the parent
1513
- // const parent = node.parentNode
1514
- // if (parent) {
1515
- // const parentAsLunchboxNode = parent as Lunchbox.Node
1516
- // parentAsLunchboxNode.attached[finalKey] = result
1517
- // ; (parentAsLunchboxNode.instance as any)[finalKey] = result
1518
- // }
1519
- }
1520
- else if (lodash.get(target, finalKey, undefined) !== undefined) {
1521
- // blank strings evaluate to `true`
1522
- // <mesh castShadow receiveShadow /> will work the same as
1523
- // <mesh :castShadow="true" :receiveShadow="true" />
1524
- lodash.set(target, finalKey, value === '' ? true : value);
1525
- }
1526
- else {
1527
- // if you see this error in production, you might need to add `finalKey`
1528
- // to `internalLunchboxVueKeys` below
1529
- console.log(`No property ${finalKey} found on ${target}`);
1530
- }
1531
- // mark that we need to update if needed
1532
- const targetTypeRaw = target?.texture?.type || target?.type;
1533
- if (typeof targetTypeRaw === 'string') {
1534
- const targetType = targetTypeRaw.toLowerCase();
1535
- switch (true) {
1536
- case targetType.includes('material'):
1537
- target.needsUpdate = true;
1538
- break;
1539
- case targetType.includes('camera') &&
1540
- target.updateProjectionMatrix:
1541
- target.updateProjectionMatrix();
1542
- break;
1543
- }
1295
+
1296
+ function updateObjectProp({
1297
+ node,
1298
+ key,
1299
+ interactables,
1300
+ value
1301
+ }) {
1302
+ // handle and return early if prop is an event
1303
+ // (event list from react-three-fiber)
1304
+ if (isEventKey(key)) {
1305
+ return addEventListener({
1306
+ node,
1307
+ key,
1308
+ interactables,
1309
+ value
1310
+ });
1311
+ } // update THREE property
1312
+ // get final key
1313
+
1314
+
1315
+ const camelKey = key.replace(/-/g, '.');
1316
+ const finalKey = propertyShortcuts[camelKey] || camelKey; // handle and return early if prop is specific to Vue/Lunchbox
1317
+
1318
+ if (internalLunchboxVueKeys.includes(key) || internalLunchboxVueKeys.includes(finalKey)) return node; // everything else should be Three-specific, so let's cancel if this isn't a standard node
1319
+
1320
+ if (!isLunchboxStandardNode(node)) return node; // parse $attached values
1321
+
1322
+ if (typeof value === 'string' && value.startsWith('$attached')) {
1323
+ const attachedName = value.replace('$attached.', '');
1324
+ value = lodash.get(node.attached, attachedName, null);
1325
+ } // save instance
1326
+
1327
+
1328
+ const target = node.instance; // cancel if no target
1329
+
1330
+ if (!target) return node; // burrow down until we get property to change
1331
+
1332
+ let liveProperty;
1333
+
1334
+ for (let i = 0; i < nestedPropertiesToCheck.length && !liveProperty; i++) {
1335
+ const nestedProperty = nestedPropertiesToCheck[i];
1336
+ const fullPath = [nestedProperty, finalKey].filter(Boolean).join('.');
1337
+ liveProperty = liveProperty = lodash.get(target, fullPath);
1338
+ } // change property
1339
+ // first, save as array in case we need to spread it
1340
+
1341
+
1342
+ if (liveProperty && lodash.isNumber(value) && liveProperty.setScalar) {
1343
+ // if value is a number and the property has a `setScalar` method, use that
1344
+ liveProperty.setScalar(value);
1345
+ } else if (liveProperty && liveProperty.set) {
1346
+ // if property has `set` method, use that (https://github.com/pmndrs/react-three-fiber/blob/master/markdown/api.md#shortcuts)
1347
+ const nextValueAsArray = Array.isArray(value) ? value : [value];
1348
+ target[finalKey].set(...nextValueAsArray);
1349
+ } else if (typeof liveProperty === 'function') {
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
1361
+ // const parent = node.parentNode
1362
+ // if (parent) {
1363
+ // const parentAsLunchboxNode = parent as Lunchbox.Node
1364
+ // parentAsLunchboxNode.attached[finalKey] = result
1365
+ // ; (parentAsLunchboxNode.instance as any)[finalKey] = result
1366
+ // }
1367
+
1368
+ } else if (lodash.get(target, finalKey, undefined) !== undefined) {
1369
+ // blank strings evaluate to `true`
1370
+ // <mesh castShadow receiveShadow /> will work the same as
1371
+ // <mesh :castShadow="true" :receiveShadow="true" />
1372
+ lodash.set(target, finalKey, value === '' ? true : value);
1373
+ } else {
1374
+ // if you see this error in production, you might need to add `finalKey`
1375
+ // to `internalLunchboxVueKeys` below
1376
+ console.log(`No property ${finalKey} found on ${target}`);
1377
+ } // mark that we need to update if needed
1378
+
1379
+
1380
+ const targetTypeRaw = target?.texture?.type || target?.type;
1381
+
1382
+ if (typeof targetTypeRaw === 'string') {
1383
+ const targetType = targetTypeRaw.toLowerCase();
1384
+
1385
+ switch (true) {
1386
+ case targetType.includes('material'):
1387
+ target.needsUpdate = true;
1388
+ break;
1389
+
1390
+ case targetType.includes('camera') && target.updateProjectionMatrix:
1391
+ target.updateProjectionMatrix();
1392
+ break;
1544
1393
  }
1545
- return node;
1394
+ }
1395
+
1396
+ return node;
1546
1397
  }
1547
1398
  const propertyShortcuts = {
1548
- x: 'position.x',
1549
- y: 'position.y',
1550
- z: 'position.z',
1399
+ x: 'position.x',
1400
+ y: 'position.y',
1401
+ z: 'position.z'
1551
1402
  };
1552
1403
  const nestedPropertiesToCheck = ['', 'parameters'];
1553
1404
  /** props that Lunchbox intercepts and prevents passing to created instances */
1554
- const internalLunchboxVueKeys = [
1555
- 'args',
1556
- 'attach',
1557
- 'attachArray',
1558
- 'is.default',
1559
- 'isDefault',
1560
- 'key',
1561
- 'onAdded',
1562
- // 'onReady',
1563
- 'ref',
1564
- 'src',
1565
- ];
1405
+
1406
+ const internalLunchboxVueKeys = ['args', 'attach', 'attachArray', 'is.default', 'isDefault', 'key', 'onAdded', // 'onReady',
1407
+ 'ref', 'src'];
1566
1408
 
1567
1409
  const autoAttach = ['geometry', 'material'];
1568
1410
  const createElement = (type, isSVG, isCustomizedBuiltin, vnodeProps) => {
1569
- const options = { type };
1570
- if (vnodeProps) {
1571
- options.props = vnodeProps;
1572
- }
1573
- // handle dom node
1574
- const isDomNode = isLunchboxDomComponent(type);
1575
- if (isDomNode) {
1576
- const node = createDomNode(options);
1577
- return node;
1578
- }
1579
- // handle standard node
1580
- const node = createNode(options);
1581
- // autoattach
1582
- autoAttach.forEach((key) => {
1583
- if (type.toLowerCase().endsWith(key)) {
1584
- node.props.attach = key;
1585
- }
1586
- });
1587
- // TODO: array autoattach
1411
+ const options = {
1412
+ type,
1413
+ props: vnodeProps
1414
+ }; // handle dom node
1415
+
1416
+ const isDomNode = isLunchboxDomComponent(options);
1417
+
1418
+ if (isDomNode) {
1419
+ const node = createDomNode(options);
1588
1420
  return node;
1421
+ } // handle standard node
1422
+
1423
+
1424
+ const node = createNode(options); // autoattach
1425
+
1426
+ autoAttach.forEach(key => {
1427
+ if (type.toLowerCase().endsWith(key)) {
1428
+ node.props.attach = key;
1429
+ }
1430
+ }); // TODO: array autoattach
1431
+
1432
+ return node;
1589
1433
  };
1590
1434
 
1591
1435
  const insert = (child, parent, anchor) => {
1592
- // add to parent tree node if we have one
1593
- let effectiveParent = parent ?? ensureRootNode();
1594
- effectiveParent.insertBefore(child, anchor);
1595
- // handle comment & text nodes
1596
- if (child.metaType === 'commentMeta' || child.metaType === 'textMeta') {
1597
- return;
1436
+ if (!parent) {
1437
+ throw new Error('missing parent');
1438
+ } // add to parent tree node if we have one
1439
+ // let effectiveParent = parent ?? ensureRootNode()
1440
+
1441
+
1442
+ parent.insertBefore(child, anchor); // handle comment & text nodes
1443
+
1444
+ if (child.metaType === 'commentMeta' || child.metaType === 'textMeta') {
1445
+ return;
1446
+ } // handle dom element
1447
+
1448
+
1449
+ if (isLunchboxDomComponent(child)) {
1450
+ if (isLunchboxDomComponent(parent) || isLunchboxRootNode(parent)) {
1451
+ parent.domElement.appendChild(child.domElement);
1598
1452
  }
1599
- // handle dom element
1600
- if (isLunchboxDomComponent(child)) {
1601
- if (isLunchboxDomComponent(parent) || isLunchboxRootNode(parent)) {
1602
- parent.domElement.appendChild(child.domElement);
1453
+ } // handle standard nodes
1454
+
1455
+
1456
+ if (isLunchboxStandardNode(child)) {
1457
+ // let effectiveParent = parent
1458
+ let effectiveParentNodeType = parent.metaType;
1459
+
1460
+ if (effectiveParentNodeType === 'textMeta' || effectiveParentNodeType === 'commentMeta') {
1461
+ const path = parent.getPath();
1462
+
1463
+ for (let i = path.length - 1; i >= 0; i--) {
1464
+ if (path[i].metaType !== 'textMeta' && path[i].metaType !== 'commentMeta') {
1465
+ parent = path[i];
1466
+ break;
1603
1467
  }
1468
+ }
1604
1469
  }
1605
- // handle standard nodes
1606
- if (isLunchboxStandardNode(child)) {
1607
- // let effectiveParent = parent
1608
- let effectiveParentNodeType = effectiveParent.metaType;
1609
- if (effectiveParentNodeType === 'textMeta' ||
1610
- effectiveParentNodeType === 'commentMeta') {
1611
- const path = effectiveParent.getPath();
1612
- for (let i = path.length - 1; i >= 0; i--) {
1613
- if (path[i].metaType !== 'textMeta' &&
1614
- path[i].metaType !== 'commentMeta') {
1615
- effectiveParent = path[i];
1616
- break;
1617
- }
1618
- }
1619
- }
1620
- // add to scene if parent is the wrapper node
1621
- if (child.metaType === 'standardMeta' &&
1622
- child.type !== 'scene' &&
1623
- isLunchboxRootNode(effectiveParent)) {
1624
- // ensure scene exists
1625
- const sceneNode = ensuredScene.value;
1626
- if (sceneNode.instance && child) {
1627
- sceneNode.addChild(child);
1628
- }
1629
- if (child.instance &&
1630
- child.instance.isObject3D &&
1631
- sceneNode.instance) {
1632
- if (sceneNode !== child) {
1633
- sceneNode.instance.add(child.instance);
1634
- }
1635
- }
1636
- }
1637
- // add to hierarchy otherwise
1638
- else if (isLunchboxStandardNode(child) &&
1639
- child.instance?.isObject3D &&
1640
- isLunchboxStandardNode(effectiveParent) &&
1641
- effectiveParent.instance?.isObject3D) {
1642
- effectiveParent.instance?.add?.(child.instance);
1643
- }
1644
- // add attached props
1645
- if (child?.props?.attach &&
1646
- isLunchboxStandardNode(parent) &&
1647
- parent?.instance) {
1648
- // if this element is a loader and the `src` attribute is being used,
1649
- // let's assume we want to create the loader and run `load`
1650
- const isUsingLoaderSugar = child.type?.toLowerCase().endsWith('loader') &&
1651
- child.props.src &&
1652
- (child.props.attach || child.props.attachArray);
1653
- // run special loader behavior
1654
- if (isUsingLoaderSugar) {
1655
- runLoader(child, parent);
1656
- }
1657
- else {
1658
- // update attached normally
1659
- attachToParentInstance(child, parent, child.props.attach);
1660
- }
1661
- }
1662
- // fire onAdded event
1663
- if (child.props?.onAdded) {
1664
- child.props.onAdded({
1665
- instance: child.instance,
1666
- });
1667
- }
1470
+
1471
+ if (isLunchboxStandardNode(child) && child.instance?.isObject3D && isLunchboxStandardNode(parent) && parent.instance?.isObject3D) {
1472
+ parent.instance?.add?.(child.instance);
1473
+ } // add attached props
1474
+
1475
+
1476
+ if (child?.props?.attach && isLunchboxStandardNode(parent) && parent?.instance) {
1477
+ // if this element is a loader and the `src` attribute is being used,
1478
+ // let's assume we want to create the loader and run `load`
1479
+ const isUsingLoaderSugar = child.type?.toLowerCase().endsWith('loader') && child.props.src && (child.props.attach || child.props.attachArray); // run special loader behavior
1480
+
1481
+ if (isUsingLoaderSugar) {
1482
+ runLoader(child, parent);
1483
+ } else {
1484
+ // update attached normally
1485
+ attachToParentInstance(child, parent, child.props.attach);
1486
+ }
1487
+ } // fire onAdded event
1488
+
1489
+
1490
+ if (child.props?.onAdded) {
1491
+ child.props.onAdded({
1492
+ instance: child.instance
1493
+ });
1668
1494
  }
1495
+ }
1669
1496
  };
1497
+
1670
1498
  function runLoader(child, parent) {
1671
- const loader = child.instance;
1672
- // ensure parent has attached spaces ready
1673
- parent.attached = parent.attached || {};
1674
- parent.attachedArray = parent.attachedArray || {};
1675
- // this should never be true, but just in case
1676
- if (!child.props.attach)
1677
- return;
1678
- if (child.type?.toLowerCase() === 'textureloader') {
1679
- // if this is a texture loader, immediately pass
1680
- // load function to parent attachment
1681
- const textureLoader = loader;
1682
- const inProgressTexture = textureLoader.load(child.props.src);
1683
- attachToParentInstance(child, parent, child.props.attach, inProgressTexture);
1684
- }
1685
- else {
1686
- // use a standard callback-based loader
1687
- loader.load(child.props.src, (loadedData) => {
1688
- attachToParentInstance(child, parent, child.props.attach, loadedData);
1689
- }, null, (err) => {
1690
- throw new Error(err);
1691
- });
1692
- }
1499
+ const loader = child.instance; // ensure parent has attached spaces ready
1500
+
1501
+ parent.attached = parent.attached || {};
1502
+ parent.attachedArray = parent.attachedArray || {}; // this should never be true, but just in case
1503
+
1504
+ if (!child.props.attach) return;
1505
+
1506
+ if (child.type?.toLowerCase() === 'textureloader') {
1507
+ // if this is a texture loader, immediately pass
1508
+ // load function to parent attachment
1509
+ const textureLoader = loader;
1510
+ const inProgressTexture = textureLoader.load(child.props.src);
1511
+ attachToParentInstance(child, parent, child.props.attach, inProgressTexture);
1512
+ } else {
1513
+ // use a standard callback-based loader
1514
+ loader.load(child.props.src, loadedData => {
1515
+ attachToParentInstance(child, parent, child.props.attach, loadedData);
1516
+ }, null, err => {
1517
+ throw new Error(err);
1518
+ });
1519
+ }
1693
1520
  }
1521
+
1694
1522
  function attachToParentInstance(child, parent, key, value) {
1695
- const finalValueToAttach = value ?? child.instance;
1696
- const parentInstanceAsAny = parent.instance;
1697
- if (child.props.attach === key) {
1698
- parent.attached = {
1699
- [key]: finalValueToAttach,
1700
- ...(parent.attached || {}),
1701
- };
1702
- parentInstanceAsAny[key] = value ?? child.instance;
1703
- }
1704
- if (child.props.attachArray === key) {
1705
- if (!parent.attachedArray[child.props.attachArray]) {
1706
- parent.attachedArray[child.props.attachArray] = [];
1707
- }
1708
- parent.attachedArray[child.props.attachArray].push(finalValueToAttach);
1709
- // TODO: implement auto-attaching array
1710
- parentInstanceAsAny[key] = [parentInstanceAsAny[key]];
1523
+ const finalValueToAttach = value ?? child.instance;
1524
+ const parentInstanceAsAny = parent.instance;
1525
+
1526
+ if (child.props.attach === key) {
1527
+ parent.attached = {
1528
+ [key]: finalValueToAttach,
1529
+ ...(parent.attached || {})
1530
+ };
1531
+ parentInstanceAsAny[key] = value ?? child.instance;
1532
+ }
1533
+
1534
+ if (child.props.attachArray === key) {
1535
+ if (!parent.attachedArray[child.props.attachArray]) {
1536
+ parent.attachedArray[child.props.attachArray] = [];
1711
1537
  }
1538
+
1539
+ parent.attachedArray[child.props.attachArray].push(finalValueToAttach); // TODO: implement auto-attaching array
1540
+
1541
+ parentInstanceAsAny[key] = [parentInstanceAsAny[key]];
1542
+ }
1712
1543
  }
1713
1544
 
1714
- const remove = (node) => {
1715
- if (!node)
1716
- return;
1717
- const overrideKeys = Object.keys(overrides);
1718
- // prep subtree
1719
- const subtree = [];
1720
- node.walk((descendant) => {
1721
- subtree.push(descendant);
1722
- return true;
1723
- });
1724
- // clean up subtree
1725
- subtree.forEach((n) => {
1726
- const overrideKey = overrideKeys.find((key) => overrides[key]?.uuid === n.uuid);
1727
- // if this node is an override, remove it from the overrides list
1728
- if (overrideKey) {
1729
- overrides[overrideKey] = null;
1730
- }
1731
- if (isLunchboxStandardNode(n)) {
1732
- // try to remove three object
1733
- n.instance?.removeFromParent?.();
1734
- // try to dispose three object
1735
- const dispose =
1736
- // calling `dispose` on a scene triggers an error,
1737
- // so let's ignore if this node is a scene
1738
- n.type !== 'scene' &&
1739
- n.instance?.dispose;
1740
- if (dispose)
1741
- dispose.bind(n.instance)();
1742
- n.instance = null;
1743
- }
1744
- // drop tree node
1745
- n.drop();
1746
- // remove Lunchbox node from main list
1747
- const idx = allNodes.findIndex((v) => v.uuid === n.uuid);
1748
- if (idx !== -1) {
1749
- allNodes.splice(idx, 1);
1750
- }
1751
- });
1545
+ const remove = node => {
1546
+ if (!node) return; // prep subtree
1547
+
1548
+ const subtree = [];
1549
+ node.walk(descendant => {
1550
+ subtree.push(descendant);
1551
+ return true;
1552
+ }); // clean up subtree
1553
+
1554
+ subtree.forEach(n => {
1555
+ if (isLunchboxStandardNode(n)) {
1556
+ // try to remove three object
1557
+ n.instance?.removeFromParent?.(); // try to dispose three object
1558
+
1559
+ const dispose = // calling `dispose` on a scene triggers an error,
1560
+ // so let's ignore if this node is a scene
1561
+ n.type !== 'scene' && n.instance?.dispose;
1562
+ if (dispose) dispose.bind(n.instance)();
1563
+ n.instance = null;
1564
+ } // drop tree node
1565
+
1566
+
1567
+ n.drop();
1568
+ });
1752
1569
  };
1753
1570
 
1754
1571
  /*
1755
1572
  Elements are `create`d from the outside in, then `insert`ed from the inside out.
1756
1573
  */
1757
- const nodeOps = {
1574
+
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
+ // ====================
1583
+
1584
+ const nodeOps = {
1758
1585
  createElement,
1586
+
1759
1587
  createText(text) {
1760
- return createTextNode({ text });
1588
+ return createTextNode({
1589
+ text
1590
+ });
1761
1591
  },
1592
+
1762
1593
  createComment(text) {
1763
- return createCommentNode({ text });
1594
+ return createCommentNode({
1595
+ text
1596
+ });
1764
1597
  },
1598
+
1765
1599
  insert,
1600
+
1766
1601
  nextSibling(node) {
1767
- const result = node.nextSibling;
1768
- // console.log('found', result)
1769
- if (!result)
1770
- return null;
1771
- return result;
1602
+ const result = node.nextSibling;
1603
+ if (!result) return null;
1604
+ return result;
1772
1605
  },
1606
+
1773
1607
  parentNode(node) {
1774
- const result = node.parentNode;
1775
- if (!result)
1776
- return null;
1777
- return result;
1608
+ const result = node.parentNode;
1609
+ if (!result) return null;
1610
+ return result;
1778
1611
  },
1612
+
1779
1613
  patchProp(node, key, prevValue, nextValue) {
1780
- if (isLunchboxDomComponent(node)) {
1781
- // handle DOM node
1782
- if (key === 'style') {
1783
- // special handling for style
1784
- Object.keys(nextValue).forEach((k) => {
1785
- node.domElement.style[k] = nextValue[k];
1786
- });
1787
- }
1788
- else {
1789
- node.domElement.setAttribute(key, nextValue);
1790
- }
1791
- return;
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);
1792
1623
  }
1793
- // ignore if root node, or Lunchbox internal prop
1794
- if (isLunchboxRootNode(node) || key.startsWith('$')) {
1795
- return;
1796
- }
1797
- // otherwise, update prop
1798
- updateObjectProp({ node: node, key, value: nextValue });
1624
+
1625
+ return;
1626
+ } // ignore if root node, or Lunchbox internal prop
1627
+
1628
+
1629
+ if (isLunchboxRootNode(node) || key.startsWith('$')) {
1630
+ return;
1631
+ } // otherwise, update prop
1632
+
1633
+
1634
+ updateObjectProp({
1635
+ node: node,
1636
+ key,
1637
+ interactables,
1638
+ value: nextValue
1639
+ });
1799
1640
  },
1641
+
1800
1642
  remove,
1801
- setElementText() {
1802
- // noop
1803
- },
1804
- setText() {
1805
- // noop
1643
+
1644
+ setElementText() {// noop
1806
1645
  },
1807
- };
1808
1646
 
1809
- /** Useful globals. */
1810
- const globals = {
1811
- dpr: vue.ref(1),
1812
- inputActive,
1813
- mousePos,
1647
+ setText() {// noop
1648
+ }
1649
+
1650
+ };
1651
+ return {
1652
+ nodeOps,
1653
+ interactables
1654
+ };
1814
1655
  };
1815
- /** The current camera. Often easier to use `useCamera` instead of this. */
1816
- const camera = vue.computed(() => ensuredCamera.value?.instance ?? null);
1656
+
1657
+ /** The current camera as a computed value. */
1658
+
1659
+ const useCamera = () => vue.inject(appCameraKey);
1817
1660
  /** Run a function using the current camera when it's present. */
1818
- function useCamera(callback) {
1819
- return vue.watch(camera, (newVal) => {
1820
- if (!newVal)
1821
- return;
1822
- callback(newVal);
1823
- }, { immediate: true });
1824
- }
1825
- /** The current renderer. Often easier to use `useRenderer` instead of this. */
1826
- const renderer = vue.computed(() => ensureRenderer.value?.instance ?? null);
1661
+
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. */
1673
+
1674
+ const useRenderer = () => vue.inject(appRenderersKey);
1827
1675
  /** Run a function using the current renderer when it's present. */
1828
- function useRenderer(callback) {
1829
- return vue.watch(renderer, (newVal) => {
1830
- if (!newVal)
1831
- return;
1832
- callback(newVal);
1833
- }, { immediate: true });
1834
- }
1835
- /** The current scene. Often easier to use `useScene` instead of this. */
1836
- const scene = vue.computed(() => ensuredScene.value.instance);
1676
+
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. */
1688
+
1689
+ const useScene = () => vue.inject(appSceneKey);
1837
1690
  /** Run a function using the current scene when it's present. */
1838
- function useScene(callback) {
1839
- return vue.watch(scene, (newVal) => {
1840
- if (!newVal)
1841
- return;
1842
- callback(newVal);
1843
- }, { immediate: true });
1844
- }
1845
- // CUSTOM RENDER SUPPORT
1691
+
1692
+ const onSceneReady = cb => {
1693
+ const stopWatch = vue.watch(useScene(), newVal => {
1694
+ if (newVal) {
1695
+ cb(newVal);
1696
+ stopWatch();
1697
+ }
1698
+ }, {
1699
+ immediate: true
1700
+ });
1701
+ }; // CUSTOM RENDER SUPPORT
1846
1702
  // ====================
1847
- let app = null;
1848
- let queuedCustomRenderFunction = null;
1703
+
1849
1704
  /** Set a custom render function, overriding the Lunchbox app's default render function.
1850
1705
  * Changing this requires the user to manually render their scene.
1706
+ *
1707
+ * Invokes immediately - use `useCustomRender().setCustomRender`
1708
+ * if you need to call somewhere outside of `setup`.
1851
1709
  */
1852
- const setCustomRender = (render) => {
1853
- if (app)
1854
- app.setCustomRender(render);
1855
- else
1856
- queuedCustomRenderFunction = render;
1710
+
1711
+ const setCustomRender = render => {
1712
+ useCustomRender()?.setCustomRender?.(render);
1857
1713
  };
1858
- /** Clear the active app's custom render function. */
1714
+ /** Clear the active app's custom render function.
1715
+ *
1716
+ * Invokes immediately - use `useCustomRender().clearCustomRender`
1717
+ * if you need to call somewhere outside of `setup`.
1718
+ */
1719
+
1859
1720
  const clearCustomRender = () => {
1860
- if (app)
1861
- app.clearCustomRender();
1862
- else
1863
- queuedCustomRenderFunction = null;
1721
+ useCustomRender()?.clearCustomRender?.();
1864
1722
  };
1865
- // CREATE APP
1723
+ /** Provides `setCustomRender` and `clearCustomRender` functions to be called in a non-`setup` context. */
1724
+
1725
+ const useCustomRender = () => {
1726
+ return {
1727
+ /** Set a custom render function, overriding the Lunchbox app's default render function.
1728
+ * Changing this requires the user to manually render their scene. */
1729
+ setCustomRender: vue.inject(setCustomRenderKey),
1730
+
1731
+ /** Clear the active app's custom render function. */
1732
+ clearCustomRender: vue.inject(clearCustomRenderKey)
1733
+ };
1734
+ };
1735
+ /** Use app-level globals. */
1736
+
1737
+ const useGlobals = () => vue.inject(globalsInjectionKey);
1738
+ /** Construct a function to update your app-level globals.
1739
+ *
1740
+ * ```js
1741
+ * // in setup():
1742
+ * const updateGlobals = useUpdateGlobals()
1743
+ *
1744
+ * // ...later, to update the device pixel resolution...
1745
+ * updateGlobals({ dpr: 2 })
1746
+ * ```
1747
+ */
1748
+
1749
+ const useUpdateGlobals = () => vue.inject(updateGlobalsInjectionKey);
1750
+ /** Update app-level globals.
1751
+ *
1752
+ * Invokes immediately - use `useUpdateGlobals`
1753
+ * if you need to call somewhere outside of `setup`.
1754
+ */
1755
+
1756
+ const updateGlobals = newValue => {
1757
+ useUpdateGlobals()?.(newValue);
1758
+ };
1759
+ /** Use the current Lunchbox app. Usually used internally by Lunchbox. */
1760
+
1761
+ const useApp = () => vue.inject(appKey);
1762
+ /** Obtain a list of the start callback functions. Usually used internally by Lunchbox. */
1763
+
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. */
1767
+
1768
+ const onStart = (cb, index = Infinity) => {
1769
+ const callbacks = useStartCallbacks();
1770
+
1771
+ if (index === Infinity) {
1772
+ callbacks?.push(cb);
1773
+ } else {
1774
+ callbacks?.splice(index, 0, cb);
1775
+ }
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
1866
1780
  // ====================
1867
- const createApp = (root) => {
1868
- app = vue.createRenderer(nodeOps).createApp(root);
1869
- // register all components
1870
- Object.keys(components).forEach((key) => {
1871
- app?.component(key, components[key]);
1872
- });
1873
- // update mount function to match Lunchbox.Node
1874
- const { mount } = app;
1875
- app.mount = (root, ...args) => {
1876
- // find DOM element to use as app root
1877
- const domElement = (typeof root === 'string' ? document.querySelector(root) : root);
1878
- // create or find root node
1879
- const rootNode = ensureRootNode({
1880
- domElement,
1881
- isLunchboxRootNode: true,
1882
- name: 'root',
1883
- metaType: 'rootMeta',
1884
- type: 'root',
1885
- uuid: rootUuid,
1886
- });
1887
- app.rootNode = rootNode;
1888
- const mounted = mount(rootNode, ...args);
1889
- return mounted;
1890
- };
1891
- // embed .extend function
1892
- app.extend = (targets) => {
1893
- extend({ app: app, ...targets });
1894
- return app;
1895
- };
1896
- // prep for custom render support
1897
- app.setCustomRender = (newRender) => {
1898
- app.customRender = newRender;
1899
- };
1900
- // add queued custom render if we have one
1901
- if (queuedCustomRenderFunction) {
1902
- app.setCustomRender(queuedCustomRenderFunction);
1903
- queuedCustomRenderFunction = null;
1781
+
1782
+ const createApp = root => {
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
1791
+ // ====================
1792
+
1793
+ Object.keys(components).forEach(key => {
1794
+ app?.component(key, components[key]);
1795
+ }); // provide custom renderer functions
1796
+ // ====================
1797
+
1798
+ app.provide(setCustomRenderKey, render => {
1799
+ app.setCustomRender(render);
1800
+ });
1801
+ app.provide(clearCustomRenderKey, () => {
1802
+ app.clearCustomRender();
1803
+ }); // before render
1804
+ // ====================
1805
+
1806
+ const beforeRender = [];
1807
+ app.provide(beforeRenderKey, beforeRender);
1808
+ app.provide(onBeforeRenderKey, (cb, index = Infinity) => {
1809
+ if (index === Infinity) {
1810
+ beforeRender.push(cb);
1811
+ } else {
1812
+ beforeRender.splice(index, 0, cb);
1904
1813
  }
1905
- // add custom render removal
1906
- app.clearCustomRender = () => {
1907
- app.customRender = null;
1908
- };
1909
- // done
1814
+ });
1815
+ app.provide(offBeforeRenderKey, cb => {
1816
+ if (isFinite(cb)) {
1817
+ beforeRender.splice(cb, 1);
1818
+ } else {
1819
+ const idx = beforeRender.findIndex(v => v == cb);
1820
+
1821
+ if (idx !== -1) {
1822
+ beforeRender.splice(idx, 1);
1823
+ }
1824
+ }
1825
+ }); // after render
1826
+ // ====================
1827
+
1828
+ const afterRender = [];
1829
+ app.provide(afterRenderKey, afterRender);
1830
+ app.provide(onAfterRenderKey, (cb, index = Infinity) => {
1831
+ if (index === Infinity) {
1832
+ afterRender.push(cb);
1833
+ } else {
1834
+ afterRender.splice(index, 0, cb);
1835
+ }
1836
+ });
1837
+ app.provide(offAfterRenderKey, cb => {
1838
+ if (isFinite(cb)) {
1839
+ afterRender.splice(cb, 1);
1840
+ } else {
1841
+ const idx = afterRender.findIndex(v => v == cb);
1842
+
1843
+ if (idx !== -1) {
1844
+ afterRender.splice(idx, 1);
1845
+ }
1846
+ }
1847
+ }); // save app-level components
1848
+ // ====================
1849
+
1850
+ app.config.globalProperties.lunchbox = vue.reactive({
1851
+ afterRender,
1852
+ beforeRender,
1853
+ camera: null,
1854
+ dpr: 1,
1855
+ frameId: -1,
1856
+ renderer: null,
1857
+ scene: null,
1858
+ watchStopHandle: null // TODO: inputActive, mousePos
1859
+
1860
+ }); // provide app-level globals & globals update method
1861
+ // ====================
1862
+
1863
+ app.provide(globalsInjectionKey, app.config.globalProperties.lunchbox);
1864
+ app.provide(updateGlobalsInjectionKey, newGlobals => {
1865
+ Object.keys(newGlobals).forEach(key => {
1866
+ const typedKey = key; // TODO: fix
1867
+
1868
+ app.config.globalProperties.lunchbox[typedKey] = newGlobals[typedKey];
1869
+ });
1870
+ }); // frame ID (used for update functions)
1871
+ // ====================
1872
+
1873
+ app.provide(frameIdKey, app.config.globalProperties.lunchbox.frameId); // watch stop handler (used for conditional update loop)
1874
+ // ====================
1875
+
1876
+ app.provide(watchStopHandleKey, app.config.globalProperties.lunchbox.watchStopHandle); // update mount function to match Lunchbox.Node
1877
+ // ====================
1878
+
1879
+ const {
1880
+ mount
1881
+ } = app;
1882
+
1883
+ app.mount = (root, ...args) => {
1884
+ // find DOM element to use as app root
1885
+ const domElement = typeof root === 'string' ? document.querySelector(root) : root; // create or find root node
1886
+
1887
+ const rootNode = new exports.MiniDom.RendererRootNode({
1888
+ domElement,
1889
+ isLunchboxRootNode: true,
1890
+ name: 'root',
1891
+ metaType: 'rootMeta',
1892
+ type: 'root',
1893
+ uuid: 'LUNCHBOX_ROOT'
1894
+ });
1895
+ app.rootNode = rootNode;
1896
+ app.provide(appRootNodeKey, rootNode);
1897
+ const mounted = mount(rootNode, ...args);
1898
+ return mounted;
1899
+ }; // embed .extend function
1900
+ // ====================
1901
+
1902
+
1903
+ app.extend = targets => {
1904
+ extend({
1905
+ app: app,
1906
+ ...targets
1907
+ });
1910
1908
  return app;
1909
+ }; // start callback functions
1910
+ // ====================
1911
+
1912
+
1913
+ const startCallbacks = [];
1914
+ app.provide(startCallbackKey, startCallbacks); // prep for custom render support
1915
+ // ====================
1916
+
1917
+ app.setCustomRender = newRender => {
1918
+ if (app) {
1919
+ app.customRender = newRender;
1920
+ }
1921
+ }; // add custom render removal
1922
+
1923
+
1924
+ app.clearCustomRender = () => {
1925
+ if (app) {
1926
+ app.customRender = null;
1927
+ }
1928
+ }; // provide app
1929
+ // ====================
1930
+
1931
+
1932
+ app.provide(appKey, app);
1933
+ app.provide(appRenderersKey, vue.computed(() => app.config.globalProperties.lunchbox.renderer));
1934
+ app.provide(appSceneKey, vue.computed(() => app.config.globalProperties.lunchbox.scene));
1935
+ app.provide(appCameraKey, vue.computed(() => app.config.globalProperties.lunchbox.camera)); // done
1936
+
1937
+ return app;
1911
1938
  };
1912
1939
 
1913
- exports.camera = camera;
1940
+ exports.addEventListener = addEventListener;
1941
+ exports.afterRenderKey = afterRenderKey;
1942
+ exports.appCameraKey = appCameraKey;
1943
+ exports.appKey = appKey;
1944
+ exports.appRenderersKey = appRenderersKey;
1945
+ exports.appRootNodeKey = appRootNodeKey;
1946
+ exports.appSceneKey = appSceneKey;
1947
+ exports.beforeRenderKey = beforeRenderKey;
1948
+ exports.cancelUpdate = cancelUpdate;
1949
+ exports.cancelUpdateSource = cancelUpdateSource;
1914
1950
  exports.clearCustomRender = clearCustomRender;
1951
+ exports.clearCustomRenderKey = clearCustomRenderKey;
1915
1952
  exports.createApp = createApp;
1953
+ exports.createCommentNode = createCommentNode;
1954
+ exports.createDomNode = createDomNode;
1955
+ exports.createNode = createNode;
1956
+ exports.createTextNode = createTextNode;
1957
+ exports.extend = extend;
1916
1958
  exports.find = find;
1917
- exports.globals = globals;
1959
+ exports.frameIdKey = frameIdKey;
1960
+ exports.globalsInjectionKey = globalsInjectionKey;
1961
+ exports.instantiateThreeObject = instantiateThreeObject;
1962
+ exports.isMinidomNode = isMinidomNode;
1963
+ exports.lunchboxInteractables = lunchboxInteractables;
1964
+ exports.nestedPropertiesToCheck = nestedPropertiesToCheck;
1918
1965
  exports.offAfterRender = offAfterRender;
1966
+ exports.offAfterRenderKey = offAfterRenderKey;
1919
1967
  exports.offBeforeRender = offBeforeRender;
1968
+ exports.offBeforeRenderKey = offBeforeRenderKey;
1920
1969
  exports.onAfterRender = onAfterRender;
1970
+ exports.onAfterRenderKey = onAfterRenderKey;
1921
1971
  exports.onBeforeRender = onBeforeRender;
1972
+ exports.onBeforeRenderKey = onBeforeRenderKey;
1973
+ exports.onCameraReady = onCameraReady;
1974
+ exports.onRendererReady = onRendererReady;
1975
+ exports.onSceneReady = onSceneReady;
1922
1976
  exports.onStart = onStart;
1923
- exports.renderer = renderer;
1924
- exports.scene = scene;
1925
1977
  exports.setCustomRender = setCustomRender;
1978
+ exports.setCustomRenderKey = setCustomRenderKey;
1979
+ exports.startCallbackKey = startCallbackKey;
1980
+ exports.update = update;
1981
+ exports.updateGlobals = updateGlobals;
1982
+ exports.updateGlobalsInjectionKey = updateGlobalsInjectionKey;
1983
+ exports.updateObjectProp = updateObjectProp;
1984
+ exports.useAfterRender = useAfterRender;
1985
+ exports.useApp = useApp;
1986
+ exports.useBeforeRender = useBeforeRender;
1926
1987
  exports.useCamera = useCamera;
1988
+ exports.useCancelUpdate = useCancelUpdate;
1989
+ exports.useCancelUpdateSource = useCancelUpdateSource;
1990
+ exports.useCustomRender = useCustomRender;
1991
+ exports.useGlobals = useGlobals;
1992
+ exports.useLunchboxInteractables = useLunchboxInteractables;
1927
1993
  exports.useRenderer = useRenderer;
1928
1994
  exports.useScene = useScene;
1995
+ exports.useStartCallbacks = useStartCallbacks;
1996
+ exports.useUpdateGlobals = useUpdateGlobals;
1997
+ exports.watchStopHandleKey = watchStopHandleKey;
1929
1998
 
1930
1999
  Object.defineProperty(exports, '__esModule', { value: true });
1931
2000