react-x11 0.0.1 → 1.2.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,223 @@
1
+ // Geometry generators for the primitive `<*Geometry>` elements. Each takes
2
+ // the same `args` tuple as its three.js counterpart and returns plain
3
+ // arrays — positions, normals, uvs and a triangle index — which the display
4
+ // list compiler turns into one server-side list (src/scene3d.js).
5
+
6
+ /**
7
+ * Build a parametric surface as a (segmentsX+1) x (segmentsY+1) vertex grid.
8
+ * `point(u, v)` returns [position, normal] for u, v in 0..1.
9
+ */
10
+ function grid(segmentsX, segmentsY, point) {
11
+ const positions = [];
12
+ const normals = [];
13
+ const uvs = [];
14
+ const index = [];
15
+ for (let iy = 0; iy <= segmentsY; iy++) {
16
+ for (let ix = 0; ix <= segmentsX; ix++) {
17
+ const u = ix / segmentsX;
18
+ const v = iy / segmentsY;
19
+ const [p, n] = point(u, v);
20
+ positions.push(p[0], p[1], p[2]);
21
+ normals.push(n[0], n[1], n[2]);
22
+ uvs.push(u, 1 - v);
23
+ }
24
+ }
25
+ const stride = segmentsX + 1;
26
+ for (let iy = 0; iy < segmentsY; iy++) {
27
+ for (let ix = 0; ix < segmentsX; ix++) {
28
+ const a = iy * stride + ix;
29
+ const b = a + 1;
30
+ const c = a + stride;
31
+ const d = c + 1;
32
+ index.push(a, c, b, b, c, d);
33
+ }
34
+ }
35
+ return { positions, normals, uvs, index };
36
+ }
37
+
38
+ function merge(parts) {
39
+ const out = { positions: [], normals: [], uvs: [], index: [] };
40
+ for (const part of parts) {
41
+ const offset = out.positions.length / 3;
42
+ out.positions.push(...part.positions);
43
+ out.normals.push(...part.normals);
44
+ out.uvs.push(...part.uvs);
45
+ for (const i of part.index) out.index.push(i + offset);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ export function planeGeometry([width = 1, height = 1, ws = 1, hs = 1] = []) {
51
+ return grid(ws, hs, (u, v) => [
52
+ [(u - 0.5) * width, (0.5 - v) * height, 0],
53
+ [0, 0, 1],
54
+ ]);
55
+ }
56
+
57
+ export function boxGeometry([
58
+ width = 1,
59
+ height = 1,
60
+ depth = 1,
61
+ ws = 1,
62
+ hs = 1,
63
+ ds = 1,
64
+ ] = []) {
65
+ // one grid per face, oriented by (right, up, normal) basis vectors
66
+ const face = (right, up, normal, w, h, segU, segV, offset) =>
67
+ grid(segU, segV, (u, v) => {
68
+ const su = (u - 0.5) * w;
69
+ const sv = (0.5 - v) * h;
70
+ return [
71
+ [
72
+ right[0] * su + up[0] * sv + normal[0] * offset,
73
+ right[1] * su + up[1] * sv + normal[1] * offset,
74
+ right[2] * su + up[2] * sv + normal[2] * offset,
75
+ ],
76
+ normal,
77
+ ];
78
+ });
79
+
80
+ const x = width / 2;
81
+ const y = height / 2;
82
+ const z = depth / 2;
83
+ return merge([
84
+ face([0, 0, 1], [0, 1, 0], [-1, 0, 0], depth, height, ds, hs, x), // -x
85
+ face([0, 0, -1], [0, 1, 0], [1, 0, 0], depth, height, ds, hs, x), // +x
86
+ face([1, 0, 0], [0, 0, -1], [0, 1, 0], width, depth, ws, ds, y), // +y
87
+ face([1, 0, 0], [0, 0, 1], [0, -1, 0], width, depth, ws, ds, y), // -y
88
+ face([-1, 0, 0], [0, 1, 0], [0, 0, -1], width, height, ws, hs, z), // -z
89
+ face([1, 0, 0], [0, 1, 0], [0, 0, 1], width, height, ws, hs, z), // +z
90
+ ]);
91
+ }
92
+
93
+ export function sphereGeometry([radius = 1, ws = 32, hs = 16] = []) {
94
+ return grid(ws, hs, (u, v) => {
95
+ const theta = u * Math.PI * 2;
96
+ const phi = v * Math.PI;
97
+ const n = [
98
+ -Math.sin(phi) * Math.cos(theta),
99
+ Math.cos(phi),
100
+ Math.sin(phi) * Math.sin(theta),
101
+ ];
102
+ return [[n[0] * radius, n[1] * radius, n[2] * radius], n];
103
+ });
104
+ }
105
+
106
+ export function cylinderGeometry([
107
+ radiusTop = 1,
108
+ radiusBottom = 1,
109
+ height = 1,
110
+ radialSegments = 32,
111
+ heightSegments = 1,
112
+ openEnded = false,
113
+ ] = []) {
114
+ const half = height / 2;
115
+ const slope = (radiusBottom - radiusTop) / height;
116
+ const side = grid(radialSegments, heightSegments, (u, v) => {
117
+ const theta = u * Math.PI * 2;
118
+ const radius = radiusTop + (radiusBottom - radiusTop) * v;
119
+ const cos = Math.cos(theta);
120
+ const sin = Math.sin(theta);
121
+ const len = Math.hypot(1, slope) || 1;
122
+ return [
123
+ [radius * sin, half - v * height, radius * cos],
124
+ [sin / len, slope / len, cos / len],
125
+ ];
126
+ });
127
+ if (openEnded) return side;
128
+
129
+ // caps: a triangle fan around the centre, expressed as a 1-segment grid
130
+ const cap = (radius, y, dir) =>
131
+ grid(radialSegments, 1, (u, v) => {
132
+ const theta = u * Math.PI * 2 * dir;
133
+ const r = v * radius;
134
+ return [
135
+ [r * Math.sin(theta), y, r * Math.cos(theta)],
136
+ [0, dir, 0],
137
+ ];
138
+ });
139
+ const parts = [side];
140
+ if (radiusTop > 0) parts.push(cap(radiusTop, half, 1));
141
+ if (radiusBottom > 0) parts.push(cap(radiusBottom, -half, -1));
142
+ return merge(parts);
143
+ }
144
+
145
+ export function torusGeometry([
146
+ radius = 1,
147
+ tube = 0.4,
148
+ radialSegments = 12,
149
+ tubularSegments = 48,
150
+ ] = []) {
151
+ return grid(tubularSegments, radialSegments, (u, v) => {
152
+ const theta = u * Math.PI * 2;
153
+ const phi = v * Math.PI * 2;
154
+ const cx = radius * Math.cos(theta);
155
+ const cz = radius * Math.sin(theta);
156
+ const n = [
157
+ Math.cos(theta) * Math.cos(phi),
158
+ Math.sin(phi),
159
+ Math.sin(theta) * Math.cos(phi),
160
+ ];
161
+ return [[cx + tube * n[0], tube * n[1], cz + tube * n[2]], n];
162
+ });
163
+ }
164
+
165
+ export const GEOMETRY_BUILDERS = {
166
+ boxGeometry,
167
+ planeGeometry,
168
+ sphereGeometry,
169
+ cylinderGeometry,
170
+ torusGeometry,
171
+ };
172
+
173
+ /** `<bufferGeometry position={…} normal={…} uv={…} index={…} />` */
174
+ export function bufferGeometry(props) {
175
+ const positions = props.position ?? props.positions ?? [];
176
+ const count = positions.length / 3;
177
+ let normals = props.normal ?? props.normals;
178
+ const index = props.index ?? null;
179
+ if (!normals) normals = faceNormals(positions, index, count);
180
+ return {
181
+ positions,
182
+ normals,
183
+ uvs: props.uv ?? props.uvs ?? new Array(count * 2).fill(0),
184
+ index,
185
+ };
186
+ }
187
+
188
+ /** Flat normals from the triangles themselves, averaged per vertex. */
189
+ function faceNormals(positions, index, count) {
190
+ const normals = new Array(count * 3).fill(0);
191
+ const tri = index ?? Array.from({ length: count }, (_, i) => i);
192
+ for (let i = 0; i + 2 < tri.length; i += 3) {
193
+ const [a, b, c] = [tri[i], tri[i + 1], tri[i + 2]];
194
+ const ax = positions[a * 3];
195
+ const ay = positions[a * 3 + 1];
196
+ const az = positions[a * 3 + 2];
197
+ const ux = positions[b * 3] - ax;
198
+ const uy = positions[b * 3 + 1] - ay;
199
+ const uz = positions[b * 3 + 2] - az;
200
+ const vx = positions[c * 3] - ax;
201
+ const vy = positions[c * 3 + 1] - ay;
202
+ const vz = positions[c * 3 + 2] - az;
203
+ const nx = uy * vz - uz * vy;
204
+ const ny = uz * vx - ux * vz;
205
+ const nz = ux * vy - uy * vx;
206
+ for (const v of [a, b, c]) {
207
+ normals[v * 3] += nx;
208
+ normals[v * 3 + 1] += ny;
209
+ normals[v * 3 + 2] += nz;
210
+ }
211
+ }
212
+ for (let i = 0; i < normals.length; i += 3) {
213
+ const len = Math.hypot(normals[i], normals[i + 1], normals[i + 2]);
214
+ if (len > 0) {
215
+ normals[i] /= len;
216
+ normals[i + 1] /= len;
217
+ normals[i + 2] /= len;
218
+ } else {
219
+ normals[i + 1] = 1;
220
+ }
221
+ }
222
+ return normals;
223
+ }
package/src/glnodes.js ADDED
@@ -0,0 +1,275 @@
1
+ // <glarea>: the one drawn element that owns a real X window.
2
+ //
3
+ // GLX needs a drawable created for a GL-capable visual, and GL output cannot
4
+ // share the parent window's XRender pipeline — so this is a child X window
5
+ // (NEXT_STEPS §4), sized and positioned by the parent's yoga layout like any
6
+ // other drawn node. Everything about the surface is here; the scene graph
7
+ // that draws into it comes later (docs/glx-plan.md).
8
+ import { cssColor } from 'ntk';
9
+
10
+ import { Node } from './nodes.js';
11
+ import { ScenePointer, sceneWantsPointer } from './pointer3d.js';
12
+ import { SceneRenderer } from './scene3d.js';
13
+
14
+ // One visual query per (app, spec): GetFBConfigs is a round trip and every
15
+ // <glarea> in an app wants the same answer.
16
+ const configCache = new WeakMap();
17
+
18
+ export function glxConfig(app, spec) {
19
+ const key = JSON.stringify(spec ?? null);
20
+ let perApp = configCache.get(app);
21
+ if (!perApp) configCache.set(app, (perApp = new Map()));
22
+ let promise = perApp.get(key);
23
+ if (!promise) {
24
+ promise =
25
+ typeof app.chooseGLXConfig === 'function'
26
+ ? app.chooseGLXConfig(spec)
27
+ : Promise.reject(
28
+ new Error(
29
+ 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLXConfig)',
30
+ ),
31
+ );
32
+ perApp.set(key, promise);
33
+ }
34
+ return promise;
35
+ }
36
+
37
+ /** clearColor as a CSS string or an [r, g, b, a] float tuple. */
38
+ function clearColorOf(props) {
39
+ const value = props.clearColor ?? 'black';
40
+ if (Array.isArray(value)) return value.length === 4 ? value : [...value, 1];
41
+ const parsed = cssColor(value);
42
+ return parsed ?? [0, 0, 0, 1];
43
+ }
44
+
45
+ const px = (v) => Math.max(1, Math.round(v || 0));
46
+
47
+ /**
48
+ * `<glarea>` — an OpenGL surface in the layout.
49
+ *
50
+ * ```jsx
51
+ * <glarea flexGrow={1} clearColor="#0b1021" frameLoop="always"
52
+ * onDraw={(gl, { width, height }) => { ... }} />
53
+ * ```
54
+ *
55
+ * Props: layout props as usual, plus
56
+ * - `onDraw(gl, { width, height, node })` — draw a frame. The viewport and
57
+ * the clear are already done; `SwapBuffers` follows.
58
+ * - `onCreated(gl, { width, height, node })` — once, when the context is
59
+ * current: one-time GL state (`Enable(DEPTH_TEST)`, display lists).
60
+ * - `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
61
+ * - `frameLoop` — `'demand'` (default: redraw on prop/size/expose changes)
62
+ * or `'always'` (drive ntk's frame clock continuously).
63
+ * - `glx` — a `chooseGLXConfig` spec, e.g. `{ DEPTH_SIZE: 24 }`.
64
+ *
65
+ * The X child window is stacked above everything drawn in the parent, so 2D
66
+ * content cannot overlap it — put HUD content in a sibling `<popup>`.
67
+ */
68
+ export class GlAreaNode extends Node {
69
+ constructor(props, app) {
70
+ super('glarea', props, app);
71
+ this.window = null;
72
+ this.gl = null;
73
+ this.rect = null; // geometry last sent to the X window
74
+ this._realizing = false;
75
+ this._frameScheduled = false;
76
+ this._created = false;
77
+ this.scene = new SceneRenderer(this);
78
+ this.pointer = new ScenePointer(this);
79
+ this._pointerDirty = true;
80
+ }
81
+
82
+ get isGlArea() {
83
+ return true;
84
+ }
85
+
86
+ _setRoot(root) {
87
+ super._setRoot(root);
88
+ // the owning window may already exist (a <glarea> mounted into a live
89
+ // tree); otherwise WindowNode.realize picks the subtree up
90
+ if (root?.window) this.realize();
91
+ }
92
+
93
+ /** Create the GL child window. Async: the visual comes from the server. */
94
+ realize() {
95
+ if (this.window || this.destroyed || this._realizing) return;
96
+ const parent = this.root?.window;
97
+ if (!parent || typeof this.app?.createWindow !== 'function') return;
98
+ this._realizing = true;
99
+ glxConfig(this.app, this.props.glx).then(
100
+ (config) => {
101
+ this._realizing = false;
102
+ if (this.destroyed || this.window) return;
103
+ this._create(config, parent);
104
+ },
105
+ (err) => {
106
+ this._realizing = false;
107
+ this.error = err;
108
+ this.props.onError?.(err);
109
+ if (!this.props.onError) {
110
+ console.warn(`react-x11: <glarea> has no GL surface: ${err.message}`);
111
+ }
112
+ },
113
+ );
114
+ }
115
+
116
+ _create(config, parent) {
117
+ const rect = this._geometry();
118
+ const wnd = this.app.createWindow({
119
+ parent,
120
+ x: rect.x,
121
+ y: rect.y,
122
+ width: rect.width,
123
+ height: rect.height,
124
+ visual: config.visual,
125
+ depth: config.depth,
126
+ // GL draws into the window itself: no 2d backing pixmap, and the
127
+ // frame clock is ours to drive
128
+ backingStore: false,
129
+ });
130
+ this.window = wnd;
131
+ this.rect = rect;
132
+ this.config = config;
133
+ wnd._reactX11Node = this;
134
+ this.gl = wnd.getContext('opengl', config);
135
+ wnd.on?.('expose', () => this.requestFrame());
136
+ wnd.map?.();
137
+ this.requestFrame();
138
+ }
139
+
140
+ _geometry() {
141
+ return {
142
+ x: Math.round(this.abs.x),
143
+ y: Math.round(this.abs.y),
144
+ width: px(this.abs.width),
145
+ height: px(this.abs.height),
146
+ };
147
+ }
148
+
149
+ absolutize(originX, originY) {
150
+ super.absolutize(originX, originY);
151
+ this._syncGeometry();
152
+ }
153
+
154
+ _syncGeometry() {
155
+ const wnd = this.window;
156
+ if (!wnd) return;
157
+ const rect = this._geometry();
158
+ const prev = this.rect;
159
+ if (
160
+ prev &&
161
+ prev.x === rect.x &&
162
+ prev.y === rect.y &&
163
+ prev.width === rect.width &&
164
+ prev.height === rect.height
165
+ ) {
166
+ return;
167
+ }
168
+ this.rect = rect;
169
+ if (typeof wnd.setState === 'function') wnd.setState(rect);
170
+ else {
171
+ wnd.move?.(rect.x, rect.y);
172
+ wnd.resize?.(rect.width, rect.height);
173
+ }
174
+ this.requestFrame();
175
+ }
176
+
177
+ /** Draw one frame on the child window's next frame tick. */
178
+ requestFrame() {
179
+ if (!this.window || this.destroyed || this._frameScheduled) return;
180
+ this._frameScheduled = true;
181
+ const schedule =
182
+ typeof this.window.requestAnimationFrame === 'function'
183
+ ? (cb) => this.window.requestAnimationFrame(cb)
184
+ : (cb) => setImmediate(cb);
185
+ schedule(() => {
186
+ this._frameScheduled = false;
187
+ this._drawFrame();
188
+ });
189
+ }
190
+
191
+ _drawFrame() {
192
+ const gl = this.gl;
193
+ if (!gl || this.destroyed) return;
194
+ const { width, height } = this.rect;
195
+ const info = { width, height, node: this };
196
+ if (!this._created) {
197
+ this._created = true;
198
+ this.props.onCreated?.(gl, info);
199
+ }
200
+ this._syncPointerListeners();
201
+ gl.Viewport(0, 0, width, height);
202
+ const [r, g, b, a] = clearColorOf(this.props);
203
+ gl.ClearColor(r, g, b, a);
204
+ gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
205
+ this.scene.render(gl, info);
206
+ this.props.onDraw?.(gl, info);
207
+ gl.SwapBuffers();
208
+ if (this.props.frameLoop === 'always') this.requestFrame();
209
+ }
210
+
211
+ applyProps(newProps, oldProps) {
212
+ super.applyProps(newProps, oldProps);
213
+ // onDraw/clearColor are read at frame time, so any update is a new frame
214
+ this.requestFrame();
215
+ }
216
+
217
+ setHidden(hidden) {
218
+ super.setHidden(hidden);
219
+ if (hidden) this.window?.unmap?.();
220
+ else this.window?.map?.();
221
+ }
222
+
223
+ /**
224
+ * X pointer events are only worth selecting when something in the scene
225
+ * listens for them — the r3f rule that only handler-bearing objects take
226
+ * part in picking, applied one level up, to the wire.
227
+ */
228
+ _syncPointerListeners() {
229
+ if (!this._pointerDirty || !this.window || this.pointer.attached) return;
230
+ this._pointerDirty = false;
231
+ if (!sceneWantsPointer(this.children)) return;
232
+ this.pointer.attach(this.window);
233
+ }
234
+
235
+ /** A scene node was added, removed, or gained/lost pointer handlers. */
236
+ _sceneChanged() {
237
+ this._pointerDirty = true;
238
+ this.requestFrame();
239
+ }
240
+
241
+ /** scene children (<mesh>, <group>, …) attach to this surface. */
242
+ insertBefore(child, beforeChild) {
243
+ super.insertBefore(child, beforeChild);
244
+ child._setSurface?.(this);
245
+ this._sceneChanged();
246
+ }
247
+
248
+ removeChild(child) {
249
+ super.removeChild(child);
250
+ this.invalidateGeometry(child);
251
+ this.pointer.forget(child);
252
+ this._sceneChanged();
253
+ }
254
+
255
+ /** A removed geometry's display list is no longer needed server-side. */
256
+ invalidateGeometry(node) {
257
+ if (!node) return;
258
+ if (node.isGeometry) this.scene.forget(this.gl, node);
259
+ for (const child of node.children ?? []) this.invalidateGeometry(child);
260
+ }
261
+
262
+ // the child window covers this rect: nothing to paint into the parent's
263
+ // 2d context, and no drawn children are allowed under it
264
+ paint() {}
265
+
266
+ destroySubtree() {
267
+ if (this.destroyed) return;
268
+ super.destroySubtree();
269
+ this.scene.dispose(this.gl);
270
+ this.gl?.destroy?.();
271
+ this.gl = null;
272
+ this.window?.destroy?.();
273
+ this.window = null;
274
+ }
275
+ }
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ export {
2
+ render,
3
+ createRoot,
4
+ unmountComponentAtNode,
5
+ Renderer,
6
+ } from './Reconciler.js';
7
+ export {
8
+ Select,
9
+ SelectThemeProvider,
10
+ ThemeProvider,
11
+ Button,
12
+ Checkbox,
13
+ Radio,
14
+ RadioGroup,
15
+ Switch,
16
+ ProgressBar,
17
+ Slider,
18
+ Tooltip,
19
+ Dialog,
20
+ MenuBar,
21
+ ContextMenu,
22
+ useAnchor,
23
+ anchorRect,
24
+ centerRect,
25
+ Canvas3D,
26
+ } from './components/index.js';
27
+
28
+ import { render, createRoot, unmountComponentAtNode } from './Reconciler.js';
29
+
30
+ export default { render, createRoot, unmountComponentAtNode };