314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
|
|
import * as THREE from 'three';
|
|
import * as React from 'react';
|
|
import { extend, useFrame } from '@react-three/fiber';
|
|
import { setUpdateRange } from '../helpers/deprecated.js';
|
|
|
|
function isFunctionChild(value) {
|
|
return typeof value === 'function';
|
|
}
|
|
const _instanceLocalMatrix = /* @__PURE__ */new THREE.Matrix4();
|
|
const _instanceWorldMatrix = /* @__PURE__ */new THREE.Matrix4();
|
|
const _instanceIntersects = [];
|
|
const _mesh = /* @__PURE__ */new THREE.Mesh();
|
|
class PositionMesh extends THREE.Group {
|
|
constructor() {
|
|
super();
|
|
this.color = new THREE.Color('white');
|
|
this.instance = {
|
|
current: undefined
|
|
};
|
|
this.instanceKey = {
|
|
current: undefined
|
|
};
|
|
}
|
|
|
|
// This will allow the virtual instance have bounds
|
|
get geometry() {
|
|
var _this$instance$curren;
|
|
return (_this$instance$curren = this.instance.current) == null ? void 0 : _this$instance$curren.geometry;
|
|
}
|
|
|
|
// And this will allow the virtual instance to receive events
|
|
raycast(raycaster, intersects) {
|
|
const parent = this.instance.current;
|
|
if (!parent) return;
|
|
if (!parent.geometry || !parent.material) return;
|
|
_mesh.geometry = parent.geometry;
|
|
const matrixWorld = parent.matrixWorld;
|
|
const instanceId = parent.userData.instances.indexOf(this.instanceKey);
|
|
// If the instance wasn't found or exceeds the parents draw range, bail out
|
|
if (instanceId === -1 || instanceId > parent.count) return;
|
|
// calculate the world matrix for each instance
|
|
parent.getMatrixAt(instanceId, _instanceLocalMatrix);
|
|
_instanceWorldMatrix.multiplyMatrices(matrixWorld, _instanceLocalMatrix);
|
|
// the mesh represents this single instance
|
|
_mesh.matrixWorld = _instanceWorldMatrix;
|
|
// raycast side according to instance material
|
|
if (parent.material instanceof THREE.Material) _mesh.material.side = parent.material.side;else _mesh.material.side = parent.material[0].side;
|
|
_mesh.raycast(raycaster, _instanceIntersects);
|
|
// process the result of raycast
|
|
for (let i = 0, l = _instanceIntersects.length; i < l; i++) {
|
|
const intersect = _instanceIntersects[i];
|
|
intersect.instanceId = instanceId;
|
|
intersect.object = this;
|
|
intersects.push(intersect);
|
|
}
|
|
_instanceIntersects.length = 0;
|
|
}
|
|
}
|
|
const globalContext = /* @__PURE__ */React.createContext(null);
|
|
const parentMatrix = /* @__PURE__ */new THREE.Matrix4();
|
|
const instanceMatrix = /* @__PURE__ */new THREE.Matrix4();
|
|
const tempMatrix = /* @__PURE__ */new THREE.Matrix4();
|
|
const translation = /* @__PURE__ */new THREE.Vector3();
|
|
const rotation = /* @__PURE__ */new THREE.Quaternion();
|
|
const scale = /* @__PURE__ */new THREE.Vector3();
|
|
const isInstancedBufferAttribute = attr => attr.isInstancedBufferAttribute;
|
|
const Instance = /* @__PURE__ */React.forwardRef(({
|
|
context,
|
|
children,
|
|
...props
|
|
}, ref) => {
|
|
React.useMemo(() => extend({
|
|
PositionMesh
|
|
}), []);
|
|
const group = React.useRef(null);
|
|
React.useImperativeHandle(ref, () => group.current, []);
|
|
const {
|
|
subscribe,
|
|
getParent
|
|
} = React.useContext(context || globalContext);
|
|
React.useLayoutEffect(() => subscribe(group), []);
|
|
return /*#__PURE__*/React.createElement("positionMesh", _extends({
|
|
instance: getParent(),
|
|
instanceKey: group,
|
|
ref: group
|
|
}, props), children);
|
|
});
|
|
const Instances = /* @__PURE__ */React.forwardRef(({
|
|
context,
|
|
children,
|
|
range,
|
|
limit = 1000,
|
|
frames = Infinity,
|
|
...props
|
|
}, ref) => {
|
|
const [{
|
|
localContext,
|
|
instance
|
|
}] = React.useState(() => {
|
|
const localContext = /*#__PURE__*/React.createContext(null);
|
|
return {
|
|
localContext,
|
|
instance: /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.createElement(Instance, _extends({
|
|
context: localContext
|
|
}, props, {
|
|
ref: ref
|
|
})))
|
|
};
|
|
});
|
|
const parentRef = React.useRef(null);
|
|
React.useImperativeHandle(ref, () => parentRef.current, []);
|
|
const [instances, setInstances] = React.useState([]);
|
|
const [[matrices, colors]] = React.useState(() => {
|
|
const mArray = new Float32Array(limit * 16);
|
|
for (let i = 0; i < limit; i++) tempMatrix.identity().toArray(mArray, i * 16);
|
|
return [mArray, new Float32Array([...new Array(limit * 3)].map(() => 1))];
|
|
});
|
|
React.useEffect(() => {
|
|
// We might be a frame too late? 🤷♂️
|
|
parentRef.current.instanceMatrix.needsUpdate = true;
|
|
});
|
|
let iterations = 0;
|
|
let count = 0;
|
|
const attributes = React.useRef([]);
|
|
React.useLayoutEffect(() => {
|
|
attributes.current = Object.entries(parentRef.current.geometry.attributes).filter(([_name, value]) => isInstancedBufferAttribute(value));
|
|
});
|
|
useFrame(() => {
|
|
if (frames === Infinity || iterations < frames) {
|
|
parentRef.current.updateMatrix();
|
|
parentRef.current.updateMatrixWorld();
|
|
parentMatrix.copy(parentRef.current.matrixWorld).invert();
|
|
count = Math.min(limit, range !== undefined ? range : limit, instances.length);
|
|
parentRef.current.count = count;
|
|
setUpdateRange(parentRef.current.instanceMatrix, {
|
|
start: 0,
|
|
count: count * 16
|
|
});
|
|
setUpdateRange(parentRef.current.instanceColor, {
|
|
start: 0,
|
|
count: count * 3
|
|
});
|
|
for (let i = 0; i < instances.length; i++) {
|
|
const instance = instances[i].current;
|
|
// Multiply the inverse of the InstancedMesh world matrix or else
|
|
// Instances will be double-transformed if <Instances> isn't at identity
|
|
instance.matrixWorld.decompose(translation, rotation, scale);
|
|
instanceMatrix.compose(translation, rotation, scale).premultiply(parentMatrix);
|
|
instanceMatrix.toArray(matrices, i * 16);
|
|
parentRef.current.instanceMatrix.needsUpdate = true;
|
|
instance.color.toArray(colors, i * 3);
|
|
parentRef.current.instanceColor.needsUpdate = true;
|
|
}
|
|
iterations++;
|
|
}
|
|
});
|
|
const api = React.useMemo(() => ({
|
|
getParent: () => parentRef,
|
|
subscribe: ref => {
|
|
setInstances(instances => [...instances, ref]);
|
|
return () => setInstances(instances => instances.filter(item => item.current !== ref.current));
|
|
}
|
|
}), []);
|
|
return /*#__PURE__*/React.createElement("instancedMesh", _extends({
|
|
userData: {
|
|
instances,
|
|
limit,
|
|
frames
|
|
},
|
|
matrixAutoUpdate: false,
|
|
ref: parentRef,
|
|
args: [null, null, 0],
|
|
raycast: () => null
|
|
}, props), /*#__PURE__*/React.createElement("instancedBufferAttribute", {
|
|
attach: "instanceMatrix",
|
|
args: [matrices, 16],
|
|
usage: THREE.DynamicDrawUsage
|
|
}), /*#__PURE__*/React.createElement("instancedBufferAttribute", {
|
|
attach: "instanceColor",
|
|
args: [colors, 3],
|
|
usage: THREE.DynamicDrawUsage
|
|
}), isFunctionChild(children) ? /*#__PURE__*/React.createElement(localContext.Provider, {
|
|
value: api
|
|
}, children(instance)) : context ? /*#__PURE__*/React.createElement(context.Provider, {
|
|
value: api
|
|
}, children) : /*#__PURE__*/React.createElement(globalContext.Provider, {
|
|
value: api
|
|
}, children));
|
|
});
|
|
// TODO: make this non-recursive and type-safe
|
|
const Merged = /* @__PURE__ */React.forwardRef(function Merged({
|
|
meshes,
|
|
children,
|
|
...props
|
|
}, ref) {
|
|
const isArray = Array.isArray(meshes);
|
|
// Filter out meshes from collections, which may contain non-meshes
|
|
// @ts-expect-error
|
|
if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key];
|
|
const render = args => isArray ?
|
|
// @ts-expect-error
|
|
children(...args) : children(
|
|
// @ts-expect-error
|
|
Object.keys(meshes)
|
|
// @ts-expect-error
|
|
.filter(key => meshes[key].isMesh).reduce((acc, key, i) => ({
|
|
...acc,
|
|
[key]: args[i]
|
|
}), {}));
|
|
|
|
// @ts-expect-error
|
|
const components = (isArray ? meshes : Object.values(meshes)).map(({
|
|
geometry,
|
|
material
|
|
}) => /*#__PURE__*/React.createElement(Instances, _extends({
|
|
key: geometry.uuid,
|
|
geometry: geometry,
|
|
material: material
|
|
}, props)));
|
|
return /*#__PURE__*/React.createElement("group", {
|
|
ref: ref
|
|
}, renderRecursive(render, components));
|
|
});
|
|
|
|
// https://github.com/jamesplease/react-composer
|
|
function renderRecursive(render, components, results = []) {
|
|
// Once components is exhausted, we can render out the results array.
|
|
if (!components[0]) {
|
|
return render(results);
|
|
}
|
|
|
|
// Continue recursion for remaining items.
|
|
// results.concat([value]) ensures [...results, value] instead of [...results, ...value]
|
|
function nextRender(value) {
|
|
return renderRecursive(render, components.slice(1), results.concat([value]));
|
|
}
|
|
|
|
// Each props.components entry is either an element or function [element factory]
|
|
return typeof components[0] === 'function' ?
|
|
// When it is a function, produce an element by invoking it with "render component values".
|
|
components[0]({
|
|
results,
|
|
render: nextRender
|
|
}) :
|
|
/*#__PURE__*/
|
|
// When it is an element, enhance the element's props with the render prop.
|
|
React.cloneElement(components[0], {
|
|
children: nextRender
|
|
});
|
|
}
|
|
|
|
/** Idea and implementation for global instances and instanced attributes by
|
|
/* Matias Gonzalez Fernandez https://x.com/matiNotFound
|
|
/* and Paul Henschel https://x.com/0xca0a
|
|
*/
|
|
function createInstances() {
|
|
const context = /*#__PURE__*/React.createContext(null);
|
|
return [/*#__PURE__*/React.forwardRef((props, fref) => /*#__PURE__*/React.createElement(Instances, _extends({
|
|
ref: fref,
|
|
context: context
|
|
}, props))), /*#__PURE__*/React.forwardRef((props, fref) => /*#__PURE__*/React.createElement(Instance, _extends({
|
|
ref: fref,
|
|
context: context
|
|
}, props)))];
|
|
}
|
|
const InstancedAttribute = /*#__PURE__*/React.forwardRef(({
|
|
name,
|
|
defaultValue,
|
|
normalized,
|
|
usage = THREE.DynamicDrawUsage
|
|
}, fref) => {
|
|
const ref = React.useRef(null);
|
|
React.useImperativeHandle(fref, () => ref.current, []);
|
|
React.useLayoutEffect(() => {
|
|
const parent = ref.current.__r3f.parent.object;
|
|
parent.geometry.attributes[name] = ref.current;
|
|
const value = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
|
|
const array = Array.from({
|
|
length: parent.userData.limit
|
|
}, () => value).flat();
|
|
ref.current.array = new Float32Array(array);
|
|
ref.current.itemSize = value.length;
|
|
// @ts-expect-error
|
|
ref.current.count = array.length / ref.current.itemSize;
|
|
return () => {
|
|
delete parent.geometry.attributes[name];
|
|
};
|
|
}, [name]);
|
|
let iterations = 0;
|
|
useFrame(() => {
|
|
const parent = ref.current.__r3f.parent.object;
|
|
if (parent.userData.frames === Infinity || iterations < parent.userData.frames) {
|
|
for (let i = 0; i < parent.userData.instances.length; i++) {
|
|
const instance = parent.userData.instances[i].current;
|
|
const value = instance[name];
|
|
if (value !== undefined) {
|
|
ref.current.set(Array.isArray(value) ? value : typeof value.toArray === 'function' ? value.toArray() : [value], i * ref.current.itemSize);
|
|
ref.current.needsUpdate = true;
|
|
}
|
|
}
|
|
iterations++;
|
|
}
|
|
});
|
|
// @ts-expect-error we're abusing three API here by mutating immutable args
|
|
return /*#__PURE__*/React.createElement("instancedBufferAttribute", {
|
|
ref: ref,
|
|
usage: usage,
|
|
normalized: normalized
|
|
});
|
|
});
|
|
|
|
export { Instance, InstancedAttribute, Instances, Merged, PositionMesh, createInstances };
|