@maxenve/ren-three 0.0.0-stage → 0.1.1

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