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