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.
@@ -1,4 +1,4 @@
1
- import { ref, onMounted, getCurrentInstance, onBeforeUnmount, h, defineComponent, isRef, isVNode, watch, computed, createRenderer } from 'vue';
1
+ import { toRaw, ref, onMounted, getCurrentInstance, onBeforeUnmount, h, defineComponent, isRef, isVNode, watch, reactive, computed, createRenderer } from 'vue';
2
2
  import * as THREE from 'three';
3
3
  import { Color } from 'three';
4
4
  import { set, get, isNumber } from 'lodash';
@@ -7,29 +7,39 @@ import { set, get, isNumber } from 'lodash';
7
7
  const allNodes = [];
8
8
 
9
9
  const resizeCanvas = (width, height) => {
10
- const renderer = ensureRenderer().instance;
11
- const scene = ensureScene().instance;
10
+ const renderer = ensureRenderer.value?.instance;
11
+ const scene = ensuredScene.value.instance;
12
+ const camera = ensuredCamera.value;
12
13
  // ignore if no element
13
- if (!renderer?.domElement || !scene)
14
+ if (!renderer?.domElement || !scene || !camera)
14
15
  return;
15
16
  width = width ?? window.innerWidth;
16
17
  height = height ?? window.innerHeight;
17
18
  // update camera
18
19
  const aspect = width / height;
19
- const camera = ensureCamera();
20
20
  if (camera.type?.toLowerCase() === 'perspectivecamera') {
21
21
  const perspectiveCamera = camera.instance;
22
22
  perspectiveCamera.aspect = aspect;
23
23
  perspectiveCamera.updateProjectionMatrix();
24
24
  }
25
+ else if (camera.type?.toLowerCase() === 'orthographiccamera') {
26
+ // console.log('TODO: ortho camera update')
27
+ const orthoCamera = camera.instance;
28
+ const heightInTermsOfWidth = height / width;
29
+ orthoCamera.top = heightInTermsOfWidth * 10;
30
+ orthoCamera.bottom = -heightInTermsOfWidth * 10;
31
+ orthoCamera.right = 10;
32
+ orthoCamera.left = -10;
33
+ orthoCamera.updateProjectionMatrix();
34
+ }
25
35
  else {
26
- console.log('TODO: ortho camera update');
36
+ console.log('TODO: non-ortho or perspective camera');
27
37
  }
28
38
  // update canvas
29
39
  renderer.setSize(width, height);
30
40
  // render immediately so there's no flicker
31
41
  if (scene && camera.instance) {
32
- renderer.render(scene, camera.instance);
42
+ renderer.render(toRaw(scene), toRaw(camera.instance));
33
43
  }
34
44
  };
35
45
 
