@maxenve/ren-three 0.0.0-stage → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wilton Lazary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,307 @@
1
- # Temporary Holding Version
1
+ # ren-three
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The three.js renderer for [ren-core](https://github.com/wiltonlazary/ren-core): a ren-core `Host`
4
+ (`ren-core/host`) over a three.js scene graph. JSX describes the scene; rrx-core
5
+ signals drive it; ren-core's control flow (`Show`, `For`, `Choose`, `Render`,
6
+ `Pending`, `Recover`) adds and removes objects. Plain JavaScript with JSDoc
7
+ types, strict `tsc` checking and generated declarations.
8
+
9
+ The package is published on npm as `@maxenve/ren-three` under the MIT license;
10
+ install it under its import name: `npm install ren-three@npm:@maxenve/ren-three`. Its runtime dependencies are ren-canv and ren-core (linked from
11
+ the sibling checkouts); three.js is a peer
12
+ the caller imports and passes in (`>=0.160`; tested with 0.186.1), so the
13
+ application decides how it loads (ren-show loads it as a chunk of its
14
+ own on first use). It can share a page with ren-dom and ren-skia: each
15
+ application root has its own renderer.
16
+
17
+ ```jsx
18
+ import * as THREE from 'three';
19
+ import { Root, $signal, $read, $write, $active, For } from 'ren-core';
20
+ import { $dom } from 'ren-dom';
21
+ import { $three, $animate } from 'ren-three';
22
+
23
+ const Lantern = () => {
24
+ const lit = $signal($active(), false);
25
+ let globe = null;
26
+ $animate(frame => (globe.position.y = Math.sin(frame.time) * 0.2));
27
+ return (
28
+ <>
29
+ <hemisphereLight intensity={0.6} />
30
+ <pointLight position={[0, 2, 3]} intensity={() => ($read(lit) ? 30 : 5)} />
31
+ <mesh ref={m => (globe = m)} onClick={() => $write(lit, !$read(lit))}>
32
+ <sphereGeometry params={[0.7, 32, 16]} />
33
+ <meshPhongMaterial color={() => ($read(lit) ? '#ffd166' : '#5b6c8f')} shininess={60} />
34
+ </mesh>
35
+ <For each={[-1.2, 0, 1.2]}>
36
+ {x => <mesh position={() => [x(), -1.4, 0]}><cylinderGeometry params={[0.15, 0.2, 0.6]} /><meshNormalMaterial /></mesh>}
37
+ </For>
38
+ </>
39
+ );
40
+ };
41
+
42
+ Root({ id: 'stage', target: document.body, renderer: $three(THREE, { width: 640, height: 360 }), children: <Lantern /> });
43
+ // Nested in a DOM view, fed by props: <Root id="stage" renderer={$three(THREE)} props={…}>{() => <Scene />}</Root>
44
+ ```
45
+
46
+ | Export | What |
47
+ | --- | --- |
48
+ | `$three(THREE, options)` | the renderer (a `Renderer`): the Host, its `scene` and `camera`, `frame(time)`, `resize(w, h)`, `pick(x, y)`, `dispatch(type, x, y)`, and `dragOver(x, y, files, types)`, `dragLeave()`, `dropFiles(x, y, files)` |
49
+ | `render(view, renderer, options)` | `mount` into the renderer's scene; returns the dispose function |
50
+ | `$animate(fn)` | in a component: runs `fn(frame)` before every frame while the component lives (`frame.time`, `frame.delta` in seconds, `frame.scene`, `frame.camera`, `frame.renderer`) |
51
+ | `CONSTRUCT` | the props `element` reads at creation; a worker passes it to `serve` |
52
+ | `loadLayout`, `remoteLayout` | the primitives' layout engine: load it once; lay out in a worker |
53
+ | `loadShaper` | the primitives' text shaper (HarfBuzz): in a browser, load it once with the URL of harfbuzzjs's `harfbuzz.wasm`; node loads it when text first needs it |
54
+ | `Renderer`, `Tick` | the classes |
55
+
56
+ ## Elements
57
+
58
+ - **Tags are three.js classes.** A lowercase element is the class of the
59
+ same name, capitalized, from the namespace passed to `$three`: `<mesh>`,
60
+ `<group>`, `<boxGeometry>`, `<meshStandardMaterial>`, `<ambientLight>`,
61
+ `<perspectiveCamera>`, `<instancedMesh>`, … An unknown name throws.
62
+ `<existing of={made}>` places an object made elsewhere (and leaves
63
+ its disposal to its owner unless `dispose` is true).
64
+ - **A host node is the three.js object itself**, with ren-core's record on it
65
+ under `__ren`; a `ref` receives the object.
66
+ - **Construction.** `params` are the constructor's arguments, compiled or not
67
+ (the Host's `construct` props). New params (a different list of values)
68
+ rebuild the object in place: it keeps its identity, parent and handlers; a
69
+ fresh one is made and copied in (a scene object takes only what its class
70
+ sets beyond Object3D's fields, never its transform or children; a
71
+ geometry frees its old GPU buffers), and the properties the view set apply
72
+ again. A class without `copy` needs a keyed element (a `Show` or a `For`
73
+ row) to make a new one.
74
+ - **Slots.** Geometries go into the parent's `geometry`, materials into
75
+ `material`, fog into `fog`, and `slot="name"` into any other property;
76
+ removal restores the previous value. Other objects are added as children.
77
+ - **Properties** are set by name. A dash sets a path (`position-x`,
78
+ `rotation-y`). A property holding a math object (Vector3, Euler,
79
+ Color, Quaternion, …) is updated in place: an array spreads into `set`,
80
+ the same type is copied, a number is a Color's hex or goes to `setScalar`,
81
+ anything else to `set` (`color="#f80"`, `scale={2}`). Reactive values bind
82
+ as everywhere in ren-core.
83
+ - **Resources.** A removed object's geometries, materials and textures are
84
+ disposed at the end of the flush, unless it was inserted again meanwhile
85
+ (a moved row). `dispose={null}` on a geometry, material or texture
86
+ element keeps that resource; on an object it skips only that object's own
87
+ `dispose()`, and the resources of its elements still go.
88
+ - **`<gl>`** sets the WebGL renderer's own properties from the view
89
+ (`<gl toneMapping={ACESFilmicToneMapping} toneMappingExposure={1.2}
90
+ shadowMap-enabled />`): kept, applied to the renderer as it is made, and
91
+ reactive like any property.
92
+ - **Cursor.** `cursor="pointer"` on an object shows that CSS cursor while
93
+ the pointer is over it or a child (where no primitive is on top).
94
+ - **Camera.** A PerspectiveCamera by default (`camera` option), sized to the
95
+ canvas; `<perspectiveCamera main>` in the view replaces it.
96
+ - **Portable primitives render too** (`View`, `Layout`, `Scroll`, `Label`,
97
+ `Edit`, `Action`, `Image`, `Shape`), through
98
+ [ren-canv](https://github.com/wiltonlazary/ren-canv): the same layout, portable style,
99
+ editing, text selection, native text entry and accessibility mirror as on
100
+ ren-skia, drawn with a 2D canvas into a texture. An application of
101
+ primitives alone runs here as on ren-dom or ren-skia, and mixes freely
102
+ with three.js objects:
103
+ - **The overlay.** Primitives placed in the scene itself lie over the
104
+ canvas, pixel for pixel, drawn after the scene; in the order of the
105
+ view among the scene's objects.
106
+ - **Panels.** Primitives placed in a three.js object (`<group>`,
107
+ `<mesh>`) become a plane in it, as large as their content (`pixel`
108
+ world units per pixel, 0.01), facing +z.
109
+ - **Input.** The pointer reaches the overlay first, then a panel under
110
+ it (by the raycast's texture coordinate; a drag stays with the surface
111
+ it started on), then the scene's objects, so a press on a button never
112
+ also clicks the mesh behind it. Keys, copy and select all go to the
113
+ surface last pressed; an Edit's native text entry lies over it on the
114
+ canvas (a panel's projected). A touch follows the `touchAction` of
115
+ the primitives under it on the surface it presses (the overlay where
116
+ it has something there, else a panel), as ren-canv's attach does: its
117
+ start is cancelled where they allow no pan, its first move where it
118
+ goes along an axis they forbid, a second finger where they forbid a
119
+ pinch zoom; where none sets one, a touch that a surface or an object
120
+ with a handler took keeps the page from panning. When the browser
121
+ takes the pointer back (a `pointercancel`), the surface holding the
122
+ press hears `onPointerCancel` where the pointer last was and the press
123
+ ends with no `pointerup`; with no surface holding it, the scene's
124
+ objects there hear `pointercancel` through the raycast.
125
+ - **Drag and drop.** Files dragged over the canvas go to the surfaces as
126
+ presses do: the overlay where it has something there or an item that
127
+ hears drags or drops, else a panel by the raycast's texture coordinate.
128
+ Its items hear `onDragEnter`, `onDragOver`, `onDragLeave` and `onDrop`
129
+ (`{ files, x, y }` in its pixels) as on ren-canv; a drag moving to
130
+ another surface leaves the first, then enters the second, and leaving
131
+ the canvas leaves the one under it; a drop ends the drag without a
132
+ leave. The browser's default (opening a dropped file) is prevented only
133
+ where a surface or a scene object takes the drag. Main thread only.
134
+ - They need the `font` option: the typeface's bytes, which measure the
135
+ text (from the font file itself) and draw it (registered as a
136
+ FontFace); `families` and `fallback` add typefaces by family name and
137
+ for the characters a typeface lacks, measured and registered the same
138
+ way. A character no typeface has measures as the missing glyph, but the
139
+ 2D canvas draws it in the browser's fallback font, at another width.
140
+ Right-to-left text is laid out by ren-canv's bidirectional algorithm;
141
+ the 2D canvas sets each right-to-left run from its right edge in its
142
+ visual order. Text that needs shaping (Arabic joined, Indic clusters,
143
+ marks) is measured shaped (ren-canv's `loadShaper`: in a browser, load
144
+ it once with the URL of harfbuzzjs's `harfbuzz.wasm`) and the browser
145
+ shapes what it draws with the same features (kerning and optional
146
+ ligatures off), at the width measured. A three.js object inside a
147
+ primitive throws.
148
+ - **Surfaces.** Each is a ren-canv Document: `renderer.overlay` is the
149
+ overlay's (null until a primitive is placed in the scene) and
150
+ `renderer.surfaces` holds the overlay's and every panel's, with the
151
+ Document's API (`commands()`, `copied()`, `press`, `key`, …). Pointer
152
+ events, keys, focus, scrolling and the cursor behave as on the DOM
153
+ (ren-canv's README, "Input as the DOM has it"). A root's `$keys` and
154
+ `$scrolls` (listened on the scene) hear every surface: the overlay's
155
+ and each panel's keys, chords too, and scrolls; a key with no surface
156
+ focused goes to the overlay (else the first panel). One surface has
157
+ the focus: a press, an item's `focus()` or `focusFirst()` and
158
+ `$focus(ref)` take it from the others, and the keys go there; a blur
159
+ handler there that moves the focus wins, in whichever surface, as in
160
+ Chrome.
161
+ - **Theme.** `renderer.setTheme(tokens)` takes a new theme at once for the
162
+ overlay and every panel; the scene and all state stay.
163
+ - **Viewport.** In a three.js root `$viewport()` and `$media()` read the
164
+ canvas's size in CSS pixels, followed as it resizes, not the page's
165
+ window; the dark color scheme and reduced motion come from the
166
+ window the canvas is in (none without one). In worker mode the
167
+ connection carries them to the worker (ren-core's README, "Viewport
168
+ and media queries"). In a panel whose primitive sets its width and
169
+ height in pixels (its style's `width` and `height`, or the `width` and
170
+ `height` props), they read the panel's size, followed as it changes;
171
+ the preferences stay the window's. A panel sized by its content reads
172
+ the canvas's: its size follows what it shows, so a view reading it
173
+ could swing between two layouts. The Host's `viewportOf` gives each
174
+ primitive this source, bound to its panel once the primitive is
175
+ placed (a component runs before its nodes are), the canvas's until
176
+ then; in worker mode the connection carries the panel's too.
177
+
178
+ ## Frames
179
+
180
+ Rendering is on demand: a flush that changed the scene asks for one frame
181
+ (`requestAnimationFrame`), and an unchanged flush draws nothing. While any
182
+ `$animate` callback lives, every animation frame runs the callbacks and
183
+ renders. `schedule: false` leaves frames to the caller (`frame(time)`), for
184
+ tests and hosts with their own loop.
185
+
186
+ ## Events
187
+
188
+ `on<Name>` handlers on any object receive raycast pointer events: `click`,
189
+ `dblclick`, `contextmenu`, `pointerdown`, `pointerup`, `pointermove`,
190
+ `pointercancel` (the browser took the pointer back; where it last was),
191
+ `wheel`, and `pointerover`/`pointerout` as the objects under the pointer
192
+ change. Files dragged over the canvas where no primitive takes them reach
193
+ `onDragOver` (with `files`, their count, and `types`) and `onDrop`
194
+ (`files`, the dropped File objects) on the objects under them. The nearest hit's handler runs first, then its ancestors', until one
195
+ calls `stopPropagation()`. The event carries `object` (hit), `currentObject`
196
+ (the handler's), `point`, `distance`, `intersection`, `intersections` and
197
+ `native`. The pointer is read in the canvas's drawn pixels, so a canvas
198
+ the page's CSS resizes still picks what is under it. An object that is
199
+ not drawn (`visible={false}` on it or an ancestor) takes no pointer, as a
200
+ hidden element on the DOM; a catcher that must take it without showing
201
+ stays visible with `colorWrite={false} depthWrite={false}`. Handlers run as a `$batch` in the context they were bound in, as
202
+ everywhere in ren-core. The raycast runs only while some object has a handler or a cursor.
203
+
204
+ ## Layout
205
+
206
+ The primitives are laid out by ren-canv's engine, Taffy by default: `await
207
+ loadLayout()` once before the first renderer that draws them (in a browser,
208
+ with the URL of taffy-layout's `taffy_wasm_bg.wasm`). Or in a worker:
209
+ `layout: remoteLayout(worker)`, with `serveLayout` from `ren-canv/worker`
210
+ there; the overlay and every panel lay out there and paint when their boxes
211
+ come back.
212
+
213
+ ## Options
214
+
215
+ | Option | Meaning |
216
+ | --- | --- |
217
+ | `width`, `height` | the canvas in CSS pixels; by default the host element's size, followed as it resizes (300 × 150 without one) |
218
+ | `scale` | the device pixel ratio (the window's) |
219
+ | `renderer` | what draws: a `WebGLRenderer` made on the canvas by default (antialiased); or one given, or a function of the canvas that makes it (a WebGPU renderer, a stub in tests) |
220
+ | `camera` | a three.js camera, or `{ fov, near, far, position }` for the default PerspectiveCamera (50°, 0.1, 1000, at [0, 0, 5]) |
221
+ | `background` | the scene's background color |
222
+ | `schedule` | `'frame'` (default): frames on `requestAnimationFrame`; `false`: only when `frame()` is called |
223
+ | `font` | the typeface's bytes for the primitives (needed once one renders) |
224
+ | `mono` | a monospace typeface's bytes: text whose `fontFamily` names monospace is measured and drawn in it |
225
+ | `families` | typefaces' bytes by family name: text whose `fontFamily` names one (the first of its list given here, before a generic family) is measured and drawn in it |
226
+ | `fallback` | typefaces' bytes, in order: a character a typeface lacks is measured and drawn in the first of them that has it |
227
+ | `pixel` | world units per pixel of a panel (0.01) |
228
+ | `theme` | theme overrides for the primitives (ren-core's tokens) |
229
+ | `aria` | the primitives' accessibility mirror, as ren-canv's (`true`, `false`, `'demand'`) |
230
+ | `layout` | where the primitives are laid out: this thread (default), or a layout worker, `remoteLayout(port)` |
231
+ | `image` | loads an `Image` source for a 2D canvas (an HTMLImageElement by default) |
232
+
233
+ An application root in a document makes a `<canvas>` in its host element and
234
+ routes its pointer events; `close` (the root's dispose) empties the scene,
235
+ disposes its resources and the WebGL renderer, and removes the canvas.
236
+
237
+ ## Worker mode
238
+
239
+ As with ren-dom and ren-skia, three.js and the scene stay on the main
240
+ thread, with the renderer; the worker holds only the application and its
241
+ components (no three.js, no DOM, no renderer code).
242
+
243
+ ```jsx
244
+ // worker.jsx: the application only
245
+ import { serve } from 'ren-core/worker';
246
+ import { CONSTRUCT } from 'ren-three';
247
+ serve(() => <Scene />, { id: 'stage', construct: CONSTRUCT });
248
+
249
+ // main.jsx: three.js and the renderer
250
+ import * as THREE from 'three';
251
+ Root({ id: 'stage', runtime: 'worker', worker: () => new Worker(url, { type: 'module' }), target, renderer: $three(THREE) });
252
+ ```
253
+
254
+ - `construct: CONSTRUCT` sends each element's constructor `params` (and
255
+ `slot`, `dispose`) with it, and each compiled template instance's, so
256
+ the main thread builds the same objects. `<existing of>` holds an
257
+ object made on the main thread, so it belongs to the direct runtime.
258
+ - `$animate` runs in the worker on its own frames, with a `Tick` whose
259
+ renderer, scene and camera are null: animate through signals
260
+ (`rotation-y={angle}`), whose writes cross like any update.
261
+ - Pointer events are raycast on the main thread and cross with their canvas
262
+ `x` and `y`, buttons and modifier keys; refs in the worker are handles.
263
+ - Importing `CONSTRUCT` and `$animate` brings no renderer code into a worker
264
+ bundle (`$animate` recognizes the renderer by its shape).
265
+
266
+ ## Tests
267
+
268
+ ```sh
269
+ (cd ../../../rrx-core/wtree/_repo_ && npm install) # rrx-core, ren-core, ren-canv, then ren-dom: their installs generate the types checked here
270
+ (cd ../../../ren-core/wtree/_repo_ && npm install)
271
+ (cd ../../../ren-canv/wtree/_repo_ && npm install)
272
+ (cd ../../../ren-dom/wtree/_repo_ && npm install)
273
+ npm install # ren-core, rrx-core, ren-canv and ren-dom (linked), pinned devDependencies: three, esbuild, happy-dom, typescript
274
+ npm test # declarations, tsc --checkJs --strict, the TypeScript consumer, node:test twice
275
+ ```
276
+
277
+ `test/three.test.jsx` builds and raycasts real three.js scenes in node (no
278
+ WebGL: a drawer stub counts frames): classes, params, slots and
279
+ properties; reactive properties and regions with one render per changed
280
+ flush; resource disposal of removed objects, not of moved ones or
281
+ `<existing>` ones; raycast events with bubbling and hover; `$animate`; a
282
+ `main` camera and resizing; the refusals; and a three.js root nested
283
+ in a ren-dom view with props; and worker mode (the application served over
284
+ a MessageChannel: params, props, raycast clicks answered by the worker,
285
+ `$animate` in the worker). ren-show checks both on WebGL in headless
286
+ Chrome, the worker mode with a real Worker. `test/primitives.test.jsx`
287
+ renders the portable primitives through ren-canv, in the overlay and on a
288
+ panel: their layout, text, input and a layout worker.
289
+ `test/viewport.test.jsx` checks `$viewport` and `$media` against the
290
+ canvas's size as it resizes, directly and in worker mode.
291
+ `test/deps.test.js` checks that `src/` reaches no module a dependent does not install.
292
+ `test/timers.test.js` fails a timer call that does not say why it is one,
293
+ and `test/exports.test.js` locks the public contract to
294
+ `test/exports.lock.json`. `npm test` runs the
295
+ suites with the automatic runtime (`ren-core/loader`) and compiled by
296
+ ren-core/esbuild (`ren-core/compiled`).
297
+
298
+ ## Benchmarks
299
+
300
+ The harnesses live in [ren-bench](https://github.com/wiltonlazary/ren-bench).
301
+ Run benchmark commands from that checkout.
302
+
303
+ [ren-bench's three.js benchmark](https://github.com/wiltonlazary/ren-bench/blob/main/bench/r3f/README.md) measures ren-three
304
+ against a market renderer on the same scene
305
+ (1,000 meshes in a keyed list, created, recolored, reordered, removed), as
306
+ each library's users write it; its README has the method and the current
307
+ results.
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@maxenve/ren-three",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The three.js renderer for ren-core: JSX over a three.js scene graph, with raycast events, on-demand rendering and a frame loop.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wiltonlazary/ren-three.git"
10
+ },
11
+ "homepage": "https://github.com/wiltonlazary/ren-three#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/wiltonlazary/ren-three/issues"
14
+ },
15
+ "sideEffects": false,
16
+ "engines": {
17
+ "node": ">=22.15"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "types",
22
+ "README.md"
23
+ ],
24
+ "types": "./types/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./types/index.d.ts",
28
+ "default": "./src/index.js"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "types": "node scripts/types.mjs",
33
+ "check": "tsc -p tsconfig.json",
34
+ "test": "node scripts/types.mjs && tsc -p tsconfig.json && tsc -p test/types/tsconfig.json && node --import ren-core/loader --test test/*.test.js test/*.test.jsx && node --import ren-core/compiled --test test/*.test.jsx",
35
+ "prepare": "node scripts/types.mjs",
36
+ "prepack": "node scripts/types.mjs"
37
+ },
38
+ "dependencies": {
39
+ "ren-canv": "npm:@maxenve/ren-canv@0.1.0",
40
+ "ren-core": "npm:@maxenve/ren-core@0.1.0"
41
+ },
42
+ "peerDependencies": {
43
+ "three": ">=0.160"
44
+ },
45
+ "devDependencies": {
46
+ "esbuild": "0.28.2",
47
+ "happy-dom": "20.14.5",
48
+ "rrx-core": "npm:@maxenve/rrx-core@0.1.0",
49
+ "ren-dom": "npm:@maxenve/ren-dom@0.1.0",
50
+ "three": "0.186.1",
51
+ "typescript": "7.0.2"
52
+ }
53
+ }