lunchboxjs 0.1.4018 → 0.2.1001-beta.0

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