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