lunchboxjs 0.1.4017 → 0.2.1001-beta.1

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