/** * Three constructor catalogue and component-form `extend` support. * * Adapted from React Three Fiber v9.6.1: * https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/reconciler.tsx#L138-L190 */ import * as THREE from 'three'; import { defineUniversalComponent, markUniversalHostComponent, type UniversalComponent, universalPlan, universalProps, universalValue, } from 'octane/universal'; import type { EventHandlers } from './events.js'; import { registerTrustedThreeConstructor } from './constructors.js'; export const THREE_RENDERER_ID = 'three'; export type ConstructorRepresentation = new (...args: any[]) => T; export interface Catalogue { [name: string]: ConstructorRepresentation; } export type Args = T extends ConstructorRepresentation ? T extends typeof THREE.Color ? [r: number, g: number, b: number] | [color: THREE.ColorRepresentation] : ConstructorParameters : unknown[]; type IsOptional = undefined extends T ? true : false; type IsAllOptional = T extends readonly [infer First, ...infer Rest] ? IsOptional extends true ? IsAllOptional : false : true; type ArgsProp = IsAllOptional> extends true ? { args?: Args } : { args: Args }; export type ThreeAttachFunction = (parent: unknown, self: T) => void | (() => void); export type Attach = string | ThreeAttachFunction; /** R3F-compatible name for a function attachment, adapted to Octane cleanup callbacks. */ export type AttachFnType = ThreeAttachFunction; /** R3F-compatible name for the public attachment union. */ export type AttachType = Attach; export type ThreeKey = string | number | symbol | bigint; export type ThreeRef = ((value: T | null) => void | (() => void)) | { current: T | null } | readonly ThreeRef[]; export interface ThreeInstanceProps { attach?: Attach; children?: unknown; dispose?: null; key?: ThreeKey; onUpdate?: (self: T) => void; ref?: ThreeRef; } export interface RaycastableRepresentation { raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]): void; } export type EventProps = T extends RaycastableRepresentation ? EventHandlers : {}; /** R3F-compatible logical instance props, separate from authored JSX props. */ export type InstanceProps = (P extends ConstructorRepresentation ? ArgsProp

: { args: unknown[] }) & { object?: T; dispose?: null; attach?: Attach; onUpdate?: (self: T) => void; }; type NonFunctionKeys = { [K in keyof T]-?: T[K] extends (...args: any[]) => any ? never : K; }[keyof T]; type Properties = Pick>; type Mutable = { -readonly [K in keyof T]: T[K] | Readonly }; type Overwrite = Omit & U; export interface MathRepresentation { set(...args: number[]): unknown; } export interface VectorRepresentation extends MathRepresentation { setScalar(value: number): unknown; } export type MathTypes = MathRepresentation | THREE.Euler | THREE.Color; type MutableOrReadonlyParameters any> = Parameters | Readonly>; export type MathType = T extends THREE.Color ? Args | THREE.ColorRepresentation : T extends VectorRepresentation | THREE.Layers | THREE.Euler ? T | MutableOrReadonlyParameters | number : T | MutableOrReadonlyParameters; export type MathProps = { [K in keyof T as T[K] extends MathTypes ? K : never]: T[K] extends MathTypes ? MathType : never; }; export type Vector2 = MathType; export type Vector3 = MathType; export type Vector4 = MathType; export type Color = MathType; export type Layers = MathType; export type Quaternion = MathType; export type Euler = MathType; export type Matrix3 = MathType; export type Matrix4 = MathType; /** * R3F-compatible authored-node props with Octane refs and renderable children. * The historical name is retained for source compatibility without importing * React or exposing React ownership semantics. */ export interface ReactProps { children?: unknown; ref?: ThreeRef; key?: ThreeKey; } export type ElementProps> = Partial< Overwrite & ReactProps

& EventProps

