@effect-motion/three 0.5.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.
@@ -0,0 +1,204 @@
1
+ import type { Scope } from "effect";
2
+ import { Effect } from "effect";
3
+ import * as Pipeable from "effect/Pipeable";
4
+ import * as THREE from "three/webgpu";
5
+ import type * as RenderTarget from "./RenderTarget.js";
6
+ import type * as Scene from "./Scene.js";
7
+ import type { ThreeException } from "./ThreeException.js";
8
+ /**
9
+ * The GPU renderer — a scoped handle over three's `WebGPURenderer`.
10
+ *
11
+ * @remarks
12
+ * {@link make} hands back a renderer whose async initialization has already
13
+ * completed, so nothing downstream has to await a device that might not be
14
+ * ready. On scope close it drains the GPU queue before disposing, which is
15
+ * what prevents the "destroyed texture used in a submit" errors that
16
+ * disposing mid-flight would otherwise produce.
17
+ *
18
+ * Work that touches the GPU — {@link render}, {@link readRenderTarget},
19
+ * {@link compile} — is an Effect, typed as `ThreeException`. Sizing and
20
+ * output-target bookkeeping cannot fail, so those stay synchronous and
21
+ * chain through `.pipe`.
22
+ */
23
+ export declare const TypeId: "~three/Renderer";
24
+ /**
25
+ * A handle to an initialized GPU renderer.
26
+ *
27
+ * @remarks
28
+ * The three renderer stays reachable through `~three.renderer` for anything
29
+ * this wrapper does not cover — a deliberate escape hatch, not the front
30
+ * door.
31
+ */
32
+ export interface Renderer extends Pipeable.Pipeable {
33
+ readonly [TypeId]: typeof TypeId;
34
+ readonly "~three.renderer": THREE.WebGPURenderer;
35
+ }
36
+ /** Whether `u` is a {@link Renderer} handle. */
37
+ export declare const isRenderer: (u: unknown) => u is Renderer;
38
+ type WebGPURendererParameters = NonNullable<ConstructorParameters<typeof THREE.WebGPURenderer>[0]>;
39
+ /**
40
+ * Everything three's `WebGPURenderer` accepts, plus initial sizing applied
41
+ * before initialization.
42
+ *
43
+ * @remarks
44
+ * Sizing here rather than after `make` avoids an initial render at the
45
+ * wrong size. Canvas CSS is never touched — callers own the element's
46
+ * styling.
47
+ */
48
+ export interface MakeOptions extends WebGPURendererParameters {
49
+ readonly width?: number;
50
+ readonly height?: number;
51
+ readonly pixelRatio?: number;
52
+ }
53
+ /**
54
+ * Acquire a renderer, initialization already awaited.
55
+ *
56
+ * @remarks
57
+ * Scoped: on close the renderer waits for in-flight GPU work to land,
58
+ * drains the queue, and disposes. A failure during that teardown is logged
59
+ * rather than raised, so it never masks the scope's real outcome.
60
+ *
61
+ * Without a `canvas`, three creates one. For headless use, pass the canvas
62
+ * and device from `@effect-motion/three/node`.
63
+ *
64
+ * @param options - Renderer parameters plus optional initial sizing.
65
+ * @returns A renderer, valid for the current scope.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const renderer = yield* Renderer.make({ width: 640, height: 360 });
70
+ * ```
71
+ */
72
+ export declare const make: (options?: MakeOptions) => Effect.Effect<Renderer, ThreeException, Scope.Scope>;
73
+ /**
74
+ * Draw a scene through a camera.
75
+ *
76
+ * @remarks
77
+ * Output goes wherever {@link setRenderTarget} last pointed — the canvas by
78
+ * default, or an offscreen target. Safe to call immediately after
79
+ * {@link make}, since initialization is already complete by then.
80
+ */
81
+ export declare const render: (self: Renderer, scene: Scene.Scene, camera: THREE.Camera) => Effect.Effect<void, ThreeException>;
82
+ /**
83
+ * Read rendered pixels back off the GPU as RGBA bytes.
84
+ *
85
+ * @remarks
86
+ * The result is exactly `width * height * 4` bytes, top-down and tightly
87
+ * packed — ready to hand to an image encoder. WebGPU itself pads each row
88
+ * to a 256-byte boundary; that padding is stripped here so callers never
89
+ * deal with stride.
90
+ *
91
+ * Colors come back LINEAR. Render through a
92
+ * {@link PostProcessing.RenderPipeline} first if you want the sRGB output
93
+ * transform applied — reading a raw render target and encoding it directly
94
+ * produces a visibly dark image.
95
+ *
96
+ * @param target - The target to read.
97
+ * @param width - Region width in device pixels.
98
+ * @param height - Region height in device pixels.
99
+ * @returns `width * height * 4` bytes of RGBA.
100
+ */
101
+ export declare const readRenderTarget: (self: Renderer, target: RenderTarget.RenderTarget, width: number, height: number) => Effect.Effect<Uint8Array, ThreeException>;
102
+ /**
103
+ * Compile the shader pipelines a scene needs, ahead of drawing it.
104
+ *
105
+ * @remarks
106
+ * WebGPU compiles a pipeline the first time it is used, which lands as a
107
+ * visible hitch (roughly 40–80ms) on the first frame. Calling this after
108
+ * the scene is populated but before anything is shown moves that cost into
109
+ * startup.
110
+ */
111
+ export declare const compile: (self: Renderer, scene: Scene.Scene, camera: THREE.Camera) => Effect.Effect<void, ThreeException>;
112
+ /**
113
+ * Point the renderer's output at an offscreen target, or `null` to draw to
114
+ * the canvas.
115
+ *
116
+ * @remarks
117
+ * Rendering to a target is how a result becomes something to sample —
118
+ * reading pixels back, or feeding a texture into another pass. Save and
119
+ * restore the previous target around nested renders; {@link getRenderTarget}
120
+ * is there for exactly that.
121
+ */
122
+ export declare const setRenderTarget: {
123
+ (target: RenderTarget.RenderTarget | null): (self: Renderer) => Renderer;
124
+ (self: Renderer, target: RenderTarget.RenderTarget | null): Renderer;
125
+ };
126
+ /**
127
+ * The current output target, or `null` when drawing to the canvas.
128
+ *
129
+ * @remarks
130
+ * Read it before redirecting output so you can restore it afterwards.
131
+ */
132
+ export declare const getRenderTarget: (self: Renderer) => RenderTarget.RenderTarget | null;
133
+ /**
134
+ * Whether the renderer clears the canvas before each render.
135
+ *
136
+ * @remarks
137
+ * Turning it off is how a second pass draws ON TOP of what is already
138
+ * there — an overlay or HUD tier. Turn it back on afterwards, or the next
139
+ * frame will accumulate over this one.
140
+ */
141
+ export declare const setAutoClear: {
142
+ (autoClear: boolean): (self: Renderer) => Renderer;
143
+ (self: Renderer, autoClear: boolean): Renderer;
144
+ };
145
+ /**
146
+ * Clear the depth buffer.
147
+ *
148
+ * @remarks
149
+ * Paired with `setAutoClear(false)` for an overlay pass: without it, the
150
+ * overlay's geometry would be depth-tested against the world it is meant to
151
+ * sit above and could be hidden by it.
152
+ */
153
+ export declare const clearDepth: (self: Renderer) => Renderer;
154
+ /**
155
+ * Advance three's internal frame counter by one.
156
+ *
157
+ * @remarks
158
+ * Only needed when driving renders yourself rather than through three's
159
+ * animation loop — a headless export, above all.
160
+ *
161
+ * Nodes that dedupe their work per frame (the scene pass especially) decide
162
+ * whether to recompute by comparing against this counter. three only ticks
163
+ * it inside its own rAF loop, which an export outruns, so consecutive
164
+ * exported frames would sample a stale texture and come out
165
+ * pairwise-duplicated. Calling this once per exported frame makes one
166
+ * exported frame mean one three frame.
167
+ */
168
+ export declare const advanceFrame: (self: Renderer) => Renderer;
169
+ /**
170
+ * Set the renderer's logical size in CSS pixels.
171
+ *
172
+ * @remarks
173
+ * The actual drawing buffer is this multiplied by the pixel ratio. Canvas
174
+ * CSS is left alone unless `updateStyle` is true, since callers usually own
175
+ * the element's layout.
176
+ *
177
+ * @defaultValue `updateStyle` — `false`
178
+ */
179
+ export declare const setSize: {
180
+ (width: number, height: number, updateStyle?: boolean): (self: Renderer) => Renderer;
181
+ (self: Renderer, width: number, height: number, updateStyle?: boolean): Renderer;
182
+ };
183
+ /**
184
+ * Set device pixels per logical pixel.
185
+ *
186
+ * @remarks
187
+ * Pass `window.devicePixelRatio` for a sharp result on a high-DPI display,
188
+ * or a fixed value above 1 to supersample an export for cleaner edges.
189
+ */
190
+ export declare const setPixelRatio: {
191
+ (pixelRatio: number): (self: Renderer) => Renderer;
192
+ (self: Renderer, pixelRatio: number): Renderer;
193
+ };
194
+ /** The current device-pixels-per-logical-pixel ratio. */
195
+ export declare const getPixelRatio: (self: Renderer) => number;
196
+ /**
197
+ * The drawing buffer's size in device pixels — the logical size multiplied
198
+ * by the pixel ratio, and therefore the dimensions to read pixels back at.
199
+ */
200
+ export declare const getDrawingBufferSize: (self: Renderer) => {
201
+ readonly width: number;
202
+ readonly height: number;
203
+ };
204
+ export {};
@@ -0,0 +1,241 @@
1
+ import { Effect, Predicate } from "effect";
2
+ import { dual } from "effect/Function";
3
+ import * as Pipeable from "effect/Pipeable";
4
+ import * as THREE from "three/webgpu";
5
+ import { wrap, wrapPromise } from "./Interop.js";
6
+ import * as RenderTargetModule from "./RenderTarget.js";
7
+ /**
8
+ * The GPU renderer — a scoped handle over three's `WebGPURenderer`.
9
+ *
10
+ * @remarks
11
+ * {@link make} hands back a renderer whose async initialization has already
12
+ * completed, so nothing downstream has to await a device that might not be
13
+ * ready. On scope close it drains the GPU queue before disposing, which is
14
+ * what prevents the "destroyed texture used in a submit" errors that
15
+ * disposing mid-flight would otherwise produce.
16
+ *
17
+ * Work that touches the GPU — {@link render}, {@link readRenderTarget},
18
+ * {@link compile} — is an Effect, typed as `ThreeException`. Sizing and
19
+ * output-target bookkeeping cannot fail, so those stay synchronous and
20
+ * chain through `.pipe`.
21
+ */
22
+ export const TypeId = "~three/Renderer";
23
+ /** Whether `u` is a {@link Renderer} handle. */
24
+ export const isRenderer = (u) => Predicate.hasProperty(u, TypeId);
25
+ /**
26
+ * `dual`'s predicate receives the whole `arguments` object, not the first
27
+ * argument — dispatch on `args[0]`. Guard-based, never arity (AGENTS.md).
28
+ */
29
+ const firstArgIsRenderer = (args) => isRenderer(args[0]);
30
+ const brand = (renderer) => {
31
+ const self = {
32
+ [TypeId]: TypeId,
33
+ "~three.renderer": renderer,
34
+ // see Scene.ts on the array-like cast
35
+ pipe(...fns) {
36
+ return Pipeable.pipeArguments(self, fns);
37
+ },
38
+ };
39
+ return self;
40
+ };
41
+ const acquire = Effect.fnUntraced(function* (options) {
42
+ const { width, height, pixelRatio, ...parameters } = options;
43
+ const renderer = yield* wrap("WebGPURenderer", () => new THREE.WebGPURenderer(parameters));
44
+ if (pixelRatio !== undefined) {
45
+ renderer.setPixelRatio(pixelRatio);
46
+ }
47
+ if (width !== undefined && height !== undefined) {
48
+ renderer.setSize(width, height, false);
49
+ }
50
+ yield* wrapPromise("WebGPURenderer.init", () => renderer.init());
51
+ return brand(renderer);
52
+ });
53
+ // disposing destroys GPU textures immediately, but the backend's in-flight
54
+ // async chains (deferred submits, per-render resolves) can still submit
55
+ // afterwards — "Destroyed texture used in a submit" validation spam. Let
56
+ // pending work land while resources are alive, drain the queue, then
57
+ // dispose. A release failure is logged, never thrown — teardown must not
58
+ // mask the scope's real outcome.
59
+ const release = (self) => {
60
+ const renderer = self["~three.renderer"];
61
+ return Effect.sleep("50 millis").pipe(Effect.andThen(wrapPromise("WebGPURenderer queue drain", async () => {
62
+ const device = renderer.backend.device;
63
+ if (device !== undefined) {
64
+ await device.queue.onSubmittedWorkDone();
65
+ }
66
+ })), Effect.andThen(wrap("WebGPURenderer.dispose", () => renderer.dispose())), Effect.catchCause((cause) => Effect.logWarning("WebGPURenderer dispose failed", cause)));
67
+ };
68
+ /**
69
+ * Acquire a renderer, initialization already awaited.
70
+ *
71
+ * @remarks
72
+ * Scoped: on close the renderer waits for in-flight GPU work to land,
73
+ * drains the queue, and disposes. A failure during that teardown is logged
74
+ * rather than raised, so it never masks the scope's real outcome.
75
+ *
76
+ * Without a `canvas`, three creates one. For headless use, pass the canvas
77
+ * and device from `@effect-motion/three/node`.
78
+ *
79
+ * @param options - Renderer parameters plus optional initial sizing.
80
+ * @returns A renderer, valid for the current scope.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const renderer = yield* Renderer.make({ width: 640, height: 360 });
85
+ * ```
86
+ */
87
+ export const make = (options = {}) => Effect.acquireRelease(acquire(options), release);
88
+ /**
89
+ * Draw a scene through a camera.
90
+ *
91
+ * @remarks
92
+ * Output goes wherever {@link setRenderTarget} last pointed — the canvas by
93
+ * default, or an offscreen target. Safe to call immediately after
94
+ * {@link make}, since initialization is already complete by then.
95
+ */
96
+ export const render = (self, scene, camera) => wrap("WebGPURenderer.render", () => self["~three.renderer"].render(scene["~three.scene"], camera));
97
+ /**
98
+ * Read rendered pixels back off the GPU as RGBA bytes.
99
+ *
100
+ * @remarks
101
+ * The result is exactly `width * height * 4` bytes, top-down and tightly
102
+ * packed — ready to hand to an image encoder. WebGPU itself pads each row
103
+ * to a 256-byte boundary; that padding is stripped here so callers never
104
+ * deal with stride.
105
+ *
106
+ * Colors come back LINEAR. Render through a
107
+ * {@link PostProcessing.RenderPipeline} first if you want the sRGB output
108
+ * transform applied — reading a raw render target and encoding it directly
109
+ * produces a visibly dark image.
110
+ *
111
+ * @param target - The target to read.
112
+ * @param width - Region width in device pixels.
113
+ * @param height - Region height in device pixels.
114
+ * @returns `width * height * 4` bytes of RGBA.
115
+ */
116
+ export const readRenderTarget = (self, target, width, height) => wrapPromise("readRenderTargetPixelsAsync", () => self["~three.renderer"].readRenderTargetPixelsAsync(target["~three.renderTarget"], 0, 0, width, height)).pipe(Effect.map((pixels) => {
117
+ const padded = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
118
+ const tightRow = width * 4;
119
+ const paddedRow = Math.ceil(tightRow / 256) * 256;
120
+ if (padded.length === tightRow * height) {
121
+ return padded;
122
+ }
123
+ const rgba = new Uint8Array(tightRow * height);
124
+ for (let y = 0; y < height; y++) {
125
+ rgba.set(padded.subarray(y * paddedRow, y * paddedRow + tightRow), y * tightRow);
126
+ }
127
+ return rgba;
128
+ }));
129
+ /**
130
+ * Compile the shader pipelines a scene needs, ahead of drawing it.
131
+ *
132
+ * @remarks
133
+ * WebGPU compiles a pipeline the first time it is used, which lands as a
134
+ * visible hitch (roughly 40–80ms) on the first frame. Calling this after
135
+ * the scene is populated but before anything is shown moves that cost into
136
+ * startup.
137
+ */
138
+ export const compile = (self, scene, camera) => wrapPromise("WebGPURenderer.compileAsync", () => self["~three.renderer"]
139
+ .compileAsync(scene["~three.scene"], camera)
140
+ .then(() => undefined));
141
+ /**
142
+ * Point the renderer's output at an offscreen target, or `null` to draw to
143
+ * the canvas.
144
+ *
145
+ * @remarks
146
+ * Rendering to a target is how a result becomes something to sample —
147
+ * reading pixels back, or feeding a texture into another pass. Save and
148
+ * restore the previous target around nested renders; {@link getRenderTarget}
149
+ * is there for exactly that.
150
+ */
151
+ export const setRenderTarget = dual(firstArgIsRenderer, (self, target) => {
152
+ self["~three.renderer"].setRenderTarget(target === null ? null : target["~three.renderTarget"]);
153
+ return self;
154
+ });
155
+ /**
156
+ * The current output target, or `null` when drawing to the canvas.
157
+ *
158
+ * @remarks
159
+ * Read it before redirecting output so you can restore it afterwards.
160
+ */
161
+ export const getRenderTarget = (self) => {
162
+ const target = self["~three.renderer"].getRenderTarget();
163
+ return target === null ? null : RenderTargetModule.fromRaw(target);
164
+ };
165
+ /**
166
+ * Whether the renderer clears the canvas before each render.
167
+ *
168
+ * @remarks
169
+ * Turning it off is how a second pass draws ON TOP of what is already
170
+ * there — an overlay or HUD tier. Turn it back on afterwards, or the next
171
+ * frame will accumulate over this one.
172
+ */
173
+ export const setAutoClear = dual(firstArgIsRenderer, (self, autoClear) => {
174
+ self["~three.renderer"].autoClear = autoClear;
175
+ return self;
176
+ });
177
+ /**
178
+ * Clear the depth buffer.
179
+ *
180
+ * @remarks
181
+ * Paired with `setAutoClear(false)` for an overlay pass: without it, the
182
+ * overlay's geometry would be depth-tested against the world it is meant to
183
+ * sit above and could be hidden by it.
184
+ */
185
+ export const clearDepth = (self) => {
186
+ self["~three.renderer"].clearDepth();
187
+ return self;
188
+ };
189
+ /**
190
+ * Advance three's internal frame counter by one.
191
+ *
192
+ * @remarks
193
+ * Only needed when driving renders yourself rather than through three's
194
+ * animation loop — a headless export, above all.
195
+ *
196
+ * Nodes that dedupe their work per frame (the scene pass especially) decide
197
+ * whether to recompute by comparing against this counter. three only ticks
198
+ * it inside its own rAF loop, which an export outruns, so consecutive
199
+ * exported frames would sample a stale texture and come out
200
+ * pairwise-duplicated. Calling this once per exported frame makes one
201
+ * exported frame mean one three frame.
202
+ */
203
+ export const advanceFrame = (self) => {
204
+ self["~three.renderer"]._nodes.nodeFrame.update();
205
+ return self;
206
+ };
207
+ /**
208
+ * Set the renderer's logical size in CSS pixels.
209
+ *
210
+ * @remarks
211
+ * The actual drawing buffer is this multiplied by the pixel ratio. Canvas
212
+ * CSS is left alone unless `updateStyle` is true, since callers usually own
213
+ * the element's layout.
214
+ *
215
+ * @defaultValue `updateStyle` — `false`
216
+ */
217
+ export const setSize = dual(firstArgIsRenderer, (self, width, height, updateStyle = false) => {
218
+ self["~three.renderer"].setSize(width, height, updateStyle);
219
+ return self;
220
+ });
221
+ /**
222
+ * Set device pixels per logical pixel.
223
+ *
224
+ * @remarks
225
+ * Pass `window.devicePixelRatio` for a sharp result on a high-DPI display,
226
+ * or a fixed value above 1 to supersample an export for cleaner edges.
227
+ */
228
+ export const setPixelRatio = dual(firstArgIsRenderer, (self, pixelRatio) => {
229
+ self["~three.renderer"].setPixelRatio(pixelRatio);
230
+ return self;
231
+ });
232
+ /** The current device-pixels-per-logical-pixel ratio. */
233
+ export const getPixelRatio = (self) => self["~three.renderer"].getPixelRatio();
234
+ /**
235
+ * The drawing buffer's size in device pixels — the logical size multiplied
236
+ * by the pixel ratio, and therefore the dimensions to read pixels back at.
237
+ */
238
+ export const getDrawingBufferSize = (self) => {
239
+ const size = self["~three.renderer"].getDrawingBufferSize(new THREE.Vector2());
240
+ return { width: size.x, height: size.y };
241
+ };
@@ -0,0 +1,105 @@
1
+ import type { Scope } from "effect";
2
+ import { Effect } from "effect";
3
+ import * as Pipeable from "effect/Pipeable";
4
+ import * as THREE from "three/webgpu";
5
+ import type * as Object3D from "./Object3D.js";
6
+ /**
7
+ * The scene graph root — a scoped handle over three's `Scene`.
8
+ *
9
+ * @remarks
10
+ * three is already shaped the way this wrapper wants, so this is a
11
+ * branding-and-lifecycle layer rather than a redesign. Everything here —
12
+ * {@link add}, {@link remove}, {@link clear}, {@link setBackground} — is
13
+ * infallible bookkeeping on an object already in hand, so it stays
14
+ * synchronous and chains through `.pipe`. Effect enters only at
15
+ * {@link make}, which registers teardown.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const scene = yield* Scene.make();
20
+ * scene.pipe(Scene.add([mesh]), Scene.setBackground(new Color(0x16161d)));
21
+ * ```
22
+ */
23
+ export declare const TypeId: "~three/Scene";
24
+ /**
25
+ * A handle to a three scene.
26
+ *
27
+ * @remarks
28
+ * The underlying scene stays reachable through `~three.scene` for anything
29
+ * this wrapper does not cover.
30
+ */
31
+ export interface Scene extends Pipeable.Pipeable {
32
+ readonly [TypeId]: typeof TypeId;
33
+ readonly "~three.scene": THREE.Scene;
34
+ }
35
+ /** Whether `u` is a {@link Scene} handle. */
36
+ export declare const isScene: (u: unknown) => u is Scene;
37
+ /**
38
+ * Wrap an existing three scene WITHOUT registering teardown.
39
+ *
40
+ * @remarks
41
+ * For a scene whose lifetime something else already owns and will clean up.
42
+ * Prefer {@link make}, which ties teardown to a scope; reach for this only
43
+ * when a longer-lived owner is genuinely in charge.
44
+ */
45
+ export declare const makeUnsafe: (scene: THREE.Scene) => Scene;
46
+ /**
47
+ * A scoped scene that detaches its children when the scope closes.
48
+ *
49
+ * @remarks
50
+ * Detaching is NOT disposal. A scene does not own the geometries,
51
+ * materials, and textures hanging off its objects — those are routinely
52
+ * shared between objects and outlive any one graph — so whoever created
53
+ * them is responsible for freeing them.
54
+ */
55
+ export declare const make: () => Effect.Effect<Scene, never, Scope.Scope>;
56
+ /**
57
+ * Detach every child from the scene root.
58
+ *
59
+ * @remarks
60
+ * Unparents only — see {@link make} on why this does not dispose anything.
61
+ */
62
+ export declare const clear: (self: Scene) => Scene;
63
+ /** The scene root's direct children (not a deep traversal). */
64
+ export declare const children: (self: Scene) => ReadonlyArray<Object3D.Object3D>;
65
+ /**
66
+ * Whether the scene root has no children.
67
+ *
68
+ * @remarks
69
+ * The "is there anything to draw" check — worth making before an optional
70
+ * pass, so an empty overlay tier costs nothing.
71
+ */
72
+ export declare const isEmpty: (self: Scene) => boolean;
73
+ /**
74
+ * Attach objects to the scene root.
75
+ *
76
+ * @remarks
77
+ * Re-adding an object that is already parented moves it, as in three
78
+ * itself — an object has exactly one parent.
79
+ */
80
+ export declare const add: {
81
+ (objects: ReadonlyArray<Object3D.Object3D>): (self: Scene) => Scene;
82
+ (self: Scene, objects: ReadonlyArray<Object3D.Object3D>): Scene;
83
+ };
84
+ /**
85
+ * Detach objects from the scene root.
86
+ *
87
+ * @remarks
88
+ * A no-op for objects that are not attached, and it does not dispose them —
89
+ * a removed object can be added back.
90
+ */
91
+ export declare const remove: {
92
+ (objects: ReadonlyArray<Object3D.Object3D>): (self: Scene) => Scene;
93
+ (self: Scene, objects: ReadonlyArray<Object3D.Object3D>): Scene;
94
+ };
95
+ /**
96
+ * Set the background color or texture, or `null` for transparency.
97
+ *
98
+ * @remarks
99
+ * `null` is what an overlay tier wants: with nothing painted behind it,
100
+ * whatever was rendered first shows through.
101
+ */
102
+ export declare const setBackground: {
103
+ (background: THREE.Color | THREE.Texture | null): (self: Scene) => Scene;
104
+ (self: Scene, background: THREE.Color | THREE.Texture | null): Scene;
105
+ };
package/dist/Scene.js ADDED
@@ -0,0 +1,118 @@
1
+ import { Effect, Predicate } from "effect";
2
+ import { dual } from "effect/Function";
3
+ import * as Pipeable from "effect/Pipeable";
4
+ import * as THREE from "three/webgpu";
5
+ /**
6
+ * The scene graph root — a scoped handle over three's `Scene`.
7
+ *
8
+ * @remarks
9
+ * three is already shaped the way this wrapper wants, so this is a
10
+ * branding-and-lifecycle layer rather than a redesign. Everything here —
11
+ * {@link add}, {@link remove}, {@link clear}, {@link setBackground} — is
12
+ * infallible bookkeeping on an object already in hand, so it stays
13
+ * synchronous and chains through `.pipe`. Effect enters only at
14
+ * {@link make}, which registers teardown.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const scene = yield* Scene.make();
19
+ * scene.pipe(Scene.add([mesh]), Scene.setBackground(new Color(0x16161d)));
20
+ * ```
21
+ */
22
+ export const TypeId = "~three/Scene";
23
+ /** Whether `u` is a {@link Scene} handle. */
24
+ export const isScene = (u) => Predicate.hasProperty(u, TypeId);
25
+ /**
26
+ * `dual`'s predicate receives the whole `arguments` object, not the first
27
+ * argument — dispatch on `args[0]`, as the motion package's animators do.
28
+ * Guard-based, never arity (AGENTS.md).
29
+ */
30
+ const firstArgIsScene = (args) => isScene(args[0]);
31
+ /**
32
+ * Wrap an existing three scene WITHOUT registering teardown.
33
+ *
34
+ * @remarks
35
+ * For a scene whose lifetime something else already owns and will clean up.
36
+ * Prefer {@link make}, which ties teardown to a scope; reach for this only
37
+ * when a longer-lived owner is genuinely in charge.
38
+ */
39
+ export const makeUnsafe = (scene) => {
40
+ const self = {
41
+ [TypeId]: TypeId,
42
+ "~three.scene": scene,
43
+ // pipeArguments reads its second parameter as an array-like; a rest
44
+ // array satisfies that at runtime, and the cast avoids both the
45
+ // `arguments` object and a lint suppression
46
+ pipe(...fns) {
47
+ return Pipeable.pipeArguments(self, fns);
48
+ },
49
+ };
50
+ return self;
51
+ };
52
+ /**
53
+ * A scoped scene that detaches its children when the scope closes.
54
+ *
55
+ * @remarks
56
+ * Detaching is NOT disposal. A scene does not own the geometries,
57
+ * materials, and textures hanging off its objects — those are routinely
58
+ * shared between objects and outlive any one graph — so whoever created
59
+ * them is responsible for freeing them.
60
+ */
61
+ export const make = Effect.fnUntraced(function* () {
62
+ const scene = new THREE.Scene();
63
+ yield* Effect.addFinalizer(() => Effect.sync(() => scene.clear()));
64
+ return makeUnsafe(scene);
65
+ });
66
+ /**
67
+ * Detach every child from the scene root.
68
+ *
69
+ * @remarks
70
+ * Unparents only — see {@link make} on why this does not dispose anything.
71
+ */
72
+ export const clear = (self) => {
73
+ self["~three.scene"].clear();
74
+ return self;
75
+ };
76
+ /** The scene root's direct children (not a deep traversal). */
77
+ export const children = (self) => self["~three.scene"].children;
78
+ /**
79
+ * Whether the scene root has no children.
80
+ *
81
+ * @remarks
82
+ * The "is there anything to draw" check — worth making before an optional
83
+ * pass, so an empty overlay tier costs nothing.
84
+ */
85
+ export const isEmpty = (self) => self["~three.scene"].children.length === 0;
86
+ /**
87
+ * Attach objects to the scene root.
88
+ *
89
+ * @remarks
90
+ * Re-adding an object that is already parented moves it, as in three
91
+ * itself — an object has exactly one parent.
92
+ */
93
+ export const add = dual(firstArgIsScene, (self, objects) => {
94
+ self["~three.scene"].add(...objects);
95
+ return self;
96
+ });
97
+ /**
98
+ * Detach objects from the scene root.
99
+ *
100
+ * @remarks
101
+ * A no-op for objects that are not attached, and it does not dispose them —
102
+ * a removed object can be added back.
103
+ */
104
+ export const remove = dual(firstArgIsScene, (self, objects) => {
105
+ self["~three.scene"].remove(...objects);
106
+ return self;
107
+ });
108
+ /**
109
+ * Set the background color or texture, or `null` for transparency.
110
+ *
111
+ * @remarks
112
+ * `null` is what an overlay tier wants: with nothing painted behind it,
113
+ * whatever was rendered first shows through.
114
+ */
115
+ export const setBackground = dual(firstArgIsScene, (self, background) => {
116
+ self["~three.scene"].background = background;
117
+ return self;
118
+ });
@@ -0,0 +1,24 @@
1
+ declare const ThreeException_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "ThreeException";
3
+ } & Readonly<A>;
4
+ /**
5
+ * A three.js operation failed.
6
+ *
7
+ * @remarks
8
+ * Every fallible call in this package reports through this one error type,
9
+ * so a caller catches by tag rather than guarding each three API
10
+ * separately. `operation` names which call failed — `"WebGPURenderer.init"`,
11
+ * `"readRenderTargetPixelsAsync"` — and `cause` carries whatever three
12
+ * threw or rejected with.
13
+ *
14
+ * Typical causes are environmental rather than logical: no WebGPU adapter,
15
+ * a lost device, a shader that failed to compile.
16
+ */
17
+ export declare class ThreeException extends ThreeException_base<{
18
+ /** The underlying error three threw or rejected with. */
19
+ cause?: unknown;
20
+ /** Name of the three.js operation that failed. */
21
+ operation?: string;
22
+ }> {
23
+ }
24
+ export {};