lunchboxjs 0.1.4016 → 0.2.1001-beta.0

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