UNPKG

react-ogl

Version:

A barebones react renderer for OGL.

356 lines (355 loc) 12.8 kB
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