lunchboxjs 0.1.4001 → 0.1.4005

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.
@@ -28,29 +28,39 @@
28
28
  const allNodes = [];
29
29
 
30
30
  const resizeCanvas = (width, height) => {
31
- const renderer = ensureRenderer().instance;
32
- const scene = ensureScene().instance;
31
+ const renderer = ensureRenderer.value?.instance;
32
+ const scene = ensuredScene.value.instance;
33
+ const camera = ensuredCamera.value;
33
34
  // ignore if no element
34
- if (!renderer?.domElement || !scene)
35
+ if (!renderer?.domElement || !scene || !camera)
35
36
  return;
36
37
  width = width ?? window.innerWidth;
37
38
  height = height ?? window.innerHeight;
38
39
  // update camera
39
40
  const aspect = width / height;
40
- const camera = ensureCamera();
41
41
  if (camera.type?.toLowerCase() === 'perspectivecamera') {
42
42
  const perspectiveCamera = camera.instance;
43
43
  perspectiveCamera.aspect = aspect;
44
44
  perspectiveCamera.updateProjectionMatrix();
45
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
+ }
46
56
  else {
47
- console.log('TODO: ortho camera update');
57
+ console.log('TODO: non-ortho or perspective camera');
48
58
  }
49
59
  // update canvas
50
60
  renderer.setSize(width, height);
51
61
  // render immediately so there's no flicker
52
62
  if (scene && camera.instance) {
53
- renderer.render(scene, camera.instance);
63
+ renderer.render(vue.toRaw(scene), vue.toRaw(camera.instance));
54
64
  }
55
65
  };
56
66
 
