@octanejs/three 0.1.0
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/LICENSE +49 -0
- package/README.md +215 -0
- package/UPSTREAM.md +58 -0
- package/package.json +77 -0
- package/src/config.ts +50 -0
- package/src/core/attach.ts +117 -0
- package/src/core/catalogue.ts +245 -0
- package/src/core/driver.ts +1609 -0
- package/src/core/events.ts +539 -0
- package/src/core/hooks.ts +203 -0
- package/src/core/index.ts +86 -0
- package/src/core/loader.ts +210 -0
- package/src/core/loop.ts +210 -0
- package/src/core/portal.ts +236 -0
- package/src/core/props.ts +645 -0
- package/src/core/root.ts +652 -0
- package/src/core/store.ts +741 -0
- package/src/index.ts +86 -0
- package/src/intrinsics.ts +18 -0
- package/src/renderer.ts +12 -0
- package/src/testing.ts +259 -0
- package/src/web/Canvas.tsrx +295 -0
- package/src/web/Canvas.tsrx.d.ts +23 -0
- package/src/web/events.ts +79 -0
- package/src/web/measure.ts +96 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three constructor catalogue and component-form `extend` support.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from React Three Fiber v9.6.1:
|
|
5
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/reconciler.tsx#L138-L190
|
|
6
|
+
*/
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import {
|
|
9
|
+
defineUniversalComponent,
|
|
10
|
+
type UniversalComponent,
|
|
11
|
+
universalPlan,
|
|
12
|
+
universalProps,
|
|
13
|
+
universalValue,
|
|
14
|
+
} from 'octane/universal';
|
|
15
|
+
import type { EventHandlers } from './events.js';
|
|
16
|
+
|
|
17
|
+
export const THREE_RENDERER_ID = 'three';
|
|
18
|
+
|
|
19
|
+
export type ConstructorRepresentation<T = unknown> = new (...args: any[]) => T;
|
|
20
|
+
|
|
21
|
+
export interface Catalogue {
|
|
22
|
+
[name: string]: ConstructorRepresentation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Args<T> = T extends ConstructorRepresentation
|
|
26
|
+
? T extends typeof THREE.Color
|
|
27
|
+
? [r: number, g: number, b: number] | [color: THREE.ColorRepresentation]
|
|
28
|
+
: ConstructorParameters<T>
|
|
29
|
+
: unknown[];
|
|
30
|
+
|
|
31
|
+
type IsOptional<T> = undefined extends T ? true : false;
|
|
32
|
+
type IsAllOptional<T extends readonly unknown[]> = T extends readonly [infer First, ...infer Rest]
|
|
33
|
+
? IsOptional<First> extends true
|
|
34
|
+
? IsAllOptional<Rest>
|
|
35
|
+
: false
|
|
36
|
+
: true;
|
|
37
|
+
|
|
38
|
+
type ArgsProp<T extends ConstructorRepresentation> =
|
|
39
|
+
IsAllOptional<ConstructorParameters<T>> extends true ? { args?: Args<T> } : { args: Args<T> };
|
|
40
|
+
|
|
41
|
+
export type ThreeAttachFunction<T = unknown> = (parent: unknown, self: T) => void | (() => void);
|
|
42
|
+
|
|
43
|
+
export type Attach<T = unknown> = string | ThreeAttachFunction<T>;
|
|
44
|
+
export type ThreeKey = string | number | symbol | bigint;
|
|
45
|
+
|
|
46
|
+
export type ThreeRef<T> =
|
|
47
|
+
| ((value: T | null) => void | (() => void))
|
|
48
|
+
| { current: T | null }
|
|
49
|
+
| readonly ThreeRef<T>[];
|
|
50
|
+
|
|
51
|
+
export interface ThreeInstanceProps<T = unknown> {
|
|
52
|
+
attach?: Attach<T>;
|
|
53
|
+
children?: unknown;
|
|
54
|
+
dispose?: null;
|
|
55
|
+
key?: ThreeKey;
|
|
56
|
+
onUpdate?: (self: T) => void;
|
|
57
|
+
ref?: ThreeRef<T>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RaycastableRepresentation {
|
|
61
|
+
raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type EventProps<T> = T extends RaycastableRepresentation ? EventHandlers : {};
|
|
65
|
+
|
|
66
|
+
/** R3F-compatible logical instance props, separate from authored JSX props. */
|
|
67
|
+
export type InstanceProps<T = any, P = any> = (P extends ConstructorRepresentation
|
|
68
|
+
? ArgsProp<P>
|
|
69
|
+
: { args: unknown[] }) & {
|
|
70
|
+
object?: T;
|
|
71
|
+
dispose?: null;
|
|
72
|
+
attach?: Attach<T>;
|
|
73
|
+
onUpdate?: (self: T) => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type NonFunctionKeys<T> = {
|
|
77
|
+
[K in keyof T]-?: T[K] extends (...args: any[]) => any ? never : K;
|
|
78
|
+
}[keyof T];
|
|
79
|
+
|
|
80
|
+
type Properties<T> = Pick<T, NonFunctionKeys<T>>;
|
|
81
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] | Readonly<T[K]> };
|
|
82
|
+
type Overwrite<T, U> = Omit<T, keyof U> & U;
|
|
83
|
+
|
|
84
|
+
export interface MathRepresentation {
|
|
85
|
+
set(...args: number[]): unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface VectorRepresentation extends MathRepresentation {
|
|
89
|
+
setScalar(value: number): unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type MathTypes = MathRepresentation | THREE.Euler | THREE.Color;
|
|
93
|
+
type MutableOrReadonlyParameters<T extends (...args: any[]) => any> =
|
|
94
|
+
| Parameters<T>
|
|
95
|
+
| Readonly<Parameters<T>>;
|
|
96
|
+
|
|
97
|
+
export type MathType<T extends MathTypes> = T extends THREE.Color
|
|
98
|
+
? Args<typeof THREE.Color> | THREE.ColorRepresentation
|
|
99
|
+
: T extends VectorRepresentation | THREE.Layers | THREE.Euler
|
|
100
|
+
? T | MutableOrReadonlyParameters<T['set']> | number
|
|
101
|
+
: T | MutableOrReadonlyParameters<T['set']>;
|
|
102
|
+
|
|
103
|
+
type MathProps<T> = {
|
|
104
|
+
[K in keyof T as T[K] extends MathTypes ? K : never]: T[K] extends MathTypes
|
|
105
|
+
? MathType<T[K]>
|
|
106
|
+
: never;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type ThreeElement<T extends ConstructorRepresentation> = Mutable<
|
|
110
|
+
Overwrite<
|
|
111
|
+
Partial<Overwrite<Properties<InstanceType<T>>, MathProps<InstanceType<T>>>>,
|
|
112
|
+
ArgsProp<T> & ThreeInstanceProps<InstanceType<T>> & EventProps<InstanceType<T>>
|
|
113
|
+
>
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
export type ThreeToElements<T extends Record<string, unknown>> = {
|
|
117
|
+
[K in keyof T & string as Uncapitalize<K>]: T[K] extends ConstructorRepresentation
|
|
118
|
+
? ThreeElement<T[K]>
|
|
119
|
+
: never;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type ThreeToJSXElements<T extends Record<string, unknown>> = ThreeToElements<T>;
|
|
123
|
+
|
|
124
|
+
type ThreeNamespaceElements = ThreeToElements<typeof THREE>;
|
|
125
|
+
|
|
126
|
+
export type PrimitiveProps<T extends object = Record<string, unknown>> = Partial<Properties<T>> &
|
|
127
|
+
ThreeInstanceProps<T> &
|
|
128
|
+
EventProps<T> & {
|
|
129
|
+
args?: never;
|
|
130
|
+
object: T;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export interface ThreeElements extends Omit<
|
|
134
|
+
ThreeNamespaceElements,
|
|
135
|
+
'audio' | 'source' | 'line' | 'path'
|
|
136
|
+
> {
|
|
137
|
+
primitive: PrimitiveProps<any>;
|
|
138
|
+
threeAudio: ThreeNamespaceElements['audio'];
|
|
139
|
+
threeSource: ThreeNamespaceElements['source'];
|
|
140
|
+
threeLine: ThreeNamespaceElements['line'];
|
|
141
|
+
threePath: ThreeNamespaceElements['path'];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const catalogue: Catalogue = Object.create(null);
|
|
145
|
+
const constructorComponents = new WeakMap<
|
|
146
|
+
ConstructorRepresentation,
|
|
147
|
+
UniversalComponent<Record<string, unknown>>
|
|
148
|
+
>();
|
|
149
|
+
let nextExtendedType = 0;
|
|
150
|
+
let namespaceRegistered = false;
|
|
151
|
+
|
|
152
|
+
const PREFIX_REGEX = /^three(?=[A-Z])/;
|
|
153
|
+
|
|
154
|
+
function toPascalCase(type: string): string {
|
|
155
|
+
return type.length === 0 ? type : `${type[0].toUpperCase()}${type.slice(1)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Register the built-in Three namespace once, at driver creation time. */
|
|
159
|
+
export function registerThreeNamespace(): void {
|
|
160
|
+
if (namespaceRegistered) return;
|
|
161
|
+
namespaceRegistered = true;
|
|
162
|
+
Object.assign(catalogue, THREE);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Resolve conflicting `threeLine`-style host names without shadowing explicit extensions. */
|
|
166
|
+
export function normalizeThreeType(type: string): string {
|
|
167
|
+
return Object.hasOwn(catalogue, toPascalCase(type)) ? type : type.replace(PREFIX_REGEX, '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveThreeConstructor(type: string): ConstructorRepresentation | null {
|
|
171
|
+
const normalized = normalizeThreeType(type);
|
|
172
|
+
return catalogue[toPascalCase(normalized)] ?? null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateThreeInstance(
|
|
176
|
+
type: string,
|
|
177
|
+
props: Readonly<Record<string, unknown>>,
|
|
178
|
+
): string {
|
|
179
|
+
const normalized = normalizeThreeType(type);
|
|
180
|
+
const name = toPascalCase(normalized);
|
|
181
|
+
if (normalized !== 'primitive' && catalogue[name] === undefined) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`@octanejs/three: ${name} is not in the Three catalogue. Call extend({ ${name} }) before rendering it.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (normalized === 'primitive' && !props.object) {
|
|
187
|
+
throw new Error('@octanejs/three: Primitives without an object are invalid.');
|
|
188
|
+
}
|
|
189
|
+
if (props.args !== undefined && !Array.isArray(props.args)) {
|
|
190
|
+
throw new Error('@octanejs/three: The args prop must be an array.');
|
|
191
|
+
}
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createThreeObject(
|
|
196
|
+
type: string,
|
|
197
|
+
props: Readonly<Record<string, unknown>>,
|
|
198
|
+
): { object: any; owned: boolean; type: string } {
|
|
199
|
+
const normalized = validateThreeInstance(type, props);
|
|
200
|
+
if (normalized === 'primitive') {
|
|
201
|
+
return { object: props.object, owned: false, type: normalized };
|
|
202
|
+
}
|
|
203
|
+
const Constructor = resolveThreeConstructor(normalized)!;
|
|
204
|
+
return {
|
|
205
|
+
object: new Constructor(...((props.args as readonly unknown[] | undefined) ?? [])),
|
|
206
|
+
owned: true,
|
|
207
|
+
type: normalized,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function extend<T extends ConstructorRepresentation>(
|
|
212
|
+
objects: T,
|
|
213
|
+
): UniversalComponent<ThreeElement<T>>;
|
|
214
|
+
export function extend<T extends Catalogue>(objects: T): void;
|
|
215
|
+
export function extend<T extends Catalogue | ConstructorRepresentation>(
|
|
216
|
+
objects: T,
|
|
217
|
+
): UniversalComponent<any> | void {
|
|
218
|
+
// Seed built-ins before user entries so an explicit extension can override a
|
|
219
|
+
// catalogue name deterministically, independent of driver creation order.
|
|
220
|
+
registerThreeNamespace();
|
|
221
|
+
if (typeof objects !== 'function') {
|
|
222
|
+
Object.assign(catalogue, objects);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const existing = constructorComponents.get(objects);
|
|
226
|
+
if (existing !== undefined) return existing as UniversalComponent<any>;
|
|
227
|
+
|
|
228
|
+
// Unlike R3F's bare numeric token, the branded private host name remains
|
|
229
|
+
// unambiguous in compiler output and diagnostics.
|
|
230
|
+
const type = `octaneThreeExtended${nextExtendedType++}`;
|
|
231
|
+
catalogue[toPascalCase(type)] = objects;
|
|
232
|
+
const plan = universalPlan(THREE_RENDERER_ID, { kind: 'host', type, propsSlot: 0 });
|
|
233
|
+
const component = defineUniversalComponent<Record<string, unknown>>(
|
|
234
|
+
THREE_RENDERER_ID,
|
|
235
|
+
(props) => universalValue(plan, [universalProps([['spread', props]])]),
|
|
236
|
+
{ module: '@octanejs/three' },
|
|
237
|
+
);
|
|
238
|
+
constructorComponents.set(objects, component);
|
|
239
|
+
return component as UniversalComponent<any>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Test/evidence helper: return a snapshot without exposing mutable catalogue state. */
|
|
243
|
+
export function getThreeCatalogue(): Readonly<Catalogue> {
|
|
244
|
+
return Object.freeze({ ...catalogue });
|
|
245
|
+
}
|