> >; export type ThreeElement = Mutable< Overwrite< ElementProps>>, ArgsProp & ThreeInstanceProps> & EventProps> > >; export type ThreeToElements> = { [K in keyof T & string as Uncapitalize]: T[K] extends ConstructorRepresentation ? ThreeElement : never; }; export type ThreeToJSXElements> = ThreeToElements; type ThreeNamespaceElements = ThreeToElements; export type PrimitiveProps> = Partial> & ThreeInstanceProps & EventProps & { args?: never; object: T; }; export interface ThreeElements extends Omit< ThreeNamespaceElements, 'audio' | 'source' | 'line' | 'path' > { primitive: PrimitiveProps; threeAudio: ThreeNamespaceElements['audio']; threeSource: ThreeNamespaceElements['source']; threeLine: ThreeNamespaceElements['line']; threePath: ThreeNamespaceElements['path']; } const catalogue: Catalogue = Object.create(null); const constructorComponents = new WeakMap< ConstructorRepresentation, UniversalComponent> >(); let nextExtendedType = 0; let namespaceRegistered = false; const PREFIX_REGEX = /^three(?=[A-Z])/; function toPascalCase(type: string): string { return type.length === 0 ? type : `${type[0].toUpperCase()}${type.slice(1)}`; } /** Register one compiler-selected built-in without replacing an authored extension. */ export function registerThreeIntrinsic(name: string, Constructor: ConstructorRepresentation): void { registerTrustedThreeConstructor(Constructor); if (!Object.hasOwn(catalogue, name)) catalogue[name] = Constructor; } /** Register the complete Three namespace only for Canvas's compatibility surface. */ export function registerThreeNamespace(): void { if (namespaceRegistered) return; namespaceRegistered = true; for (const name of Object.keys(THREE)) { const Constructor = (THREE as unknown as Catalogue)[name]; registerTrustedThreeConstructor(Constructor); if (!Object.hasOwn(catalogue, name)) { catalogue[name] = Constructor; } } } /** Resolve conflicting `threeLine`-style host names without shadowing explicit extensions. */ export function normalizeThreeType(type: string): string { return Object.hasOwn(catalogue, toPascalCase(type)) ? type : type.replace(PREFIX_REGEX, ''); } export function resolveThreeConstructor(type: string): ConstructorRepresentation | null { const normalized = normalizeThreeType(type); return catalogue[toPascalCase(normalized)] ?? null; } export function validateThreeInstance( type: string, props: Readonly>, ): string { const normalized = normalizeThreeType(type); const name = toPascalCase(normalized); if (normalized !== 'primitive' && catalogue[name] === undefined) { throw new Error( `@octanejs/three: ${name} is not in the Three catalogue. Call extend({ ${name} }) before rendering it.`, ); } if (normalized === 'primitive' && !props.object) { throw new Error('@octanejs/three: Primitives without an object are invalid.'); } if (props.args !== undefined && !Array.isArray(props.args)) { throw new Error('@octanejs/three: The args prop must be an array.'); } return normalized; } export function createThreeObject( type: string, props: Readonly>, ): { object: any; owned: boolean; type: string } { const normalized = validateThreeInstance(type, props); if (normalized === 'primitive') { return { object: props.object, owned: false, type: normalized }; } const Constructor = resolveThreeConstructor(normalized)!; return { object: new Constructor(...((props.args as readonly unknown[] | undefined) ?? [])), owned: true, type: normalized, }; } export function extend( objects: T, ): UniversalComponent>; export function extend(objects: T): void; export function extend( objects: T, ): UniversalComponent | void { if (typeof objects !== 'function') { Object.assign(catalogue, objects); return; } const existing = constructorComponents.get(objects); if (existing !== undefined) return existing as UniversalComponent; // Unlike R3F's bare numeric token, the branded private host name remains // unambiguous in compiler output and diagnostics. const type = `octaneThreeExtended${nextExtendedType++}`; catalogue[toPascalCase(type)] = objects; const plan = universalPlan(THREE_RENDERER_ID, { kind: 'host', type, propsSlot: 0 }); const component = defineUniversalComponent>( THREE_RENDERER_ID, (props) => universalValue(plan, [universalProps([['spread', props]])]), { module: '@octanejs/three' }, ); markUniversalHostComponent(component, THREE_RENDERER_ID, plan); constructorComponents.set(objects, component); return component as UniversalComponent; } /** Test/evidence helper: return a snapshot without exposing mutable catalogue state. */ export function getThreeCatalogue(): Readonly { return Object.freeze({ ...catalogue }); }