@@ -94,8 +104,13 @@
94
104
  props: {
95
105
  // These should match the Lunchbox.WrapperProps interface
96
106
  background: String,
107
+ cameraArgs: Array,
108
+ cameraLook: Array,
109
+ cameraLookAt: Array,
97
110
  cameraPosition: Array,
98
111
  dpr: Number,
112
+ ortho: Boolean,
113
+ orthographic: Boolean,
99
114
  rendererProperties: Object,
100
115
  shadow: [Boolean, Object],
101
116
  transparent: Boolean,
@@ -106,6 +121,7 @@
106
121
  const dpr = vue.ref(props.dpr ?? -1);
107
122
  const container = vue.ref();
108
123
  let renderer;
124
+ let camera;
109
125
  let scene;
110
126
  // MOUNT
111
127
  // ====================
@@ -113,40 +129,106 @@
113
129
  // canvas needs to exist
114
130
  if (!canvas.value)
115
131
  throw new Error('missing canvas');
116
- // ensure camera
117
- const camera = ensureCamera().instance;
118
- // move camera if needed
119
- if (camera && props.cameraPosition) {
120
- camera.position.set(...props.cameraPosition);
121
- }
122
132
  // RENDERER
123
133
  // ====================
124
- // build renderer args
125
- const rendererArgs = {
126
- antialias: true,
127
- canvas: canvas.value.domElement,
128
- };
129
- if (props.transparent) {
130
- rendererArgs.alpha = true;
131
- }
132
- const sugar = {
133
- shadow: props.shadow,
134
- };
135
- // ensure renderer
136
- renderer = ensureRenderer([rendererArgs], sugar);
137
- // set renderer props if needed
138
- if (props.rendererProperties) {
139
- Object.keys(props.rendererProperties).forEach((key) => {
140
- lodash.set(renderer, key, props.rendererProperties[key]);
134
+ // is there already a renderer?
135
+ // TODO: allow other renderer types
136
+ renderer = tryGetNodeWithInstanceType([
137
+ 'WebGLRenderer',
138
+ ]);
139
+ // if renderer is missing, initialize with options
140
+ if (!renderer) {
141
+ // build renderer args
142
+ const rendererArgs = {
143
+ antialias: true,
144
+ canvas: canvas.value.domElement,
145
+ };
146
+ if (props.transparent) {
147
+ rendererArgs.alpha = true;
148
+ }
149
+ // create new renderer
150
+ ensureRenderer.value = createNode({
151
+ type: 'WebGLRenderer',
152
+ uuid: fallbackRendererUuid,
153
+ props: {
154
+ args: [rendererArgs],
155
+ },
141
156
  });
157
+ // we've initialized the renderer, so anything depending on it can execute now
158
+ rendererReady.value = true;
159
+ const rendererAsWebGlRenderer = ensureRenderer;
160
+ // update render sugar
161
+ const sugar = {
162
+ shadow: props.shadow,
163
+ };
164
+ if (rendererAsWebGlRenderer.value.instance && sugar?.shadow) {
165
+ rendererAsWebGlRenderer.value.instance.shadowMap.enabled =
166
+ true;
167
+ if (typeof sugar.shadow === 'object') {
168
+ rendererAsWebGlRenderer.value.instance.shadowMap.type =
169
+ sugar.shadow.type;
170
+ }
171
+ }
172
+ // set renderer props if needed
173
+ if (props.rendererProperties) {
174
+ Object.keys(props.rendererProperties).forEach((key) => {
175
+ lodash.set(rendererAsWebGlRenderer.value, key, props.rendererProperties[key]);
176
+ });
177
+ }
178
+ // update using created renderer
179
+ renderer = rendererAsWebGlRenderer.value;
142
180
  }
143
- if (renderer.uuid !== fallbackRendererUuid) {
181
+ else {
144
182
  useFallbackRenderer.value = false;
183
+ // the user has initialized the renderer, so anything depending
184
+ // on the renderer can execute
185
+ rendererReady.value = true;
145
186
  return;
146
187
  }
188
+ // CAMERA
189
+ // ====================
190
+ // is there already a camera?
191
+ camera = tryGetNodeWithInstanceType([
192
+ 'PerspectiveCamera',
193
+ 'OrthographicCamera',
194
+ ]);
195
+ // if not, let's create one
196
+ if (!camera) {
197
+ // create ortho camera
198
+ if (props.ortho || props.orthographic) {
199
+ ensuredCamera.value = createNode({
200
+ props: { args: props.cameraArgs ?? [] },
201
+ type: 'OrthographicCamera',
202
+ uuid: fallbackCameraUuid,
203
+ });
204
+ }
205
+ else {
206
+ ensuredCamera.value = createNode({
207
+ props: {
208
+ args: props.cameraArgs ?? [45, 0.5625, 1, 1000],
209
+ },
210
+ type: 'PerspectiveCamera',
211
+ uuid: fallbackCameraUuid,
212
+ });
213
+ }
214
+ cameraReady.value = true;
215
+ camera = ensuredCamera.value;
216
+ }
217
+ else {
218
+ cameraReady.value = true;
219
+ }
220
+ // move camera if needed
221
+ if (camera && props.cameraPosition) {
222
+ camera.instance?.position.set(...props.cameraPosition);
223
+ }
224
+ // angle camera if needed
225
+ if (camera && (props.cameraLookAt || props.cameraLook)) {
226
+ const source = (props.cameraLookAt || props.cameraLook);
227
+ camera.instance?.lookAt(...source);
228
+ }
147
229
  // SCENE
148
230
  // ====================
149
- scene = ensureScene();
231
+ scene = ensuredScene.value;
150
232
  // set background color
151
233
  if (scene && scene.instance && props.background) {
152
234
  scene.instance.background = new THREE.Color(props.background);
@@ -156,7 +238,7 @@
156
238
  if (dpr.value === -1) {
157
239
  dpr.value = window.devicePixelRatio;
158
240
  }
159
- if (renderer.instance) {
241
+ if (renderer?.instance) {
160
242
  renderer.instance.setPixelRatio(dpr.value);
161
243
  globals.dpr.value = dpr.value;
162
244
  // prep canvas (sizing, observe, unmount, etc)
@@ -167,9 +249,10 @@
167
249
  }
168
250
  // KICK UPDATE
169
251
  // ====================
252
+ // console.log(scene)
170
253
  update({
171
254
  app: vue.getCurrentInstance().appContext.app,
172
- camera,
255
+ camera: camera.instance,
173
256
  renderer: renderer.instance,
174
257
  scene: scene.instance,
175
258
  });
@@ -199,21 +282,6 @@
199
282
  },
200
283
  };
201
284
 
202
- const catalogue = {};
203
-
204
- const lunchboxDomComponentNames = [
205
- 'canvas',
206
- 'div',
207
- 'LunchboxWrapper',
208
- ];
209
- // component creation utility
210
- const createComponent$1 = (tag) => vue.defineComponent({
211
- inheritAttrs: false,
212
- name: tag,
213
- setup(props, context) {
214
- return () => vue.h(tag, context.attrs, context.slots?.default?.() || []);
215
- },
216
- });
217
285
  // list of all components to register out of the box
218
286
  const autoGeneratedComponents = [
219
287
  // ThreeJS basics
@@ -326,21 +394,11 @@
326
394
  'arrayCamera',
327
395
  // renderers
328
396
  'webGLRenderer',
329
- ].map(createComponent$1).reduce((acc, curr) => {
330
- acc[curr.name] = curr;
331
- return acc;
332
- });
333
- const components = {
334
- ...autoGeneratedComponents,
335
- 'Lunchbox': LunchboxWrapper,
336
- // Gltf,
337
- };
338
- // console.log(components, Gltf)
339
- /*
340
- // List copied from r3f
341
- // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
397
+ /*
398
+ // List copied from r3f:
399
+ // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
342
400
 
343
- // NOT IMPLEMENTED:
401
+ // NOT IMPLEMENTED (can be added via Extend - docs.lunchboxjs.com/components/extend/):
344
402
  audioListener: AudioListenerProps
345
403
  positionalAudio: PositionalAudioProps
346
404
 
@@ -388,6 +446,27 @@
388
446
  fogExp2: FogExp2Props
389
447
  shape: ShapeProps
390
448
  */
449
+ ];
450
+
451
+ const catalogue = {};
452
+
453
+ const lunchboxDomComponentNames = ['canvas', 'div', 'LunchboxWrapper'];
454
+ // component creation utility
455
+ const createComponent$1 = (tag) => vue.defineComponent({
456
+ inheritAttrs: false,
457
+ name: tag,
458
+ setup(props, context) {
459
+ return () => vue.h(tag, context.attrs, context.slots?.default?.() || []);
460
+ },
461
+ });
462
+ autoGeneratedComponents.map(createComponent$1).reduce((acc, curr) => {
463
+ acc[curr.name] = curr;
464
+ return acc;
465
+ });
466
+ const components = {
467
+ ...autoGeneratedComponents,
468
+ Lunchbox: LunchboxWrapper,
469
+ };
391
470
 
392
471
  function find(target) {
393
472
  target = vue.isRef(target) ? target.value : target;
@@ -444,7 +523,7 @@
444
523
  /** Create a new Lunchbox comment node. */
445
524
  function createCommentNode(options = {}) {
446
525
  const defaults = {
447
- text: options.text ?? ''
526
+ text: options.text ?? '',
448
527
  };
449
528
  return new MiniDom.RendererCommentNode({
450
529
  ...defaults,
@@ -468,7 +547,7 @@
468
547
  /** Create a new Lunchbox text node. */
469
548
  function createTextNode(options = {}) {
470
549
  const defaults = {
471
- text: options.text ?? ''
550
+ text: options.text ?? '',
472
551
  };
473
552
  return new MiniDom.RendererTextNode({
474
553
  ...options,
@@ -481,7 +560,7 @@
481
560
  const defaults = {
482
561
  attached: options.attached ?? [],
483
562
  attachedArray: options.attachedArray ?? {},
484
- instance: options.instance ?? null
563
+ instance: options.instance ?? null,
485
564
  };
486
565
  const node = new MiniDom.RendererStandardNode({
487
566
  ...options,
@@ -489,14 +568,25 @@
489
568
  metaType: 'standardMeta',
490
569
  });
491
570
  if (node.type && !isLunchboxRootNode(node) && !node.instance) {
571
+ // if (node.type.includes('Camera')) {
572
+ // console.log(node.type, {
573
+ // ...node.props,
574
+ // ...props,
575
+ // })
576
+ // console.trace()
577
+ // }
492
578
  node.instance = instantiateThreeObject({
493
579
  ...node,
494
580
  props: {
495
581
  ...node.props,
496
582
  ...props,
497
- }
583
+ },
498
584
  });
499
585
  }
586
+ if (node.type === 'scene') {
587
+ // manually set scene override
588
+ ensuredScene.value = node;
589
+ }
500
590
  return node;
501
591
  }
502
592
 
@@ -527,7 +617,9 @@
527
617
  node.eventListeners[key].push(value);
528
618
  // if we need it, let's get/create the main raycaster
529
619
  if (interactionsRequiringRaycaster.includes(key)) {
530
- ensureRaycaster();
620
+ // we're not using `v` here, we're just making sure the raycaster has been created
621
+ // TODO: is this necessary?
622
+ ensuredRaycaster.value;
531
623
  if (node.instance && !interactables.includes(node)) {
532
624
  addInteractable(node);
533
625
  node.eventListenerRemoveFunctions[key].push(() => removeInteractable(node));
@@ -585,7 +677,7 @@
585
677
  return;
586
678
  // add mouse events once renderer is ready
587
679
  let stopWatcher = null;
588
- stopWatcher = vue.watch(() => createdRenderer.value, (renderer) => {
680
+ stopWatcher = vue.watch(() => ensureRenderer.value, (renderer) => {
589
681
  // make sure renderer exists
590
682
  if (!renderer?.instance)
591
683
  return;
@@ -626,8 +718,8 @@
626
718
  let currentIntersections = [];
627
719
  const autoRaycasterBeforeRender = () => {
628
720
  // setup
629
- const raycaster = createdRaycaster.value?.instance;
630
- const camera = createdCamera.value?.instance;
721
+ const raycaster = ensuredRaycaster.value?.instance;
722
+ const camera = ensuredCamera.value?.instance;
631
723
  if (!raycaster || !camera)
632
724
  return;
633
725
  raycaster.setFromCamera(globals.mousePos.value, camera);
@@ -711,119 +803,147 @@
711
803
  }
712
804
  return exports.lunchboxTree;
713
805
  }
714
- // ENSURE CAMERA
806
+ // This is used in `buildEnsured` below and `LunchboxWrapper`
807
+ /** Search the overrides record and the node tree for a node in the given types */
808
+ function tryGetNodeWithInstanceType(pascalCaseTypes) {
809
+ if (!Array.isArray(pascalCaseTypes)) {
810
+ pascalCaseTypes = [pascalCaseTypes];
811
+ }
812
+ // default to override if we have one
813
+ for (let singleType of pascalCaseTypes) {
814
+ if (overrides[singleType])
815
+ return overrides[singleType];
816
+ }
817
+ // look for auto-created node
818
+ for (let singleType of pascalCaseTypes) {
819
+ const found = autoCreated[singleType] ||
820
+ allNodes.find((node) => node.type?.toLowerCase() ===
821
+ singleType.toLowerCase());
822
+ // if we have one, save and return
823
+ if (found) {
824
+ const createdAsNode = found;
825
+ autoCreated[singleType] = createdAsNode;
826
+ return createdAsNode;
827
+ }
828
+ }
829
+ return null;
830
+ }
831
+ // GENERIC ENSURE FUNCTION
715
832
  // ====================
716
- const fallbackCameraUuid = 'FALLBACK_CAMERA';
717
- const createdCamera = vue.ref(null);
718
- const ensureCamera = () => {
719
- // look for cameras
720
- // TODO: does this need to be more robust?
721
- const foundCamera = allNodes.find((node) => node.type
722
- ?.toLowerCase()
723
- .includes('camera'));
724
- // if we have one, return
725
- if (foundCamera) {
726
- const cameraAsStandardNode = foundCamera;
727
- createdCamera.value = cameraAsStandardNode;
728
- return cameraAsStandardNode;
833
+ // Problem:
834
+ // I want to make sure an object of type Xyz exists in my Lunchbox app.
835
+ // If it doesn't exist, I want to create it and add it to the root node.
836
+ //
837
+ // Solution:
838
+ // export const ensuredXyz = buildEnsured<Xyz>('Xyz', 'FALLBACK_XYZ')
839
+ //
840
+ // Now in other components, you can do both:
841
+ // import { ensuredXyz }
842
+ // ensuredXyz.value (...)
843
+ // and:
844
+ // ensuredXyz.value = ...
845
+ const autoCreated = vue.reactive({});
846
+ const overrides = vue.reactive({});
847
+ /**
848
+ * Build a computed ensured value with a getter and setter.
849
+ * @param pascalCaseTypes List of types this can be. Will autocreate first type if array provided.
850
+ * @param fallbackUuid Fallback UUID to use.
851
+ * @param props Props to pass to autocreated element
852
+ * @returns Computed getter/setter for ensured object.
853
+ */
854
+ function buildEnsured(pascalCaseTypes, fallbackUuid, props = {}, callback = null) {
855
+ // make sure we've got an array
856
+ if (!Array.isArray(pascalCaseTypes)) {
857
+ pascalCaseTypes = [pascalCaseTypes];
729
858
  }
730
- // otherwise, create a new camera
731
- const root = ensureRootNode();
732
- const cameraNode = createNode({
733
- type: 'PerspectiveCamera',
734
- uuid: fallbackCameraUuid,
735
- props: {
736
- args: [45, 0.5625, 1, 1000],
859
+ // add type for autoCreated and overrides
860
+ for (let singleType of pascalCaseTypes) {
861
+ if (!autoCreated[singleType]) {
862
+ autoCreated[singleType] = null;
863
+ }
864
+ if (!overrides[singleType]) {
865
+ overrides[singleType] = null;
866
+ }
867
+ }
868
+ return vue.computed({
869
+ get() {
870
+ // try to get existing type
871
+ const existing = tryGetNodeWithInstanceType(pascalCaseTypes);
872
+ if (existing)
873
+ return existing;
874
+ // otherwise, create a new node
875
+ const root = ensureRootNode();
876
+ const node = createNode({
877
+ type: pascalCaseTypes[0],
878
+ uuid: fallbackUuid,
879
+ props,
880
+ });
881
+ root.addChild(node);
882
+ autoCreated[pascalCaseTypes[0]] = node;
883
+ if (callback) {
884
+ callback(node);
885
+ }
886
+ return node;
887
+ },
888
+ set(val) {
889
+ const t = val.type ?? '';
890
+ const pascalType = t[0].toUpperCase() + t.slice(1);
891
+ overrides[pascalType] = val;
737
892
  },
738
893
  });
739
- root.addChild(cameraNode);
740
- createdCamera.value = cameraNode;
741
- // add camera to scene
742
- ensureScene().instance?.add(cameraNode.instance);
743
- return cameraNode;
744
- };
894
+ }
895
+ // ENSURE CAMERA
896
+ // ====================
897
+ const fallbackCameraUuid = 'FALLBACK_CAMERA';
898
+ const defaultCamera = buildEnsured(['PerspectiveCamera', 'OrthographicCamera'], fallbackCameraUuid, { args: [45, 0.5625, 1, 1000] });
899
+ /** Special value to be changed ONLY in `LunchboxWrapper`.
900
+ * Functions waiting for a Camera need to wait for this to be true. */
901
+ const cameraReady = vue.ref(false);
902
+ const ensuredCamera = vue.computed({
903
+ get() {
904
+ return (cameraReady.value ? defaultCamera.value : null);
905
+ },
906
+ set(val) {
907
+ const t = val.type ?? '';
908
+ const pascalType = t[0].toUpperCase() + t.slice(1);
909
+ overrides[pascalType] = val;
910
+ },
911
+ });
912
+ // export const ensuredCamera = buildEnsured<THREE.Camera>(
913
+ // ['PerspectiveCamera', 'OrthographicCamera'],
914
+ // fallbackCameraUuid,
915
+ // {
916
+ // args: [45, 0.5625, 1, 1000],
917
+ // }
918
+ // )
745
919
  // ENSURE RENDERER
746
920
  // ====================
747
921
  const fallbackRendererUuid = 'FALLBACK_RENDERER';
748
- const createdRenderer = vue.ref(null);
749
- const ensureRenderer = (fallbackArgs = [], sugar = {}) => {
750
- // look for renderers
751
- // TODO: does this need to be more robust?
752
- const foundRenderer = allNodes.find((node) => node.type
753
- ?.toLowerCase()
754
- .includes('renderer'));
755
- // if we have one, return
756
- if (foundRenderer) {
757
- const rendererAsStandardNode = foundRenderer;
758
- createdRenderer.value = rendererAsStandardNode;
759
- return rendererAsStandardNode;
760
- }
761
- // otherwise, create a new renderer
762
- const root = ensureRootNode();
763
- const rendererNode = createNode({
764
- type: 'WebGLRenderer',
765
- uuid: fallbackRendererUuid,
766
- }, { args: fallbackArgs });
767
- // shadow sugar
768
- if (sugar?.shadow) {
769
- rendererNode.instance.shadowMap.enabled = true;
770
- if (typeof sugar.shadow === 'object') {
771
- rendererNode.instance.shadowMap.type = sugar.shadow.type;
772
- }
773
- }
774
- root.addChild(rendererNode);
775
- createdRenderer.value = rendererNode;
776
- // return created node
777
- return rendererNode;
778
- };
922
+ const v = buildEnsured(
923
+ // TODO: ensure support for css/svg renderers
924
+ ['WebGLRenderer'], //, 'CSS2DRenderer', 'CSS3DRenderer', 'SVGRenderer'],
925
+ fallbackRendererUuid, {});
926
+ /** Special value to be changed ONLY in `LunchboxWrapper`.
927
+ * Functions waiting for a Renderer need to wait for this to be true. */
928
+ const rendererReady = vue.ref(false);
929
+ const ensureRenderer = vue.computed({
930
+ get() {
931
+ return (rendererReady.value ? v.value : null);
932
+ },
933
+ set(val) {
934
+ const t = val.type ?? '';
935
+ const pascalType = t[0].toUpperCase() + t.slice(1);
936
+ overrides[pascalType] = val;
937
+ },
938
+ });
779
939
  // ENSURE SCENE
780
940
  // ====================
781
941
  const fallbackSceneUuid = 'FALLBACK_SCENE';
782
- const createdScene = vue.ref();
783
- const ensureScene = () => {
784
- // look for scenes
785
- const foundScene = allNodes.find((node) => node.type?.toLowerCase() === 'scene');
786
- // if we have one, return
787
- if (foundScene) {
788
- const sceneAsLunchboxNode = foundScene;
789
- createdScene.value = sceneAsLunchboxNode;
790
- return sceneAsLunchboxNode;
791
- }
792
- // otherwise, create a new scene
793
- const root = ensureRootNode();
794
- const sceneNode = createNode({
795
- type: 'Scene',
796
- uuid: fallbackSceneUuid,
797
- });
798
- root.addChild(sceneNode);
799
- createdScene.value = sceneNode;
800
- return sceneNode;
801
- };
942
+ const ensuredScene = buildEnsured('Scene', fallbackSceneUuid);
802
943
  // ENSURE AUTO-RAYCASTER
803
944
  const autoRaycasterUuid = 'AUTO_RAYCASTER';
804
- const createdRaycaster = vue.ref(null);
805
- const ensureRaycaster = () => {
806
- // look for autoraycaster
807
- const found = allNodes.find((node) => node.uuid === autoRaycasterUuid);
808
- // if we have one, return
809
- if (found) {
810
- const foundAsNode = found;
811
- createdRaycaster.value = foundAsNode;
812
- return foundAsNode;
813
- }
814
- // otherwise, create raycaster
815
- const root = ensureRootNode();
816
- const raycasterNode = createNode({
817
- type: 'Raycaster',
818
- uuid: autoRaycasterUuid,
819
- });
820
- root.addChild(raycasterNode);
821
- createdRaycaster.value = raycasterNode;
822
- // finish auto-raycaster setup
823
- setupAutoRaycaster(raycasterNode);
824
- // done with raycaster
825
- return raycasterNode;
826
- };
945
+ // `unknown` is intentional here - we need to typecast the node since Raycaster isn't an Object3D
946
+ const ensuredRaycaster = buildEnsured('Raycaster', autoRaycasterUuid, {}, (node) => setupAutoRaycaster(node));
827
947
 
828
948
  const createComponent = (tag) => vue.defineComponent({
829
949
  inheritAttrs: false,
@@ -871,9 +991,11 @@
871
991
  // replace $attached values with their instances
872
992
  // we need to guarantee everything comes back as an array so we can spread $attachedArrays,
873
993
  // so we'll use processPropAsArray
874
- const argsWrappedInArrays = args.map((arg) => { return processPropAsArray({ node, prop: arg }); });
994
+ const argsWrappedInArrays = args.map((arg) => {
995
+ return processPropAsArray({ node, prop: arg });
996
+ });
875
997
  let processedArgs = [];
876
- argsWrappedInArrays.forEach(arr => {
998
+ argsWrappedInArrays.forEach((arr) => {
877
999
  processedArgs = processedArgs.concat(arr);
878
1000
  });
879
1001
  const instance = new targetClass(...processedArgs);
@@ -975,7 +1097,7 @@
975
1097
  get nextSibling() {
976
1098
  if (!this.parentNode)
977
1099
  return null;
978
- const idx = this.parentNode.children.findIndex(n => n.uuid === this.uuid);
1100
+ const idx = this.parentNode.children.findIndex((n) => n.uuid === this.uuid);
979
1101
  // return next sibling if we're present and not the last child of the parent
980
1102
  if (idx !== -1 && idx < this.parentNode.children.length - 1) {
981
1103
  return this.parentNode.children[idx + 1];
@@ -985,7 +1107,7 @@
985
1107
  insertBefore(child, anchor) {
986
1108
  child.removeAsChildFromAnyParents();
987
1109
  child.parentNode = this;
988
- const anchorIdx = this.children.findIndex(n => n.uuid === anchor?.uuid);
1110
+ const anchorIdx = this.children.findIndex((n) => n.uuid === anchor?.uuid);
989
1111
  if (anchorIdx !== -1) {
990
1112
  this.children.splice(anchorIdx, 0, child);
991
1113
  }
@@ -994,7 +1116,7 @@
994
1116
  }
995
1117
  }
996
1118
  removeChild(child) {
997
- const idx = this.children.findIndex(n => n?.uuid === child?.uuid);
1119
+ const idx = this.children.findIndex((n) => n?.uuid === child?.uuid);
998
1120
  if (idx !== -1) {
999
1121
  this.children.splice(idx, 1);
1000
1122
  }
@@ -1006,7 +1128,7 @@
1006
1128
  if (child) {
1007
1129
  // remove child from any other parents
1008
1130
  child.removeAsChildFromAnyParents();
1009
- // add to this node
1131
+ // add to this node
1010
1132
  child.parentNode = this;
1011
1133
  this.insertBefore(child, null);
1012
1134
  }
@@ -1034,11 +1156,15 @@
1034
1156
  // TODO: depth-first vs breadth-first
1035
1157
  walk(callback) {
1036
1158
  const queue = [this, ...this.children];
1159
+ const traversed = [];
1037
1160
  let canContinue = true;
1038
1161
  while (queue.length && canContinue) {
1039
1162
  const current = queue.shift();
1040
1163
  if (current) {
1041
- queue.push(...current.children);
1164
+ if (traversed.includes(current))
1165
+ continue;
1166
+ traversed.push(current);
1167
+ queue.push(...current.children.filter((child) => !traversed.includes(child)));
1042
1168
  canContinue = callback(current);
1043
1169
  }
1044
1170
  else {
@@ -1050,7 +1176,7 @@
1050
1176
  // ====================
1051
1177
  minidomType;
1052
1178
  removeAsChildFromAnyParents() {
1053
- allNodes.forEach(node => node.removeChild(this));
1179
+ allNodes.forEach((node) => node.removeChild(this));
1054
1180
  }
1055
1181
  }
1056
1182
  MiniDom.BaseNode = BaseNode;
@@ -1074,8 +1200,8 @@
1074
1200
  drop() {
1075
1201
  super.drop();
1076
1202
  // handle remove functions
1077
- Object.keys(this.eventListenerRemoveFunctions).forEach(key => {
1078
- this.eventListenerRemoveFunctions[key].forEach(func => func());
1203
+ Object.keys(this.eventListenerRemoveFunctions).forEach((key) => {
1204
+ this.eventListenerRemoveFunctions[key].forEach((func) => func());
1079
1205
  });
1080
1206
  }
1081
1207
  }
@@ -1086,7 +1212,8 @@
1086
1212
  class RendererRootNode extends MiniDom.RendererBaseNode {
1087
1213
  constructor(options = {}, parent) {
1088
1214
  super(options, parent);
1089
- this.domElement = options.domElement ?? document.createElement('div');
1215
+ this.domElement =
1216
+ options.domElement ?? document.createElement('div');
1090
1217
  }
1091
1218
  domElement;
1092
1219
  isLunchboxRootNode = true;
@@ -1103,7 +1230,8 @@
1103
1230
  class RendererDomNode extends MiniDom.RendererBaseNode {
1104
1231
  constructor(options = {}, parent) {
1105
1232
  super(options, parent);
1106
- this.domElement = options.domElement ?? document.createElement('div');
1233
+ this.domElement =
1234
+ options.domElement ?? document.createElement('div');
1107
1235
  }
1108
1236
  domElement;
1109
1237
  }
@@ -1136,7 +1264,14 @@
1136
1264
  const beforeRender = [];
1137
1265
  const afterRender = [];
1138
1266
  const update = (opts) => {
1139
- frameID = requestAnimationFrame(() => update(opts));
1267
+ // request next frame
1268
+ frameID = requestAnimationFrame(() => update({
1269
+ app: opts.app,
1270
+ renderer: ensureRenderer.value?.instance,
1271
+ scene: ensuredScene.value.instance,
1272
+ camera: ensuredCamera.value?.instance,
1273
+ }));
1274
+ // prep options
1140
1275
  const { app, renderer, scene, camera } = opts;
1141
1276
  // BEFORE RENDER
1142
1277
  beforeRender.forEach((cb) => {
@@ -1146,7 +1281,12 @@
1146
1281
  });
1147
1282
  // RENDER
1148
1283
  if (renderer && scene && camera) {
1149
- renderer.render(scene, camera);
1284
+ if (app.customRender) {
1285
+ app.customRender(opts);
1286
+ }
1287
+ else {
1288
+ renderer.render(vue.toRaw(scene), vue.toRaw(camera));
1289
+ }
1150
1290
  }
1151
1291
  // AFTER RENDER
1152
1292
  afterRender.forEach((cb) => {
@@ -1154,22 +1294,6 @@
1154
1294
  cb(opts);
1155
1295
  }
1156
1296
  });
1157
- /*
1158
- frameID = requestAnimationFrame(() => update(renderer, scene, camera))
1159
-
1160
- // Make sure we have all necessary components
1161
- if (!renderer) {
1162
- renderer = ensureRenderer().instance
1163
- }
1164
- if (!scene) {
1165
- scene = ensureScene().instance
1166
- }
1167
- if (!camera) {
1168
- camera = ensureCamera().instance
1169
- }
1170
-
1171
-
1172
- */
1173
1297
  };
1174
1298
  const onBeforeRender = (cb, index = Infinity) => {
1175
1299
  if (index === Infinity) {
@@ -1179,6 +1303,15 @@
1179
1303
  beforeRender.splice(index, 0, cb);
1180
1304
  }
1181
1305
  };
1306
+ const offBeforeRender = (cb) => {
1307
+ if (isFinite(cb)) {
1308
+ beforeRender.splice(cb, 1);
1309
+ }
1310
+ else {
1311
+ const idx = beforeRender.findIndex((v) => v == cb);
1312
+ beforeRender.splice(idx, 1);
1313
+ }
1314
+ };
1182
1315
  const onAfterRender = (cb, index = Infinity) => {
1183
1316
  if (index === Infinity) {
1184
1317
  afterRender.push(cb);
@@ -1187,6 +1320,15 @@
1187
1320
  afterRender.splice(index, 0, cb);
1188
1321
  }
1189
1322
  };
1323
+ const offAfterRender = (cb) => {
1324
+ if (isFinite(cb)) {
1325
+ afterRender.splice(cb, 1);
1326
+ }
1327
+ else {
1328
+ const idx = afterRender.findIndex((v) => v == cb);
1329
+ afterRender.splice(idx, 1);
1330
+ }
1331
+ };
1190
1332
  const cancelUpdate = () => {
1191
1333
  if (frameID)
1192
1334
  cancelAnimationFrame(frameID);
@@ -1292,10 +1434,7 @@
1292
1434
  'src',
1293
1435
  ];
1294
1436
 
1295
- const autoAttach = [
1296
- 'geometry',
1297
- 'material',
1298
- ];
1437
+ const autoAttach = ['geometry', 'material'];
1299
1438
  const createElement = (type, isSVG, isCustomizedBuiltin, vnodeProps) => {
1300
1439
  const options = { type };
1301
1440
  if (vnodeProps) {
@@ -1310,7 +1449,7 @@
1310
1449
  // handle standard node
1311
1450
  const node = createNode(options);
1312
1451
  // autoattach
1313
- autoAttach.forEach(key => {
1452
+ autoAttach.forEach((key) => {
1314
1453
  if (type.toLowerCase().endsWith(key)) {
1315
1454
  node.props.attach = key;
1316
1455
  }
@@ -1350,17 +1489,19 @@
1350
1489
  }
1351
1490
  // add to scene if parent is the wrapper node
1352
1491
  if (child.metaType === 'standardMeta' &&
1353
- child.type !== 'Scene' &&
1492
+ child.type !== 'scene' &&
1354
1493
  isLunchboxRootNode(effectiveParent)) {
1355
1494
  // ensure scene exists
1356
- const sceneNode = ensureScene();
1495
+ const sceneNode = ensuredScene.value;
1357
1496
  if (sceneNode.instance && child) {
1358
1497
  sceneNode.addChild(child);
1359
1498
  }
1360
1499
  if (child.instance &&
1361
1500
  child.instance.isObject3D &&
1362
1501
  sceneNode.instance) {
1363
- sceneNode.instance.add(child.instance);
1502
+ if (sceneNode !== child) {
1503
+ sceneNode.instance.add(child.instance);
1504
+ }
1364
1505
  }
1365
1506
  }
1366
1507
  // add to hierarchy otherwise
@@ -1443,19 +1584,29 @@
1443
1584
  const remove = (node) => {
1444
1585
  if (!node)
1445
1586
  return;
1587
+ const overrideKeys = Object.keys(overrides);
1446
1588
  // prep subtree
1447
1589
  const subtree = [];
1448
- node.walk(descendant => {
1590
+ node.walk((descendant) => {
1449
1591
  subtree.push(descendant);
1450
1592
  return true;
1451
1593
  });
1452
1594
  // clean up subtree
1453
1595
  subtree.forEach((n) => {
1596
+ const overrideKey = overrideKeys.find((key) => overrides[key]?.uuid === n.uuid);
1597
+ // if this node is an override, remove it from the overrides list
1598
+ if (overrideKey) {
1599
+ overrides[overrideKey] = null;
1600
+ }
1454
1601
  if (isLunchboxStandardNode(n)) {
1455
1602
  // try to remove three object
1456
1603
  n.instance?.removeFromParent?.();
1457
1604
  // try to dispose three object
1458
- const dispose = n.instance?.dispose;
1605
+ const dispose =
1606
+ // calling `dispose` on a scene triggers an error,
1607
+ // so let's ignore if this node is a scene
1608
+ n.type !== 'scene' &&
1609
+ n.instance?.dispose;
1459
1610
  if (dispose)
1460
1611
  dispose.bind(n.instance)();
1461
1612
  n.instance = null;
@@ -1463,7 +1614,7 @@
1463
1614
  // drop tree node
1464
1615
  n.drop();
1465
1616
  // remove Lunchbox node from main list
1466
- const idx = allNodes.findIndex(v => v.uuid === n.uuid);
1617
+ const idx = allNodes.findIndex((v) => v.uuid === n.uuid);
1467
1618
  if (idx !== -1) {
1468
1619
  allNodes.splice(idx, 1);
1469
1620
  }
@@ -1530,15 +1681,34 @@
1530
1681
  dpr: vue.ref(1),
1531
1682
  inputActive,
1532
1683
  mousePos,
1533
- camera: createdCamera,
1534
- renderer: createdRenderer,
1535
- scene: createdScene,
1536
1684
  };
1537
- const camera = vue.computed(() => globals.camera.value?.instance);
1538
- const renderer = vue.computed(() => globals.renderer.value?.instance);
1539
- const scene = vue.computed(() => globals.scene.value?.instance);
1685
+ const camera = vue.computed(() => ensuredCamera.value?.instance ?? null);
1686
+ const renderer = vue.computed(() => ensureRenderer.value?.instance ?? null);
1687
+ const scene = vue.computed(() => ensuredScene.value.instance);
1688
+ // CUSTOM RENDER SUPPORT
1689
+ // ====================
1690
+ let app = null;
1691
+ let queuedCustomRenderFunction = null;
1692
+ /** Set a custom render function, overriding the Lunchbox app's default render function.
1693
+ * Changing this requires the user to manually render their scene.
1694
+ */
1695
+ const setCustomRender = (render) => {
1696
+ if (app)
1697
+ app.setCustomRender(render);
1698
+ else
1699
+ queuedCustomRenderFunction = render;
1700
+ };
1701
+ /** Clear the active app's custom render function. */
1702
+ const clearCustomRender = () => {
1703
+ if (app)
1704
+ app.clearCustomRender();
1705
+ else
1706
+ queuedCustomRenderFunction = null;
1707
+ };
1708
+ // CREATE APP
1709
+ // ====================
1540
1710
  const createApp = (root) => {
1541
- const app = vue.createRenderer(nodeOps).createApp(root);
1711
+ app = vue.createRenderer(nodeOps).createApp(root);
1542
1712
  // register all components
1543
1713
  Object.keys(components).forEach((key) => {
1544
1714
  app.component(key, components[key]);
@@ -1561,27 +1731,38 @@
1561
1731
  };
1562
1732
  // embed .extend function
1563
1733
  app.extend = (targets) => {
1564
- extend({ app, ...targets });
1734
+ extend({ app: app, ...targets });
1565
1735
  return app;
1566
1736
  };
1567
- // kick update loop
1568
- // app.update = update
1569
- // app.update({
1570
- // app,
1571
- // })
1737
+ // prep for custom render support
1738
+ app.setCustomRender = (newRender) => {
1739
+ app.customRender = newRender;
1740
+ };
1741
+ // add queued custom render if we have one
1742
+ if (queuedCustomRenderFunction) {
1743
+ app.setCustomRender(queuedCustomRenderFunction);
1744
+ queuedCustomRenderFunction = null;
1745
+ }
1746
+ // add custom render removal
1747
+ app.clearCustomRender = () => {
1748
+ app.customRender = null;
1749
+ };
1572
1750
  // done
1573
1751
  return app;
1574
1752
  };
1575
1753
 
1576
1754
  exports.camera = camera;
1755
+ exports.clearCustomRender = clearCustomRender;
1577
1756
  exports.createApp = createApp;
1578
1757
  exports.find = find;
1579
1758
  exports.globals = globals;
1759
+ exports.offAfterRender = offAfterRender;
1760
+ exports.offBeforeRender = offBeforeRender;
1580
1761
  exports.onAfterRender = onAfterRender;
1581
1762
  exports.onBeforeRender = onBeforeRender;
1582
1763
  exports.renderer = renderer;
1583
1764
  exports.scene = scene;
1584
- exports.update = update;
1765
+ exports.setCustomRender = setCustomRender;
1585
1766
 
1586
1767
  Object.defineProperty(exports, '__esModule', { value: true });
1587
1768