lunchboxjs 0.1.4016 → 0.2.1001-beta.0

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