react-ogl
Version:
A barebones react renderer for OGL.
356 lines (355 loc) • 12.8 kB
JavaScript
import Reconciler from "react-reconciler";
import { DefaultEventPriority, ContinuousEventPriority, DiscreteEventPriority } from "react-reconciler/constants.js";
import { unstable_scheduleCallback, unstable_IdlePriority } from "scheduler";
import * as OGL from "ogl";
import * as React from "react";
import { toPascalCase, classExtends, applyProps, attach, prepare, detach } from "./utils.mjs";
import { RESERVED_PROPS } from "./constants.mjs";
const __DEV__ = /* @__PURE__ */ (() => typeof process !== "undefined" && process.env.NODE_ENV !== "production")();
function createReconciler(config) {
const reconciler2 = Reconciler(config);
reconciler2.injectIntoDevTools({
bundleType: __DEV__ ? 1 : 0,
rendererPackageName: "react-ogl",
version: React.version
});
return reconciler2;
}
const NoEventPriority = 0;
const catalogue = { ...OGL };
const catalogueGL = [
// Core
OGL.Camera,
OGL.Geometry,
OGL.Mesh,
OGL.Program,
OGL.RenderTarget,
OGL.Texture,
// Extras
OGL.Flowmap,
OGL.GPGPU,
OGL.NormalProgram,
OGL.Polyline,
OGL.Post,
OGL.Shadow,
OGL.AxesHelper,
OGL.GridHelper,
OGL.WireMesh
];
function extend(objects, gl = false) {
for (const key in objects) {
const value = objects[key];
catalogue[key] = value;
if (gl) catalogueGL.push(value);
}
}
function handleContainerEffects(parent, child) {
var _a;
const state = child.root.getState();
if (!parent.parent && parent.object !== state.scene) return;
if (child.type !== "primitive") {
const name = toPascalCase(child.type);
const target = catalogue[name];
const { args = [], ...props } = child.props;
const isGLInstance = Object.values(catalogueGL).some((elem) => classExtends(elem, target));
if (isGLInstance) {
const { gl } = child.root.getState();
const filtered = args.filter((arg) => arg !== gl);
if (child.type === "program" || child.type === "geometry") {
const attrs = Object.entries(props).reduce((acc, [key, value]) => {
if (child.type === "geometry" && !(value == null ? void 0 : value.data)) return acc;
if (!key.includes("-")) acc[key] = value;
return acc;
}, (_a = filtered[0]) != null ? _a : {});
child.object = new target(gl, attrs);
} else {
child.object = new target(gl, ...filtered);
}
} else {
child.object = new target(...args);
}
}
child.object.__ogl = child;
if (!child.props.attach) {
if (child.object instanceof OGL.Geometry) child.props.attach = "geometry";
else if (child.object instanceof OGL.Program) child.props.attach = "program";
}
applyProps(child.object, child.props);
if (child.props.attach) {
attach(parent, child);
} else if (child.object instanceof OGL.Transform && parent.object instanceof OGL.Transform) {
child.object.setParent(parent.object);
}
for (const childInstance of child.children) handleContainerEffects(child, childInstance);
}
function createInstance(type, props, root) {
const name = toPascalCase(type);
const target = catalogue[name];
if (type !== "primitive" && !target) throw `${type} is not a part of the OGL catalogue! Did you forget to extend?`;
if (type === "primitive" && !props.object) throw `"object" must be set when using primitives.`;
const instance = prepare(props.object, root, type, props);
return instance;
}
const appendChild = (parent, child) => {
child.parent = parent;
parent.children.push(child);
handleContainerEffects(parent, child);
};
function removeChild(parent, child, dispose, recursive) {
child.parent = null;
if (recursive === void 0) {
const childIndex = parent.children.indexOf(child);
if (childIndex !== -1) parent.children.splice(childIndex, 1);
}
if (child.props.attach) {
detach(parent, child);
} else if (parent.object instanceof OGL.Transform && child.object instanceof OGL.Transform) {
parent.object.removeChild(child.object);
}
const shouldDispose = child.props.dispose !== null && dispose !== false;
if (recursive !== false) {
for (const node of child.children) removeChild(child, node, shouldDispose, true);
child.children = [];
}
if (shouldDispose) {
const object = child.object;
unstable_scheduleCallback(unstable_IdlePriority, () => {
var _a;
return (_a = object.dispose) == null ? void 0 : _a.call(object);
});
delete child.object.__ogl;
child.object = null;
}
}
function insertBefore(parent, child, beforeChild, replace = false) {
if (!child) return;
child.parent = parent;
const childIndex = parent.children.indexOf(beforeChild);
if (childIndex !== -1) parent.children.splice(childIndex, replace ? 1 : 0, child);
if (replace) beforeChild.parent = null;
handleContainerEffects(parent, child);
}
function switchInstance(oldInstance, type, props, fiber) {
oldInstance.object.visible = true;
const newInstance = createInstance(type, props, oldInstance.root);
for (const child of oldInstance.children) {
removeChild(oldInstance, child, false, false);
appendChild(newInstance, child);
}
oldInstance.children = [];
const parent = oldInstance.parent;
if (parent) {
insertBefore(parent, newInstance, oldInstance, true);
}
;
[fiber, fiber.alternate].forEach((fiber2) => {
if (fiber2 !== null) {
fiber2.stateNode = newInstance;
if (fiber2.ref) {
if (typeof fiber2.ref === "function") fiber2.ref(newInstance.object);
else fiber2.ref.current = newInstance.object;
}
}
});
return newInstance;
}
function checkShallow(a, b) {
if (Array.isArray(a)) {
if (!Array.isArray(b)) return false;
if (a == b) return true;
if (a.every((v, i) => v === b[i])) return true;
}
if (a === b) return true;
return false;
}
function diffProps(instance, newProps, oldProps) {
const changedProps = {};
for (const key in newProps) {
if (RESERVED_PROPS.includes(key)) continue;
if (instance.type === "primitive" && key === "object") continue;
if (checkShallow(newProps[key], oldProps[key])) continue;
changedProps[key] = newProps[key];
}
return changedProps;
}
const NO_CONTEXT = {};
let currentUpdatePriority = NoEventPriority;
const reconciler = /* @__PURE__ */ createReconciler({
// Configure renderer for tree-like mutation and interop w/react-dom
isPrimaryRenderer: false,
supportsMutation: true,
supportsHydration: false,
supportsPersistence: false,
// Add SSR time fallbacks
scheduleTimeout: () => typeof setTimeout !== "undefined" ? setTimeout : void 0,
cancelTimeout: () => typeof clearTimeout !== "undefined" ? clearTimeout : void 0,
noTimeout: -1,
// Text isn't supported so we skip it
shouldSetTextContent: () => false,
resetTextContent: () => {
},
createTextInstance() {
throw new Error("Text is not allowed in the OGL scene-graph!");
},
hideTextInstance() {
throw new Error("Text is not allowed in the OGL scene-graph!");
},
unhideTextInstance: () => {
},
// Modifies the ref to return the instance object itself.
getPublicInstance: (instance) => instance.object,
// We can optionally access different host contexts on instance creation/update.
// Instances' data structures are self-sufficient, so we don't make use of this
getRootHostContext: () => NO_CONTEXT,
getChildHostContext: () => NO_CONTEXT,
// We can optionally mutate portal containers here, but we do that in createPortal instead from state
preparePortalMount: (container) => prepare(container.getState().scene, container, "", {}),
// This lets us store stuff at the container-level before/after React mutates our OGL elements.
// Elements are mutated in isolation, so we don't do anything here.
prepareForCommit: () => null,
resetAfterCommit: () => {
},
// This can modify the container and clear children.
// Might be useful for disposing on demand later
clearContainer: () => false,
// This creates a OGL element from a React element
createInstance,
// These methods add elements to the scene
appendChild,
appendInitialChild: appendChild,
appendChildToContainer(container, child) {
const scene = container.getState().scene.__ogl;
if (!child || !scene) return;
appendChild(scene, child);
},
// We can specify an order for children to be inserted here.
// This is useful if you want to override stuff like materials
insertBefore,
insertInContainerBefore(container, child, beforeChild) {
const scene = container.getState().scene.__ogl;
if (!child || !beforeChild || !scene) return;
insertBefore(scene, child, beforeChild);
},
// These methods remove elements from the scene
removeChild,
removeChildFromContainer(container, child) {
const scene = container.getState().scene.__ogl;
if (!child || !scene) return;
removeChild(scene, child);
},
// This is where we mutate OGL elements in the render phase
// @ts-ignore
commitUpdate(instance, type, oldProps, newProps, fiber) {
var _a, _b, _c, _d, _e;
let reconstruct = false;
if (instance.type === "primitive" && oldProps.object !== newProps.object) reconstruct = true;
else if (type === "program") {
if (oldProps.vertex !== newProps.vertex) reconstruct = true;
if (oldProps.fragment !== newProps.fragment) reconstruct = true;
} else if (type === "geometry") {
for (const key in oldProps) {
const isAttribute = ((_a = oldProps[key]) == null ? void 0 : _a.data) || ((_b = newProps[key]) == null ? void 0 : _b.data);
if (isAttribute && oldProps[key] !== newProps[key]) {
reconstruct = true;
break;
}
}
} else if (((_c = newProps.args) == null ? void 0 : _c.length) !== ((_d = oldProps.args) == null ? void 0 : _d.length)) reconstruct = true;
else if ((_e = newProps.args) == null ? void 0 : _e.some((value, index) => {
var _a2;
return value !== ((_a2 = oldProps.args) == null ? void 0 : _a2[index]);
})) reconstruct = true;
if (reconstruct) return switchInstance(instance, type, newProps, fiber);
const changedProps = diffProps(instance, newProps, oldProps);
if (Object.keys(changedProps).length) {
if (changedProps == null ? void 0 : changedProps.attach) {
if (oldProps.attach) detach(instance.parent, instance);
instance.props.attach = newProps.attach;
if (newProps.attach) attach(instance.parent, instance);
}
Object.assign(instance.props, changedProps);
applyProps(instance.object, changedProps);
}
},
// Methods to toggle instance visibility on demand.
// React uses this with React.Suspense to display fallback content
hideInstance(instance) {
if (instance.object instanceof OGL.Transform) {
instance.object.visible = false;
}
instance.isHidden = true;
},
unhideInstance(instance) {
if (instance.isHidden && instance.object instanceof OGL.Transform && instance.props.visible !== false) {
instance.object.visible = true;
}
instance.isHidden = false;
},
// Configures a callback once the tree is finalized after commit-effects are fired
finalizeInitialChildren: () => false,
commitMount() {
},
// Undocumented
getInstanceFromNode: () => null,
beforeActiveInstanceBlur() {
},
afterActiveInstanceBlur() {
},
detachDeletedInstance() {
},
prepareScopeUpdate() {
},
getInstanceFromScope: () => null,
shouldAttemptEagerTransition: () => false,
trackSchedulerEvent: () => {
},
resolveEventType: () => null,
resolveEventTimeStamp: () => -1.1,
requestPostPaintCallback() {
},
maySuspendCommit: () => false,
preloadInstance: () => true,
// true indicates already loaded
startSuspendingCommit() {
},
suspendInstance() {
},
waitForCommitToBeReady: () => null,
NotPendingTransition: null,
HostTransitionContext: /* @__PURE__ */ React.createContext(null),
setCurrentUpdatePriority(newPriority) {
currentUpdatePriority = newPriority;
},
getCurrentUpdatePriority() {
return currentUpdatePriority;
},
resolveUpdatePriority() {
var _a;
if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
switch (typeof window !== "undefined" && ((_a = window.event) == null ? void 0 : _a.type)) {
case "click":
case "contextmenu":
case "dblclick":
case "pointercancel":
case "pointerdown":
case "pointerup":
return DiscreteEventPriority;
case "pointermove":
case "pointerout":
case "pointerover":
case "pointerenter":
case "pointerleave":
case "wheel":
return ContinuousEventPriority;
default:
return DefaultEventPriority;
}
},
resetFormInstance() {
}
});
export {
extend,
reconciler
};
//# sourceMappingURL=reconciler.mjs.map