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