@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
package/LICENSE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Octane Three binding
|
|
2
|
+
====================
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
25
|
+
|
|
26
|
+
React Three Fiber-derived algorithms and types
|
|
27
|
+
===============================================
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2019-2025 Poimandres
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# `@octanejs/three`
|
|
2
|
+
|
|
3
|
+
An experimental React Three Fiber 9-compatible web renderer for Octane. Octane
|
|
4
|
+
keeps ownership of component execution, hooks, context, Suspense, refs, and
|
|
5
|
+
effects; this package supplies the Three-specific host layer.
|
|
6
|
+
|
|
7
|
+
Milestones 0, 2, 3, 4, 5, and 6 are implemented on top of Octane's renderer SDK
|
|
8
|
+
foundation. The current technical preview includes:
|
|
9
|
+
|
|
10
|
+
- the serializable compiler preset, renderer entry point, renderer-local Three
|
|
11
|
+
intrinsic types, and pinned upstream export/test crosswalk;
|
|
12
|
+
- the Three constructor catalogue, object and constructor-component `extend`
|
|
13
|
+
forms, `primitive`, and constructor `args`;
|
|
14
|
+
- real Three host objects with prop diff/application, automatic, string, and
|
|
15
|
+
function attachments, ordered placement and moves, reconstruction, retained
|
|
16
|
+
visibility, lifecycle/ref delivery, and ownership-aware disposal; and
|
|
17
|
+
- the DOM `Canvas` boundary, programmatic roots, promise-returning renderer
|
|
18
|
+
configuration, the callable root store, camera/scene/raycaster setup,
|
|
19
|
+
resize/DPR/viewport state, shadows and color configuration;
|
|
20
|
+
- one shared `always`/`demand`/`never` frame loop, global frame effects,
|
|
21
|
+
`useStore`, `useThree`, `useFrame`, `useGraph`, managed-instance helpers, and
|
|
22
|
+
the deterministic `@octanejs/three/testing` harness;
|
|
23
|
+
- R3F-compatible ray and pointer events, including 3D bubbling, hit ordering,
|
|
24
|
+
propagation, hover transitions, missed clicks, pointer capture, custom event
|
|
25
|
+
managers, external DOM sources, and coordinate prefixes;
|
|
26
|
+
- a Suspense-aware `useLoader` cache with constructor and instance loaders,
|
|
27
|
+
scalar and array inputs, extensions, progress, GLTF graph augmentation,
|
|
28
|
+
`preload`, `clear`, and cached error routing;
|
|
29
|
+
- retained Three Suspense and Activity visibility, ownership-safe teardown, and
|
|
30
|
+
client-side Three-to-DOM pending/error projection;
|
|
31
|
+
- same-renderer `createPortal` placement into borrowed `Object3D` targets,
|
|
32
|
+
R3F-shaped state/event enclaves, nested context retention, one shared frame
|
|
33
|
+
loop, physical Three event bubbling, and root-scoped target teardown; and
|
|
34
|
+
- public behavior, prepared-driver, and same-source compiled scene evidence
|
|
35
|
+
against R3F 9.6.1 with the exact Three r172 oracle.
|
|
36
|
+
|
|
37
|
+
Full Canvas SSR/hydration adoption, XR, OffscreenCanvas lifecycle, and live HMR
|
|
38
|
+
behavior follow in later milestones.
|
|
39
|
+
|
|
40
|
+
Three deliberate correctness fixes differ from R3F 9.6.1:
|
|
41
|
+
|
|
42
|
+
- removing a pierced prop such as `material-color` resets the nested material
|
|
43
|
+
property, instead of writing the default to a same-named leaf on the root
|
|
44
|
+
object; and
|
|
45
|
+
- reconstructing a captured or hovered object rewrites every stored
|
|
46
|
+
intersection to the replacement, so subsequent captured events reach the
|
|
47
|
+
live handler instead of retaining the retired object; and
|
|
48
|
+
- retained Activity subtrees are excluded from recursive raycasts while
|
|
49
|
+
hidden, rather than allowing an interactive visible ancestor to pierce an
|
|
50
|
+
invisible descendant.
|
|
51
|
+
|
|
52
|
+
## Compiler configuration
|
|
53
|
+
|
|
54
|
+
DOM remains the application renderer. Scene modules opt into Three through the
|
|
55
|
+
shared renderer preset and the `*.three.tsrx` convention:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { defineConfig } from '@octanejs/vite-plugin';
|
|
59
|
+
import { threeRenderers } from '@octanejs/three/config';
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
compiler: {
|
|
63
|
+
renderers: threeRenderers,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The same serializable `threeRenderers` value can be supplied to the Rsbuild and
|
|
69
|
+
low-level Rspack integrations. Vite and Rsbuild additionally own application
|
|
70
|
+
SSR and hydration; the Rspack plugin owns compilation and HMR only.
|
|
71
|
+
|
|
72
|
+
The preset selects `@octanejs/three/renderer`, keeps Three scene modules
|
|
73
|
+
client-only on the server, ignores authored text inside scenes, exposes a
|
|
74
|
+
renderer-local intrinsic catalogue without merging Three tags into DOM JSX,
|
|
75
|
+
and declares `Canvas.children` as the DOM-to-Three renderer boundary.
|
|
76
|
+
|
|
77
|
+
## Canvas and scene modules
|
|
78
|
+
|
|
79
|
+
The application remains a normal DOM Octane app. Only scene modules use the
|
|
80
|
+
`.three.tsrx` convention:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// App.tsrx
|
|
84
|
+
import { Canvas } from '@octanejs/three';
|
|
85
|
+
import { Scene } from './Scene.three.tsrx';
|
|
86
|
+
|
|
87
|
+
export function App() @{
|
|
88
|
+
<Canvas frameloop="demand">
|
|
89
|
+
<Scene />
|
|
90
|
+
</Canvas>
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Scene.three.tsrx
|
|
96
|
+
import { useFrame } from '@octanejs/three';
|
|
97
|
+
import { useRef } from '@octanejs/three/renderer';
|
|
98
|
+
|
|
99
|
+
export function Scene() @{
|
|
100
|
+
const mesh = useRef(null);
|
|
101
|
+
useFrame((_state, delta) => (mesh.current.rotation.x += delta));
|
|
102
|
+
<mesh ref={mesh}>
|
|
103
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
104
|
+
<meshBasicMaterial color="hotpink" />
|
|
105
|
+
</mesh>
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The low-level API follows Octane's component-plus-props root convention. Both
|
|
110
|
+
synchronous and asynchronous renderer factories settle before the component
|
|
111
|
+
can execute:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createRoot } from '@octanejs/three';
|
|
115
|
+
|
|
116
|
+
const root = createRoot(canvas);
|
|
117
|
+
await root.configure({ frameloop: 'never', dpr: 1 });
|
|
118
|
+
root.render(Scene, { color: 'hotpink' });
|
|
119
|
+
root.store.getState().advance(1 / 60);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Tests can inject the WebGL-free deterministic harness from
|
|
123
|
+
`@octanejs/three/testing`; it drives the same root, host commits, hooks, and
|
|
124
|
+
public `advance()` loop as an application. Its awaitable `fireEvent()` helper
|
|
125
|
+
directly invokes the latest committed handler and settles scheduled work when
|
|
126
|
+
raycasting itself is not under test.
|
|
127
|
+
|
|
128
|
+
`Canvas` installs the default web event manager. Use `eventSource` to subscribe
|
|
129
|
+
through another element and `eventPrefix` (`offset`, `client`, `page`, `layer`,
|
|
130
|
+
or `screen`) to choose the coordinate pair. Programmatic roots can supply a
|
|
131
|
+
custom `events(store)` manager factory and update it through `state.setEvents()`.
|
|
132
|
+
|
|
133
|
+
`useStore()` returns the upstream-compatible callable store. Because a later
|
|
134
|
+
`store(selector)` call is a dynamic function call, the compiler cannot assign
|
|
135
|
+
that call its own lexical hook slot; keep that compatibility form unconditional
|
|
136
|
+
and in stable order. Prefer `useStore(selector, equality?)` or
|
|
137
|
+
`useThree(selector, equality?)` when using Octane's conditional-hook semantics.
|
|
138
|
+
|
|
139
|
+
## Portals
|
|
140
|
+
|
|
141
|
+
`createPortal` keeps its children in the authored Octane owner/context tree but
|
|
142
|
+
places their Three hosts below a borrowed `Object3D`. The optional state layer
|
|
143
|
+
matches R3F's portal model: it has its own scene, raycaster, pointer, and event
|
|
144
|
+
priority while sharing the outer root's interaction registry and frame loop.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { createPortal, useThree } from '@octanejs/three';
|
|
148
|
+
|
|
149
|
+
function Overlay() @{
|
|
150
|
+
const scene = useThree((state) => state.scene);
|
|
151
|
+
<group name={scene.name + '-overlay'} />
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Scene(props) @{
|
|
155
|
+
<>
|
|
156
|
+
{createPortal(<Overlay />, props.overlayTarget, {
|
|
157
|
+
events: { priority: 2 },
|
|
158
|
+
})}
|
|
159
|
+
</>
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Portal targets are borrowed and never disposed by Octane. A managed target must
|
|
164
|
+
belong to the same root; a local `Object3D` target also cannot cross a commit
|
|
165
|
+
transport. Pointer hits use the portal layer's camera/raycaster, then bubble
|
|
166
|
+
through physical `Object3D.parent` ancestry as they do in R3F. Component errors,
|
|
167
|
+
effects, context, and scheduling continue to follow logical Octane ownership.
|
|
168
|
+
|
|
169
|
+
## Assets, Suspense, and errors
|
|
170
|
+
|
|
171
|
+
`useLoader` follows the R3F v9 cache contract. A loader constructor is
|
|
172
|
+
instantiated once, while an existing loader instance is used directly. The
|
|
173
|
+
loader identity and normalized input form the cache key; extensions and
|
|
174
|
+
progress callbacks configure the first request for that key.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { useLoader } from '@octanejs/three';
|
|
178
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
179
|
+
|
|
180
|
+
function Model() @{
|
|
181
|
+
const gltf = useLoader(GLTFLoader, '/model.glb');
|
|
182
|
+
<primitive object={gltf.scene} />
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function Scene() @{
|
|
186
|
+
@try {
|
|
187
|
+
<Model />
|
|
188
|
+
} @pending {
|
|
189
|
+
<group name="loading-model" />
|
|
190
|
+
} @catch (error) {
|
|
191
|
+
<group name={'model-error:' + error.message} />
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
useLoader.preload(GLTFLoader, '/model.glb');
|
|
196
|
+
// Later, when the next read must issue a fresh request:
|
|
197
|
+
useLoader.clear(GLTFLoader, '/model.glb');
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`clear` evicts the exact cache entry; it does not abort a request or dispose
|
|
201
|
+
the resolved asset. Declarative Three resources remain owned by their mounted
|
|
202
|
+
host tree, while objects passed through `primitive` remain caller-owned. A
|
|
203
|
+
root-level Three suspension or render error is projected through `Canvas` to
|
|
204
|
+
the nearest client DOM `@pending` or `@catch` arm. Streaming server fallback
|
|
205
|
+
and Canvas hydration remain Milestone 7 work.
|
|
206
|
+
|
|
207
|
+
## Compatibility target
|
|
208
|
+
|
|
209
|
+
The compatibility baseline is `@react-three/fiber@9.6.1` at commit
|
|
210
|
+
`2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7`, with `three@0.172.0` as the exact
|
|
211
|
+
behavioral oracle. React Native/Expo, R3F 10's WebGPU/TSL APIs, and Drei are
|
|
212
|
+
separate follow-on efforts.
|
|
213
|
+
|
|
214
|
+
See [`docs/three-port-plan.md`](../../docs/three-port-plan.md) for the delivery
|
|
215
|
+
phases and [`UPSTREAM.md`](./UPSTREAM.md) for source and license provenance.
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Upstream React Three Fiber audit
|
|
2
|
+
|
|
3
|
+
This port targets the immutable React Three Fiber release `v9.6.1`:
|
|
4
|
+
|
|
5
|
+
- repository: `https://github.com/pmndrs/react-three-fiber`;
|
|
6
|
+
- tag commit: `2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7`;
|
|
7
|
+
- package: `@react-three/fiber@9.6.1`;
|
|
8
|
+
- exact Three behavior and type oracle: `three@0.172.0` and
|
|
9
|
+
`@types/three@0.172.0`.
|
|
10
|
+
|
|
11
|
+
The upstream package accepts `three >=0.156`, but this port initially supports
|
|
12
|
+
only the pinned r172 lane. The broader peer range is gated on a real
|
|
13
|
+
minimum-version validation lane.
|
|
14
|
+
|
|
15
|
+
## Source boundary
|
|
16
|
+
|
|
17
|
+
The package adapts the R3F web/core behavior rather than its React renderer
|
|
18
|
+
shell:
|
|
19
|
+
|
|
20
|
+
- `packages/fiber/src/three-types.ts` informs renderer-local intrinsic and math
|
|
21
|
+
shorthand types;
|
|
22
|
+
- `packages/fiber/src/core/reconciler.tsx` informs catalogue, reconstruction,
|
|
23
|
+
ordering, attachment, disposal, and interaction-transfer behavior;
|
|
24
|
+
- `packages/fiber/src/core/utils.tsx` informs Three prop resolution and
|
|
25
|
+
application;
|
|
26
|
+
- `packages/fiber/src/core/store.ts`, `renderer.tsx`, `loop.ts`, `events.ts`, and
|
|
27
|
+
`hooks.tsx` inform their corresponding package-owned subsystems;
|
|
28
|
+
- `packages/fiber/src/web/Canvas.tsx` informs the DOM shell while Octane's
|
|
29
|
+
declared renderer boundary replaces R3F's second React root and context
|
|
30
|
+
bridge.
|
|
31
|
+
|
|
32
|
+
React Reconciler, ReactDOM, `scheduler`, `its-fine`, `react-use-measure`,
|
|
33
|
+
`use-sync-external-store`, and `suspend-react` are not runtime dependencies of
|
|
34
|
+
the port. React and R3F are development-only differential oracles.
|
|
35
|
+
|
|
36
|
+
The case-level export/test crosswalk must classify upstream behavior as
|
|
37
|
+
behavioral, differential, browser-only, type/package evidence, or not
|
|
38
|
+
applicable. It is release evidence, not permission to copy Fiber ownership
|
|
39
|
+
internals into Octane.
|
|
40
|
+
|
|
41
|
+
Milestone 5 follows the pinned `useLoader` observable contract: constructor
|
|
42
|
+
loaders are singleton instances, cache identity combines the loader and
|
|
43
|
+
normalized input, GLTF-shaped results receive the public graph map, and
|
|
44
|
+
`preload`/`clear` share that same key. Cache eviction does not imply asset
|
|
45
|
+
disposal; disposal follows Octane's declarative host ownership instead.
|
|
46
|
+
|
|
47
|
+
Milestone 6 follows the pinned `createPortal` contract: the supplied
|
|
48
|
+
`Object3D` is a borrowed physical target, the portal store has an immediate
|
|
49
|
+
`previousRoot` plus local scene/raycaster/pointer/event state, and pointer
|
|
50
|
+
events bubble through physical Three ancestry. Octane's universal portal range
|
|
51
|
+
retains logical context, error, effect, and scheduling ownership without
|
|
52
|
+
embedding React Reconciler or representing the target as a fake host child.
|
|
53
|
+
|
|
54
|
+
## License provenance
|
|
55
|
+
|
|
56
|
+
React Three Fiber is MIT-licensed, Copyright 2019–2025 Poimandres. Adapted
|
|
57
|
+
algorithms and types retain that notice in [`LICENSE`](./LICENSE). Three is a
|
|
58
|
+
peer dependency under its own MIT license; no Three source is vendored here.
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/three",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"octane": {
|
|
10
|
+
"hookSlots": {
|
|
11
|
+
"manual": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"description": "React Three Fiber-compatible web bindings for Octane's universal renderer.",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Dominic Gannaway",
|
|
19
|
+
"email": "dg@domgan.com"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
27
|
+
"directory": "packages/three"
|
|
28
|
+
},
|
|
29
|
+
"main": "src/index.ts",
|
|
30
|
+
"module": "src/index.ts",
|
|
31
|
+
"types": "src/index.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md",
|
|
36
|
+
"UPSTREAM.md"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": "./src/index.ts",
|
|
40
|
+
"./core": "./src/core/index.ts",
|
|
41
|
+
"./renderer": "./src/renderer.ts",
|
|
42
|
+
"./config": "./src/config.ts",
|
|
43
|
+
"./testing": "./src/testing.ts",
|
|
44
|
+
"./intrinsics": "./src/intrinsics.ts",
|
|
45
|
+
"./intrinsics/jsx-runtime": "./src/intrinsics.ts"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@types/three": "0.172.0",
|
|
49
|
+
"zustand": "^5.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"three": ">=0.172.0 <0.173.0",
|
|
53
|
+
"octane": "0.1.7"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@react-three/fiber": "9.6.1",
|
|
57
|
+
"@rsbuild/core": "^2.1.5",
|
|
58
|
+
"@rspack/core": "^2.1.3",
|
|
59
|
+
"@tsrx/react": "^0.2.37",
|
|
60
|
+
"esbuild": "^0.28.1",
|
|
61
|
+
"playwright": "^1.61.0",
|
|
62
|
+
"react": "^19.2.0",
|
|
63
|
+
"react-dom": "^19.2.0",
|
|
64
|
+
"three": "0.172.0",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"vite": "^8.0.16",
|
|
67
|
+
"vitest": "^4.1.9",
|
|
68
|
+
"@octanejs/rsbuild-plugin": "0.1.2",
|
|
69
|
+
"@octanejs/rspack-plugin": "0.1.2",
|
|
70
|
+
"octane": "0.1.7",
|
|
71
|
+
"@octanejs/vite-plugin": "0.1.7"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"test": "pnpm upstream:check && vitest run --root ../.. --project three",
|
|
75
|
+
"upstream:check": "node scripts/check-upstream-crosswalk.mjs"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable compiler metadata for Three-rendered TSRX modules.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally imports neither Three nor an Octane runtime. Vite,
|
|
5
|
+
* Rspack, Rsbuild, and language tooling can load the same data without creating
|
|
6
|
+
* renderer state or evaluating browser-only code.
|
|
7
|
+
*/
|
|
8
|
+
export const THREE_RENDERER_ID = 'three';
|
|
9
|
+
|
|
10
|
+
export const threeRenderer = {
|
|
11
|
+
module: '@octanejs/three/renderer',
|
|
12
|
+
target: 'universal',
|
|
13
|
+
server: 'client-only',
|
|
14
|
+
intrinsics: '@octanejs/three/intrinsics',
|
|
15
|
+
text: 'ignore',
|
|
16
|
+
capabilities: ['local-host-callback', 'visibility', 'portal'],
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export const threeRendererRegistry = {
|
|
20
|
+
[THREE_RENDERER_ID]: threeRenderer,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export const threeRendererRules = [
|
|
24
|
+
{
|
|
25
|
+
include: '**/*.three.tsrx',
|
|
26
|
+
renderer: THREE_RENDERER_ID,
|
|
27
|
+
},
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export const threeRendererBoundaries = {
|
|
31
|
+
'@octanejs/three': {
|
|
32
|
+
Canvas: {
|
|
33
|
+
ownerRenderer: 'dom',
|
|
34
|
+
childRenderer: THREE_RENDERER_ID,
|
|
35
|
+
prop: 'children',
|
|
36
|
+
server: 'omit-child',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export const threeRenderers = {
|
|
42
|
+
registry: threeRendererRegistry,
|
|
43
|
+
rules: threeRendererRules,
|
|
44
|
+
boundaries: threeRendererBoundaries,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
/** Short compatibility name for app config files. */
|
|
48
|
+
export const renderers = threeRenderers;
|
|
49
|
+
|
|
50
|
+
export default threeRenderers;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three attachment path handling.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from React Three Fiber v9.6.1's string attachment and automatic
|
|
5
|
+
* geometry/material behavior:
|
|
6
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L286-L319
|
|
7
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L525-L529
|
|
8
|
+
*
|
|
9
|
+
* Function attachments use Octane's post-accept local-callback channel, so
|
|
10
|
+
* this module intentionally owns string and automatic attachments only.
|
|
11
|
+
*/
|
|
12
|
+
import { inspectAppliedThreePropsPath, resolveProperty } from './props.js';
|
|
13
|
+
|
|
14
|
+
const ARRAY_INDEX = /-\d+$/;
|
|
15
|
+
|
|
16
|
+
export type EffectiveAttachment = string | null | undefined;
|
|
17
|
+
|
|
18
|
+
export interface AttachmentState<Parent extends object = object, Child = unknown> {
|
|
19
|
+
readonly kind: 'string';
|
|
20
|
+
readonly parent: Parent;
|
|
21
|
+
readonly child: Child;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly root: any;
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly previous: unknown;
|
|
26
|
+
active: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPropertyRoot(value: unknown): value is Record<string, any> {
|
|
30
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function requirePropertyRoot(value: unknown, path: string): asserts value is Record<string, any> {
|
|
34
|
+
if (!isPropertyRoot(value)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`@octanejs/three: Cannot attach to ${JSON.stringify(path)} because its parent is not an object.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return the authored attachment, or R3F's automatic geometry/material target.
|
|
43
|
+
* An explicit null suppresses automatic attachment.
|
|
44
|
+
*/
|
|
45
|
+
export function getEffectiveAttachment(
|
|
46
|
+
object: unknown,
|
|
47
|
+
authored: EffectiveAttachment = undefined,
|
|
48
|
+
): EffectiveAttachment {
|
|
49
|
+
if (authored !== undefined) return authored;
|
|
50
|
+
if ((object as { isBufferGeometry?: boolean } | null)?.isBufferGeometry === true) {
|
|
51
|
+
return 'geometry';
|
|
52
|
+
}
|
|
53
|
+
if ((object as { isMaterial?: boolean } | null)?.isMaterial === true) return 'material';
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Validate a final string attachment target without mutating its parent. */
|
|
58
|
+
export function validateStringAttachment(
|
|
59
|
+
parent: object,
|
|
60
|
+
path: string,
|
|
61
|
+
overrides: Readonly<Record<string, unknown>> = {},
|
|
62
|
+
): void {
|
|
63
|
+
if (path.length === 0) throw new TypeError('@octanejs/three: attach must not be empty.');
|
|
64
|
+
|
|
65
|
+
// Indexed attachments first replace the target property with an array when
|
|
66
|
+
// necessary, so only the array property's parent must already be object-like.
|
|
67
|
+
const targetPath = ARRAY_INDEX.test(path) ? path.replace(ARRAY_INDEX, '') : path;
|
|
68
|
+
const status = inspectAppliedThreePropsPath(parent, targetPath, overrides);
|
|
69
|
+
if (status === 'valid') return;
|
|
70
|
+
const reason =
|
|
71
|
+
status === 'uncertain'
|
|
72
|
+
? 'a custom setter makes its final parent shape uncertain'
|
|
73
|
+
: 'its parent is not an object';
|
|
74
|
+
throw new Error(`@octanejs/three: Cannot attach to ${JSON.stringify(path)} because ${reason}.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Attach a child at a direct or dash-pierced Three property path. */
|
|
78
|
+
export function attachString<Parent extends object, Child>(
|
|
79
|
+
parent: Parent,
|
|
80
|
+
child: Child,
|
|
81
|
+
path: string,
|
|
82
|
+
): AttachmentState<Parent, Child> {
|
|
83
|
+
if (path.length === 0) throw new TypeError('@octanejs/three: attach must not be empty.');
|
|
84
|
+
|
|
85
|
+
// R3F's `material-0` convention creates the target array when necessary.
|
|
86
|
+
if (ARRAY_INDEX.test(path)) {
|
|
87
|
+
const arrayPath = path.replace(ARRAY_INDEX, '');
|
|
88
|
+
const arrayTarget = resolveProperty(parent, arrayPath);
|
|
89
|
+
requirePropertyRoot(arrayTarget.root, arrayPath);
|
|
90
|
+
if (!Array.isArray(arrayTarget.root[arrayTarget.key])) {
|
|
91
|
+
arrayTarget.root[arrayTarget.key] = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const target = resolveProperty(parent, path);
|
|
96
|
+
requirePropertyRoot(target.root, path);
|
|
97
|
+
const previous = target.root[target.key];
|
|
98
|
+
target.root[target.key] = child;
|
|
99
|
+
return {
|
|
100
|
+
kind: 'string',
|
|
101
|
+
parent,
|
|
102
|
+
child,
|
|
103
|
+
path,
|
|
104
|
+
root: target.root,
|
|
105
|
+
key: target.key,
|
|
106
|
+
previous,
|
|
107
|
+
active: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Restore the property value captured by `attachString`, exactly once. */
|
|
112
|
+
export function detachAttachment(state: AttachmentState): void {
|
|
113
|
+
if (!state.active) return;
|
|
114
|
+
state.active = false;
|
|
115
|
+
if (state.previous === undefined) delete state.root[state.key];
|
|
116
|
+
else state.root[state.key] = state.previous;
|
|
117
|
+
}
|