@@ -73,8 +83,13 @@ const LunchboxWrapper = {
73
83
  props: {
74
84
  // These should match the Lunchbox.WrapperProps interface
75
85
  background: String,
86
+ cameraArgs: Array,
87
+ cameraLook: Array,
88
+ cameraLookAt: Array,
76
89
  cameraPosition: Array,
77
90
  dpr: Number,
91
+ ortho: Boolean,
92
+ orthographic: Boolean,
78
93
  rendererProperties: Object,
79
94
  shadow: [Boolean, Object],
80
95
  transparent: Boolean,
@@ -85,6 +100,7 @@ const LunchboxWrapper = {
85
100
  const dpr = ref(props.dpr ?? -1);
86
101
  const container = ref();
87
102
  let renderer;
103
+ let camera;
88
104
  let scene;
89
105
  // MOUNT
90
106
  // ====================
@@ -92,40 +108,106 @@ const LunchboxWrapper = {
92
108
  // canvas needs to exist
93
109
  if (!canvas.value)
94
110
  throw new Error('missing canvas');
95
- // ensure camera
96
- const camera = ensureCamera().instance;
97
- // move camera if needed
98
- if (camera && props.cameraPosition) {
99
- camera.position.set(...props.cameraPosition);
100
- }
101
111
  // RENDERER
102
112
  // ====================
103
- // build renderer args
104
- const rendererArgs = {
105
- antialias: true,
106
- canvas: canvas.value.domElement,
107
- };
108
- if (props.transparent) {
109
- rendererArgs.alpha = true;
110
- }
111
- const sugar = {
112
- shadow: props.shadow,
113
- };
114
- // ensure renderer
115
- renderer = ensureRenderer([rendererArgs], sugar);
116
- // set renderer props if needed
117
- if (props.rendererProperties) {
118
- Object.keys(props.rendererProperties).forEach((key) => {
119
- set(renderer, key, props.rendererProperties[key]);
113
+ // is there already a renderer?
114
+ // TODO: allow other renderer types
115
+ renderer = tryGetNodeWithInstanceType([
116
+ 'WebGLRenderer',
117
+ ]);
118
+ // if renderer is missing, initialize with options
119
+ if (!renderer) {
120
+ // build renderer args
121
+ const rendererArgs = {
122
+ antialias: true,
123
+ canvas: canvas.value.domElement,
124
+ };
125
+ if (props.transparent) {
126
+ rendererArgs.alpha = true;
127
+ }
128
+ // create new renderer
129
+ ensureRenderer.value = createNode({
130
+ type: 'WebGLRenderer',
131
+ uuid: fallbackRendererUuid,
132
+ props: {
133
+ args: [rendererArgs],
134
+ },
120
135
  });
136
+ // we've initialized the renderer, so anything depending on it can execute now
137
+ rendererReady.value = true;
138
+ const rendererAsWebGlRenderer = ensureRenderer;
139
+ // update render sugar
140
+ const sugar = {
141
+ shadow: props.shadow,
142
+ };
143
+ if (rendererAsWebGlRenderer.value.instance && sugar?.shadow) {
144
+ rendererAsWebGlRenderer.value.instance.shadowMap.enabled =
145
+ true;
146
+ if (typeof sugar.shadow === 'object') {
147
+ rendererAsWebGlRenderer.value.instance.shadowMap.type =
148
+ sugar.shadow.type;
149
+ }
150
+ }
151
+ // set renderer props if needed
152
+ if (props.rendererProperties) {
153
+ Object.keys(props.rendererProperties).forEach((key) => {
154
+ set(rendererAsWebGlRenderer.value, key, props.rendererProperties[key]);
155
+ });
156
+ }
157
+ // update using created renderer
158
+ renderer = rendererAsWebGlRenderer.value;
121
159
  }
122
- if (renderer.uuid !== fallbackRendererUuid) {
160
+ else {
123
161
  useFallbackRenderer.value = false;
162
+ // the user has initialized the renderer, so anything depending
163
+ // on the renderer can execute
164
+ rendererReady.value = true;
124
165
  return;
125
166
  }
167
+ // CAMERA
168
+ // ====================
169
+ // is there already a camera?
170
+ camera = tryGetNodeWithInstanceType([
171
+ 'PerspectiveCamera',
172
+ 'OrthographicCamera',
173
+ ]);
174
+ // if not, let's create one
175
+ if (!camera) {
176
+ // create ortho camera
177
+ if (props.ortho || props.orthographic) {
178
+ ensuredCamera.value = createNode({
179
+ props: { args: props.cameraArgs ?? [] },
180
+ type: 'OrthographicCamera',
181
+ uuid: fallbackCameraUuid,
182
+ });
183
+ }
184
+ else {
185
+ ensuredCamera.value = createNode({
186
+ props: {
187
+ args: props.cameraArgs ?? [45, 0.5625, 1, 1000],
188
+ },
189
+ type: 'PerspectiveCamera',
190
+ uuid: fallbackCameraUuid,
191
+ });
192
+ }
193
+ cameraReady.value = true;
194
+ camera = ensuredCamera.value;
195
+ }
196
+ else {
197
+ cameraReady.value = true;
198
+ }
199
+ // move camera if needed
200
+ if (camera && props.cameraPosition) {
201
+ camera.instance?.position.set(...props.cameraPosition);
202
+ }
203
+ // angle camera if needed
204
+ if (camera && (props.cameraLookAt || props.cameraLook)) {
205
+ const source = (props.cameraLookAt || props.cameraLook);
206
+ camera.instance?.lookAt(...source);
207
+ }
126
208
  // SCENE
127
209
  // ====================
128
- scene = ensureScene();
210
+ scene = ensuredScene.value;
129
211
  // set background color
130
212
  if (scene && scene.instance && props.background) {
131
213
  scene.instance.background = new Color(props.background);
@@ -135,7 +217,7 @@ const LunchboxWrapper = {
135
217
  if (dpr.value === -1) {
136
218
  dpr.value = window.devicePixelRatio;
137
219
  }
138
- if (renderer.instance) {
220
+ if (renderer?.instance) {
139
221
  renderer.instance.setPixelRatio(dpr.value);
140
222
  globals.dpr.value = dpr.value;
141
223
  // prep canvas (sizing, observe, unmount, etc)
@@ -146,9 +228,10 @@ const LunchboxWrapper = {
146
228
  }
147
229
  // KICK UPDATE
148
230
  // ====================
231
+ // console.log(scene)
149
232
  update({
150
233
  app: getCurrentInstance().appContext.app,
151
- camera,
234
+ camera: camera.instance,
152
235
  renderer: renderer.instance,
153
236
  scene: scene.instance,
154
237
  });
@@ -178,21 +261,6 @@ const LunchboxWrapper = {
178
261
  },
179
262
  };
180
263
 
181
- const catalogue = {};
182
-
183
- const lunchboxDomComponentNames = [
184
- 'canvas',
185
- 'div',
186
- 'LunchboxWrapper',
187
- ];
188
- // component creation utility
189
- const createComponent$1 = (tag) => defineComponent({
190
- inheritAttrs: false,
191
- name: tag,
192
- setup(props, context) {
193
- return () => h(tag, context.attrs, context.slots?.default?.() || []);
194
- },
195
- });
196
264
  // list of all components to register out of the box
197
265
  const autoGeneratedComponents = [
198
266
  // ThreeJS basics
@@ -305,21 +373,11 @@ const autoGeneratedComponents = [
305
373
  'arrayCamera',
306
374
  // renderers
307
375
  'webGLRenderer',
308
- ].map(createComponent$1).reduce((acc, curr) => {
309
- acc[curr.name] = curr;
310
- return acc;
311
- });
312
- const components = {
313
- ...autoGeneratedComponents,
314
- 'Lunchbox': LunchboxWrapper,
315
- // Gltf,
316
- };
317
- // console.log(components, Gltf)
318
- /*
319
- // List copied from r3f
320
- // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
376
+ /*
377
+ // List copied from r3f:
378
+ // https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/three-types.ts
321
379
 
322
- // NOT IMPLEMENTED:
380
+ // NOT IMPLEMENTED (can be added via Extend - docs.lunchboxjs.com/components/extend/):
323
381
  audioListener: AudioListenerProps
324
382
  positionalAudio: PositionalAudioProps
325
383
 
@@ -367,6 +425,27 @@ const components = {
367
425
  fogExp2: FogExp2Props
368
426
  shape: ShapeProps
369
427
  */
428
+ ];
429
+
430
+ const catalogue = {};
431
+
432
+ const lunchboxDomComponentNames = ['canvas', 'div', 'LunchboxWrapper'];
433
+ // component creation utility
434
+ const createComponent$1 = (tag) => defineComponent({
435
+ inheritAttrs: false,
436
+ name: tag,
437
+ setup(props, context) {
438
+ return () => h(tag, context.attrs, context.slots?.default?.() || []);
439
+ },
440
+ });
441
+ autoGeneratedComponents.map(createComponent$1).reduce((acc, curr) => {
442
+ acc[curr.name] = curr;
443
+ return acc;
444
+ });
445
+ const components = {
446
+ ...autoGeneratedComponents,
447
+ Lunchbox: LunchboxWrapper,
448
+ };
370
449
 
371
450
  function find(target) {
372
451
  target = isRef(target) ? target.value : target;
@@ -423,7 +502,7 @@ const isLunchboxRootNode = (node) => {
423
502
  /** Create a new Lunchbox comment node. */
424
503
  function createCommentNode(options = {}) {
425
504
  const defaults = {
426
- text: options.text ?? ''
505
+ text: options.text ?? '',
427
506
  };
428
507
  return new MiniDom.RendererCommentNode({
429
508
  ...defaults,
@@ -447,7 +526,7 @@ function createDomNode(options = {}) {
447
526
  /** Create a new Lunchbox text node. */
448
527
  function createTextNode(options = {}) {
449
528
  const defaults = {
450
- text: options.text ?? ''
529
+ text: options.text ?? '',
451
530
  };
452
531
  return new MiniDom.RendererTextNode({
453
532
  ...options,
@@ -460,7 +539,7 @@ function createNode(options = {}, props = {}) {
460
539
  const defaults = {
461
540
  attached: options.attached ?? [],
462
541
  attachedArray: options.attachedArray ?? {},
463
- instance: options.instance ?? null
542
+ instance: options.instance ?? null,
464
543
  };
465
544
  const node = new MiniDom.RendererStandardNode({
466
545
  ...options,
@@ -468,14 +547,25 @@ function createNode(options = {}, props = {}) {
468
547
  metaType: 'standardMeta',
469
548
  });
470
549
  if (node.type && !isLunchboxRootNode(node) && !node.instance) {
550
+ // if (node.type.includes('Camera')) {
551
+ // console.log(node.type, {
552
+ // ...node.props,
553
+ // ...props,
554
+ // })
555
+ // console.trace()
556
+ // }
471
557
  node.instance = instantiateThreeObject({
472
558
  ...node,
473
559
  props: {
474
560
  ...node.props,
475
561
  ...props,
476
- }
562
+ },
477
563
  });
478
564
  }
565
+ if (node.type === 'scene') {
566
+ // manually set scene override
567
+ ensuredScene.value = node;
568
+ }
479
569
  return node;
480
570
  }
481
571
 
@@ -506,7 +596,9 @@ function addEventListener({ node, key, value, }) {
506
596
  node.eventListeners[key].push(value);
507
597
  // if we need it, let's get/create the main raycaster
508
598
  if (interactionsRequiringRaycaster.includes(key)) {
509
- ensureRaycaster();
599
+ // we're not using `v` here, we're just making sure the raycaster has been created
600
+ // TODO: is this necessary?
601
+ ensuredRaycaster.value;
510
602
  if (node.instance && !interactables.includes(node)) {
511
603
  addInteractable(node);
512
604
  node.eventListenerRemoveFunctions[key].push(() => removeInteractable(node));
@@ -564,7 +656,7 @@ const setupAutoRaycaster = (node) => {
564
656
  return;
565
657
  // add mouse events once renderer is ready
566
658
  let stopWatcher = null;
567
- stopWatcher = watch(() => createdRenderer.value, (renderer) => {
659
+ stopWatcher = watch(() => ensureRenderer.value, (renderer) => {
568
660
  // make sure renderer exists
569
661
  if (!renderer?.instance)
570
662
  return;
@@ -605,8 +697,8 @@ const setupAutoRaycaster = (node) => {
605
697
  let currentIntersections = [];
606
698
  const autoRaycasterBeforeRender = () => {
607
699
  // setup
608
- const raycaster = createdRaycaster.value?.instance;
609
- const camera = createdCamera.value?.instance;
700
+ const raycaster = ensuredRaycaster.value?.instance;
701
+ const camera = ensuredCamera.value?.instance;
610
702
  if (!raycaster || !camera)
611
703
  return;
612
704
  raycaster.setFromCamera(globals.mousePos.value, camera);
@@ -690,119 +782,147 @@ function ensureRootNode(options = {}) {
690
782
  }
691
783
  return lunchboxRootNode;
692
784
  }
693
- // ENSURE CAMERA
785
+ // This is used in `buildEnsured` below and `LunchboxWrapper`
786
+ /** Search the overrides record and the node tree for a node in the given types */
787
+ function tryGetNodeWithInstanceType(pascalCaseTypes) {
788
+ if (!Array.isArray(pascalCaseTypes)) {
789
+ pascalCaseTypes = [pascalCaseTypes];
790
+ }
791
+ // default to override if we have one
792
+ for (let singleType of pascalCaseTypes) {
793
+ if (overrides[singleType])
794
+ return overrides[singleType];
795
+ }
796
+ // look for auto-created node
797
+ for (let singleType of pascalCaseTypes) {
798
+ const found = autoCreated[singleType] ||
799
+ allNodes.find((node) => node.type?.toLowerCase() ===
800
+ singleType.toLowerCase());
801
+ // if we have one, save and return
802
+ if (found) {
803
+ const createdAsNode = found;
804
+ autoCreated[singleType] = createdAsNode;
805
+ return createdAsNode;
806
+ }
807
+ }
808
+ return null;
809
+ }
810
+ // GENERIC ENSURE FUNCTION
694
811
  // ====================
695
- const fallbackCameraUuid = 'FALLBACK_CAMERA';
696
- const createdCamera = ref(null);
697
- const ensureCamera = () => {
698
- // look for cameras
699
- // TODO: does this need to be more robust?
700
- const foundCamera = allNodes.find((node) => node.type
701
- ?.toLowerCase()
702
- .includes('camera'));
703
- // if we have one, return
704
- if (foundCamera) {
705
- const cameraAsStandardNode = foundCamera;
706
- createdCamera.value = cameraAsStandardNode;
707
- return cameraAsStandardNode;
812
+ // Problem:
813
+ // I want to make sure an object of type Xyz exists in my Lunchbox app.
814
+ // If it doesn't exist, I want to create it and add it to the root node.
815
+ //
816
+ // Solution:
817
+ // export const ensuredXyz = buildEnsured<Xyz>('Xyz', 'FALLBACK_XYZ')
818
+ //
819
+ // Now in other components, you can do both:
820
+ // import { ensuredXyz }
821
+ // ensuredXyz.value (...)
822
+ // and:
823
+ // ensuredXyz.value = ...
824
+ const autoCreated = reactive({});
825
+ const overrides = reactive({});
826
+ /**
827
+ * Build a computed ensured value with a getter and setter.
828
+ * @param pascalCaseTypes List of types this can be. Will autocreate first type if array provided.
829
+ * @param fallbackUuid Fallback UUID to use.
830
+ * @param props Props to pass to autocreated element
831
+ * @returns Computed getter/setter for ensured object.
832
+ */
833
+ function buildEnsured(pascalCaseTypes, fallbackUuid, props = {}, callback = null) {
834
+ // make sure we've got an array
835
+ if (!Array.isArray(pascalCaseTypes)) {
836
+ pascalCaseTypes = [pascalCaseTypes];
708
837
  }
709
- // otherwise, create a new camera
710
- const root = ensureRootNode();
711
- const cameraNode = createNode({
712
- type: 'PerspectiveCamera',
713
- uuid: fallbackCameraUuid,
714
- props: {
715
- args: [45, 0.5625, 1, 1000],
838
+ // add type for autoCreated and overrides
839
+ for (let singleType of pascalCaseTypes) {
840
+ if (!autoCreated[singleType]) {
841
+ autoCreated[singleType] = null;
842
+ }
843
+ if (!overrides[singleType]) {
844
+ overrides[singleType] = null;
845
+ }
846
+ }
847
+ return computed({
848
+ get() {
849
+ // try to get existing type
850
+ const existing = tryGetNodeWithInstanceType(pascalCaseTypes);
851
+ if (existing)
852
+ return existing;
853
+ // otherwise, create a new node
854
+ const root = ensureRootNode();
855
+ const node = createNode({
856
+ type: pascalCaseTypes[0],
857
+ uuid: fallbackUuid,
858
+ props,
859
+ });
860
+ root.addChild(node);
861
+ autoCreated[pascalCaseTypes[0]] = node;
862
+ if (callback) {
863
+ callback(node);
864
+ }
865
+ return node;
866
+ },
867
+ set(val) {
868
+ const t = val.type ?? '';
869
+ const pascalType = t[0].toUpperCase() + t.slice(1);
870
+ overrides[pascalType] = val;
716
871
  },
717
872
  });
718
- root.addChild(cameraNode);
719
- createdCamera.value = cameraNode;
720
- // add camera to scene
721
- ensureScene().instance?.add(cameraNode.instance);
722
- return cameraNode;
723
- };
873
+ }
874
+ // ENSURE CAMERA
875
+ // ====================
876
+ const fallbackCameraUuid = 'FALLBACK_CAMERA';
877
+ const defaultCamera = buildEnsured(['PerspectiveCamera', 'OrthographicCamera'], fallbackCameraUuid, { args: [45, 0.5625, 1, 1000] });
878
+ /** Special value to be changed ONLY in `LunchboxWrapper`.
879
+ * Functions waiting for a Camera need to wait for this to be true. */
880
+ const cameraReady = ref(false);
881
+ const ensuredCamera = computed({
882
+ get() {
883
+ return (cameraReady.value ? defaultCamera.value : null);
884
+ },
885
+ set(val) {
886
+ const t = val.type ?? '';
887
+ const pascalType = t[0].toUpperCase() + t.slice(1);
888
+ overrides[pascalType] = val;
889
+ },
890
+ });
891
+ // export const ensuredCamera = buildEnsured<THREE.Camera>(
892
+ // ['PerspectiveCamera', 'OrthographicCamera'],
893
+ // fallbackCameraUuid,
894
+ // {
895
+ // args: [45, 0.5625, 1, 1000],
896
+ // }
897
+ // )
724
898
  // ENSURE RENDERER
725
899
  // ====================
726
900
  const fallbackRendererUuid = 'FALLBACK_RENDERER';
727
- const createdRenderer = ref(null);
728
- const ensureRenderer = (fallbackArgs = [], sugar = {}) => {
729
- // look for renderers
730
- // TODO: does this need to be more robust?
731
- const foundRenderer = allNodes.find((node) => node.type
732
- ?.toLowerCase()
733
- .includes('renderer'));
734
- // if we have one, return
735
- if (foundRenderer) {
736
- const rendererAsStandardNode = foundRenderer;
737
- createdRenderer.value = rendererAsStandardNode;
738
- return rendererAsStandardNode;
739
- }
740
- // otherwise, create a new renderer
741
- const root = ensureRootNode();
742
- const rendererNode = createNode({
743
- type: 'WebGLRenderer',
744
- uuid: fallbackRendererUuid,
745
- }, { args: fallbackArgs });
746
- // shadow sugar
747
- if (sugar?.shadow) {
748
- rendererNode.instance.shadowMap.enabled = true;
749
- if (typeof sugar.shadow === 'object') {
750
- rendererNode.instance.shadowMap.type = sugar.shadow.type;
751
- }
752
- }
753
- root.addChild(rendererNode);
754
- createdRenderer.value = rendererNode;
755
- // return created node
756
- return rendererNode;
757
- };
901
+ const v = buildEnsured(
902
+ // TODO: ensure support for css/svg renderers
903
+ ['WebGLRenderer'], //, 'CSS2DRenderer', 'CSS3DRenderer', 'SVGRenderer'],
904
+ fallbackRendererUuid, {});
905
+ /** Special value to be changed ONLY in `LunchboxWrapper`.
906
+ * Functions waiting for a Renderer need to wait for this to be true. */
907
+ const rendererReady = ref(false);
908
+ const ensureRenderer = computed({
909
+ get() {
910
+ return (rendererReady.value ? v.value : null);
911
+ },
912
+ set(val) {
913
+ const t = val.type ?? '';
914
+ const pascalType = t[0].toUpperCase() + t.slice(1);
915
+ overrides[pascalType] = val;
916
+ },
917
+ });
758
918
  // ENSURE SCENE
759
919
  // ====================
760
920
  const fallbackSceneUuid = 'FALLBACK_SCENE';
761
- const createdScene = ref();
762
- const ensureScene = () => {
763
- // look for scenes
764
- const foundScene = allNodes.find((node) => node.type?.toLowerCase() === 'scene');
765
- // if we have one, return
766
- if (foundScene) {
767
- const sceneAsLunchboxNode = foundScene;
768
- createdScene.value = sceneAsLunchboxNode;
769
- return sceneAsLunchboxNode;
770
- }
771
- // otherwise, create a new scene
772
- const root = ensureRootNode();
773
- const sceneNode = createNode({
774
- type: 'Scene',
775
- uuid: fallbackSceneUuid,
776
- });
777
- root.addChild(sceneNode);
778
- createdScene.value = sceneNode;
779
- return sceneNode;
780
- };
921
+ const ensuredScene = buildEnsured('Scene', fallbackSceneUuid);
781
922
  // ENSURE AUTO-RAYCASTER
782
923
  const autoRaycasterUuid = 'AUTO_RAYCASTER';
783
- const createdRaycaster = ref(null);
784
- const ensureRaycaster = () => {
785
- // look for autoraycaster
786
- const found = allNodes.find((node) => node.uuid === autoRaycasterUuid);
787
- // if we have one, return
788
- if (found) {
789
- const foundAsNode = found;
790
- createdRaycaster.value = foundAsNode;
791
- return foundAsNode;
792
- }
793
- // otherwise, create raycaster
794
- const root = ensureRootNode();
795
- const raycasterNode = createNode({
796
- type: 'Raycaster',
797
- uuid: autoRaycasterUuid,
798
- });
799
- root.addChild(raycasterNode);
800
- createdRaycaster.value = raycasterNode;
801
- // finish auto-raycaster setup
802
- setupAutoRaycaster(raycasterNode);
803
- // done with raycaster
804
- return raycasterNode;
805
- };
924
+ // `unknown` is intentional here - we need to typecast the node since Raycaster isn't an Object3D
925
+ const ensuredRaycaster = buildEnsured('Raycaster', autoRaycasterUuid, {}, (node) => setupAutoRaycaster(node));
806
926
 
807
927
  const createComponent = (tag) => defineComponent({
808
928
  inheritAttrs: false,
@@ -850,9 +970,11 @@ function instantiateThreeObject(node) {
850
970
  // replace $attached values with their instances
851
971
  // we need to guarantee everything comes back as an array so we can spread $attachedArrays,
852
972
  // so we'll use processPropAsArray
853
- const argsWrappedInArrays = args.map((arg) => { return processPropAsArray({ node, prop: arg }); });
973
+ const argsWrappedInArrays = args.map((arg) => {
974
+ return processPropAsArray({ node, prop: arg });
975
+ });
854
976
  let processedArgs = [];
855
- argsWrappedInArrays.forEach(arr => {
977
+ argsWrappedInArrays.forEach((arr) => {
856
978
  processedArgs = processedArgs.concat(arr);
857
979
  });
858
980
  const instance = new targetClass(...processedArgs);
@@ -954,7 +1076,7 @@ var MiniDom;
954
1076
  get nextSibling() {
955
1077
  if (!this.parentNode)
956
1078
  return null;
957
- const idx = this.parentNode.children.findIndex(n => n.uuid === this.uuid);
1079
+ const idx = this.parentNode.children.findIndex((n) => n.uuid === this.uuid);
958
1080
  // return next sibling if we're present and not the last child of the parent
959
1081
  if (idx !== -1 && idx < this.parentNode.children.length - 1) {
960
1082
  return this.parentNode.children[idx + 1];
@@ -964,7 +1086,7 @@ var MiniDom;
964
1086
  insertBefore(child, anchor) {
965
1087
  child.removeAsChildFromAnyParents();
966
1088
  child.parentNode = this;
967
- const anchorIdx = this.children.findIndex(n => n.uuid === anchor?.uuid);
1089
+ const anchorIdx = this.children.findIndex((n) => n.uuid === anchor?.uuid);
968
1090
  if (anchorIdx !== -1) {
969
1091
  this.children.splice(anchorIdx, 0, child);
970
1092
  }
@@ -973,7 +1095,7 @@ var MiniDom;
973
1095
  }
974
1096
  }
975
1097
  removeChild(child) {
976
- const idx = this.children.findIndex(n => n?.uuid === child?.uuid);
1098
+ const idx = this.children.findIndex((n) => n?.uuid === child?.uuid);
977
1099
  if (idx !== -1) {
978
1100
  this.children.splice(idx, 1);
979
1101
  }
@@ -985,7 +1107,7 @@ var MiniDom;
985
1107
  if (child) {
986
1108
  // remove child from any other parents
987
1109
  child.removeAsChildFromAnyParents();
988
- // add to this node
1110
+ // add to this node
989
1111
  child.parentNode = this;
990
1112
  this.insertBefore(child, null);
991
1113
  }
@@ -1013,11 +1135,15 @@ var MiniDom;
1013
1135
  // TODO: depth-first vs breadth-first
1014
1136
  walk(callback) {
1015
1137
  const queue = [this, ...this.children];
1138
+ const traversed = [];
1016
1139
  let canContinue = true;
1017
1140
  while (queue.length && canContinue) {
1018
1141
  const current = queue.shift();
1019
1142
  if (current) {
1020
- queue.push(...current.children);
1143
+ if (traversed.includes(current))
1144
+ continue;
1145
+ traversed.push(current);
1146
+ queue.push(...current.children.filter((child) => !traversed.includes(child)));
1021
1147
  canContinue = callback(current);
1022
1148
  }
1023
1149
  else {
@@ -1029,7 +1155,7 @@ var MiniDom;
1029
1155
  // ====================
1030
1156
  minidomType;
1031
1157
  removeAsChildFromAnyParents() {
1032
- allNodes.forEach(node => node.removeChild(this));
1158
+ allNodes.forEach((node) => node.removeChild(this));
1033
1159
  }
1034
1160
  }
1035
1161
  MiniDom.BaseNode = BaseNode;
@@ -1053,8 +1179,8 @@ var MiniDom;
1053
1179
  drop() {
1054
1180
  super.drop();
1055
1181
  // handle remove functions
1056
- Object.keys(this.eventListenerRemoveFunctions).forEach(key => {
1057
- this.eventListenerRemoveFunctions[key].forEach(func => func());
1182
+ Object.keys(this.eventListenerRemoveFunctions).forEach((key) => {
1183
+ this.eventListenerRemoveFunctions[key].forEach((func) => func());
1058
1184
  });
1059
1185
  }
1060
1186
  }
@@ -1065,7 +1191,8 @@ var MiniDom;
1065
1191
  class RendererRootNode extends MiniDom.RendererBaseNode {
1066
1192
  constructor(options = {}, parent) {
1067
1193
  super(options, parent);
1068
- this.domElement = options.domElement ?? document.createElement('div');
1194
+ this.domElement =
1195
+ options.domElement ?? document.createElement('div');
1069
1196
  }
1070
1197
  domElement;
1071
1198
  isLunchboxRootNode = true;
@@ -1082,7 +1209,8 @@ var MiniDom;
1082
1209
  class RendererDomNode extends MiniDom.RendererBaseNode {
1083
1210
  constructor(options = {}, parent) {
1084
1211
  super(options, parent);
1085
- this.domElement = options.domElement ?? document.createElement('div');
1212
+ this.domElement =
1213
+ options.domElement ?? document.createElement('div');
1086
1214
  }
1087
1215
  domElement;
1088
1216
  }
@@ -1115,7 +1243,14 @@ let frameID;
1115
1243
  const beforeRender = [];
1116
1244
  const afterRender = [];
1117
1245
  const update = (opts) => {
1118
- frameID = requestAnimationFrame(() => update(opts));
1246
+ // request next frame
1247
+ frameID = requestAnimationFrame(() => update({
1248
+ app: opts.app,
1249
+ renderer: ensureRenderer.value?.instance,
1250
+ scene: ensuredScene.value.instance,
1251
+ camera: ensuredCamera.value?.instance,
1252
+ }));
1253
+ // prep options
1119
1254
  const { app, renderer, scene, camera } = opts;
1120
1255
  // BEFORE RENDER
1121
1256
  beforeRender.forEach((cb) => {
@@ -1125,7 +1260,12 @@ const update = (opts) => {
1125
1260
  });
1126
1261
  // RENDER
1127
1262
  if (renderer && scene && camera) {
1128
- renderer.render(scene, camera);
1263
+ if (app.customRender) {
1264
+ app.customRender(opts);
1265
+ }
1266
+ else {
1267
+ renderer.render(toRaw(scene), toRaw(camera));
1268
+ }
1129
1269
  }
1130
1270
  // AFTER RENDER
1131
1271
  afterRender.forEach((cb) => {
@@ -1133,22 +1273,6 @@ const update = (opts) => {
1133
1273
  cb(opts);
1134
1274
  }
1135
1275
  });
1136
- /*
1137
- frameID = requestAnimationFrame(() => update(renderer, scene, camera))
1138
-
1139
- // Make sure we have all necessary components
1140
- if (!renderer) {
1141
- renderer = ensureRenderer().instance
1142
- }
1143
- if (!scene) {
1144
- scene = ensureScene().instance
1145
- }
1146
- if (!camera) {
1147
- camera = ensureCamera().instance
1148
- }
1149
-
1150
-
1151
- */
1152
1276
  };
1153
1277
  const onBeforeRender = (cb, index = Infinity) => {
1154
1278
  if (index === Infinity) {
@@ -1158,6 +1282,15 @@ const onBeforeRender = (cb, index = Infinity) => {
1158
1282
  beforeRender.splice(index, 0, cb);
1159
1283
  }
1160
1284
  };
1285
+ const offBeforeRender = (cb) => {
1286
+ if (isFinite(cb)) {
1287
+ beforeRender.splice(cb, 1);
1288
+ }
1289
+ else {
1290
+ const idx = beforeRender.findIndex((v) => v == cb);
1291
+ beforeRender.splice(idx, 1);
1292
+ }
1293
+ };
1161
1294
  const onAfterRender = (cb, index = Infinity) => {
1162
1295
  if (index === Infinity) {
1163
1296
  afterRender.push(cb);
@@ -1166,6 +1299,15 @@ const onAfterRender = (cb, index = Infinity) => {
1166
1299
  afterRender.splice(index, 0, cb);
1167
1300
  }
1168
1301
  };
1302
+ const offAfterRender = (cb) => {
1303
+ if (isFinite(cb)) {
1304
+ afterRender.splice(cb, 1);
1305
+ }
1306
+ else {
1307
+ const idx = afterRender.findIndex((v) => v == cb);
1308
+ afterRender.splice(idx, 1);
1309
+ }
1310
+ };
1169
1311
  const cancelUpdate = () => {
1170
1312
  if (frameID)
1171
1313
  cancelAnimationFrame(frameID);
@@ -1271,10 +1413,7 @@ const internalLunchboxVueKeys = [
1271
1413
  'src',
1272
1414
  ];
1273
1415
 
1274
- const autoAttach = [
1275
- 'geometry',
1276
- 'material',
1277
- ];
1416
+ const autoAttach = ['geometry', 'material'];
1278
1417
  const createElement = (type, isSVG, isCustomizedBuiltin, vnodeProps) => {
1279
1418
  const options = { type };
1280
1419
  if (vnodeProps) {
@@ -1289,7 +1428,7 @@ const createElement = (type, isSVG, isCustomizedBuiltin, vnodeProps) => {
1289
1428
  // handle standard node
1290
1429
  const node = createNode(options);
1291
1430
  // autoattach
1292
- autoAttach.forEach(key => {
1431
+ autoAttach.forEach((key) => {
1293
1432
  if (type.toLowerCase().endsWith(key)) {
1294
1433
  node.props.attach = key;
1295
1434
  }
@@ -1329,17 +1468,19 @@ const insert = (child, parent, anchor) => {
1329
1468
  }
1330
1469
  // add to scene if parent is the wrapper node
1331
1470
  if (child.metaType === 'standardMeta' &&
1332
- child.type !== 'Scene' &&
1471
+ child.type !== 'scene' &&
1333
1472
  isLunchboxRootNode(effectiveParent)) {
1334
1473
  // ensure scene exists
1335
- const sceneNode = ensureScene();
1474
+ const sceneNode = ensuredScene.value;
1336
1475
  if (sceneNode.instance && child) {
1337
1476
  sceneNode.addChild(child);
1338
1477
  }
1339
1478
  if (child.instance &&
1340
1479
  child.instance.isObject3D &&
1341
1480
  sceneNode.instance) {
1342
- sceneNode.instance.add(child.instance);
1481
+ if (sceneNode !== child) {
1482
+ sceneNode.instance.add(child.instance);
1483
+ }
1343
1484
  }
1344
1485
  }
1345
1486
  // add to hierarchy otherwise
@@ -1422,19 +1563,29 @@ function attachToParentInstance(child, parent, key, value) {
1422
1563
  const remove = (node) => {
1423
1564
  if (!node)
1424
1565
  return;
1566
+ const overrideKeys = Object.keys(overrides);
1425
1567
  // prep subtree
1426
1568
  const subtree = [];
1427
- node.walk(descendant => {
1569
+ node.walk((descendant) => {
1428
1570
  subtree.push(descendant);
1429
1571
  return true;
1430
1572
  });
1431
1573
  // clean up subtree
1432
1574
  subtree.forEach((n) => {
1575
+ const overrideKey = overrideKeys.find((key) => overrides[key]?.uuid === n.uuid);
1576
+ // if this node is an override, remove it from the overrides list
1577
+ if (overrideKey) {
1578
+ overrides[overrideKey] = null;
1579
+ }
1433
1580
  if (isLunchboxStandardNode(n)) {
1434
1581
  // try to remove three object
1435
1582
  n.instance?.removeFromParent?.();
1436
1583
  // try to dispose three object
1437
- const dispose = n.instance?.dispose;
1584
+ const dispose =
1585
+ // calling `dispose` on a scene triggers an error,
1586
+ // so let's ignore if this node is a scene
1587
+ n.type !== 'scene' &&
1588
+ n.instance?.dispose;
1438
1589
  if (dispose)
1439
1590
  dispose.bind(n.instance)();
1440
1591
  n.instance = null;
@@ -1442,7 +1593,7 @@ const remove = (node) => {
1442
1593
  // drop tree node
1443
1594
  n.drop();
1444
1595
  // remove Lunchbox node from main list
1445
- const idx = allNodes.findIndex(v => v.uuid === n.uuid);
1596
+ const idx = allNodes.findIndex((v) => v.uuid === n.uuid);
1446
1597
  if (idx !== -1) {
1447
1598
  allNodes.splice(idx, 1);
1448
1599
  }
@@ -1509,15 +1660,34 @@ const globals = {
1509
1660
  dpr: ref(1),
1510
1661
  inputActive,
1511
1662
  mousePos,
1512
- camera: createdCamera,
1513
- renderer: createdRenderer,
1514
- scene: createdScene,
1515
1663
  };
1516
- const camera = computed(() => globals.camera.value?.instance);
1517
- const renderer = computed(() => globals.renderer.value?.instance);
1518
- const scene = computed(() => globals.scene.value?.instance);
1664
+ const camera = computed(() => ensuredCamera.value?.instance ?? null);
1665
+ const renderer = computed(() => ensureRenderer.value?.instance ?? null);
1666
+ const scene = computed(() => ensuredScene.value.instance);
1667
+ // CUSTOM RENDER SUPPORT
1668
+ // ====================
1669
+ let app = null;
1670
+ let queuedCustomRenderFunction = null;
1671
+ /** Set a custom render function, overriding the Lunchbox app's default render function.
1672
+ * Changing this requires the user to manually render their scene.
1673
+ */
1674
+ const setCustomRender = (render) => {
1675
+ if (app)
1676
+ app.setCustomRender(render);
1677
+ else
1678
+ queuedCustomRenderFunction = render;
1679
+ };
1680
+ /** Clear the active app's custom render function. */
1681
+ const clearCustomRender = () => {
1682
+ if (app)
1683
+ app.clearCustomRender();
1684
+ else
1685
+ queuedCustomRenderFunction = null;
1686
+ };
1687
+ // CREATE APP
1688
+ // ====================
1519
1689
  const createApp = (root) => {
1520
- const app = createRenderer(nodeOps).createApp(root);
1690
+ app = createRenderer(nodeOps).createApp(root);
1521
1691
  // register all components
1522
1692
  Object.keys(components).forEach((key) => {
1523
1693
  app.component(key, components[key]);
@@ -1540,16 +1710,24 @@ const createApp = (root) => {
1540
1710
  };
1541
1711
  // embed .extend function
1542
1712
  app.extend = (targets) => {
1543
- extend({ app, ...targets });
1713
+ extend({ app: app, ...targets });
1544
1714
  return app;
1545
1715
  };
1546
- // kick update loop
1547
- // app.update = update
1548
- // app.update({
1549
- // app,
1550
- // })
1716
+ // prep for custom render support
1717
+ app.setCustomRender = (newRender) => {
1718
+ app.customRender = newRender;
1719
+ };
1720
+ // add queued custom render if we have one
1721
+ if (queuedCustomRenderFunction) {
1722
+ app.setCustomRender(queuedCustomRenderFunction);
1723
+ queuedCustomRenderFunction = null;
1724
+ }
1725
+ // add custom render removal
1726
+ app.clearCustomRender = () => {
1727
+ app.customRender = null;
1728
+ };
1551
1729
  // done
1552
1730
  return app;
1553
1731
  };
1554
1732
 
1555
- export { camera, createApp, find, globals, lunchboxRootNode as lunchboxTree, onAfterRender, onBeforeRender, renderer, scene, update };
1733
+ export { camera, clearCustomRender, createApp, find, globals, lunchboxRootNode as lunchboxTree, offAfterRender, offBeforeRender, onAfterRender, onBeforeRender, renderer, scene, setCustomRender };