react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/scene3d.js DELETED
@@ -1,683 +0,0 @@
1
- // The 3D scene tree: `<mesh>`, `<group>`, geometries and materials living
2
- // inside a `<glarea>`, drawn through indirect GLX (docs/glx-plan.md).
3
- //
4
- // It is deliberately separate from the 2D drawn tree — no yoga, no painting
5
- // into the parent's 2d context. The rule that shapes everything here is a
6
- // protocol one: GLX encodes no vertex arrays, so geometry can only travel as
7
- // immediate-mode commands. Sending a mesh per frame costs kilobytes per
8
- // frame, so every geometry is compiled into a **server-side display list**
9
- // once and replayed with a single CallList. A frame is then matrices +
10
- // material state + one CallList per mesh, whatever the triangle count.
11
- import { cssColor } from 'ntk';
12
-
13
- import {
14
- GEOMETRY_BUILDERS,
15
- bufferGeometry as buildBufferGeometry,
16
- } from './geometry3d.js';
17
- import {
18
- compose,
19
- identity,
20
- lookAt,
21
- multiply,
22
- orthographic,
23
- perspective,
24
- } from './mat4.js';
25
- import { Node } from './nodes.js';
26
- import { hasPointerHandlers } from './raycast3d.js';
27
-
28
- export const GEOMETRY_KINDS = new Set([
29
- ...Object.keys(GEOMETRY_BUILDERS),
30
- 'bufferGeometry',
31
- ]);
32
- export const MATERIAL_KINDS = new Set([
33
- 'meshBasicMaterial',
34
- 'meshLambertMaterial',
35
- 'meshPhongMaterial',
36
- ]);
37
- export const LIGHT_KINDS = new Set([
38
- 'ambientLight',
39
- 'directionalLight',
40
- 'pointLight',
41
- 'spotLight',
42
- ]);
43
- export const OBJECT_KINDS = new Set(['mesh', 'group', ...LIGHT_KINDS]);
44
- export const SCENE_KINDS = new Set([
45
- ...GEOMETRY_KINDS,
46
- ...MATERIAL_KINDS,
47
- ...OBJECT_KINDS,
48
- ]);
49
-
50
- // fixed-function GL has exactly eight light units
51
- const MAX_LIGHTS = 8;
52
-
53
- /**
54
- * react-three-fiber names that indirect GLX cannot implement — the protocol
55
- * encodes no shaders, no framebuffer objects and no instancing, so these
56
- * fail loudly instead of rendering something that only looks right.
57
- */
58
- export const UNSUPPORTED_KINDS = {
59
- shaderMaterial: 'GLSL shaders: the GLX protocol encodes no shader objects',
60
- rawShaderMaterial: 'GLSL shaders: the GLX protocol encodes no shader objects',
61
- instancedMesh: 'instancing: no vertex arrays or instanced draw commands',
62
- points: 'point clouds need vertex arrays, which GLX does not encode',
63
- line: 'line geometry needs vertex arrays, which GLX does not encode',
64
- effectComposer: 'post-processing needs framebuffer objects',
65
- };
66
-
67
- const asTriple = (value, fallback) => {
68
- if (value == null) return fallback;
69
- if (typeof value === 'number') return [value, value, value];
70
- return [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
71
- };
72
-
73
- /** Base for anything with a transform: `<mesh>`, `<group>`. */
74
- export class Object3DNode extends Node {
75
- constructor(kind, props, app) {
76
- super(kind, props, app, { yoga: false });
77
- this.surface = null; // owning GlAreaNode
78
- this._matrix = identity();
79
- this._matrixDirty = true;
80
- }
81
-
82
- get isObject3D() {
83
- return true;
84
- }
85
-
86
- /** scene nodes are not part of the 2D tree; they follow the surface. */
87
- _setRoot() {}
88
-
89
- _setSurface(surface) {
90
- if (this.surface === surface) return;
91
- this.surface = surface;
92
- for (const child of this.children) child._setSurface?.(surface);
93
- }
94
-
95
- insertBefore(child, beforeChild) {
96
- super.insertBefore(child, beforeChild);
97
- child._setSurface?.(this.surface);
98
- this.surface?._sceneChanged?.();
99
- }
100
-
101
- removeChild(child) {
102
- super.removeChild(child);
103
- this.surface?.invalidateGeometry?.(child);
104
- this.surface?.pointer?.forget(child);
105
- this.surface?._sceneChanged?.();
106
- }
107
-
108
- applyProps(newProps) {
109
- const before = this.props;
110
- this.props = newProps;
111
- this._matrixDirty = true;
112
- // gaining or losing handlers changes whether the surface needs X
113
- // pointer events at all
114
- if (hasPointerHandlers({ props: before }) !== hasPointerHandlers(this)) {
115
- this.surface?._sceneChanged?.();
116
- } else {
117
- this.surface?.requestFrame();
118
- }
119
- }
120
-
121
- setHidden(hidden) {
122
- this.hidden = hidden;
123
- this.surface?.requestFrame();
124
- }
125
-
126
- localMatrix() {
127
- if (this._matrixDirty) {
128
- const p = this.props;
129
- compose(
130
- asTriple(p.position, [0, 0, 0]),
131
- asTriple(p.rotation, [0, 0, 0]),
132
- asTriple(p.scale, [1, 1, 1]),
133
- this._matrix,
134
- );
135
- this._matrixDirty = false;
136
- }
137
- return this._matrix;
138
- }
139
-
140
- get visible() {
141
- return !this.hidden && this.props.visible !== false;
142
- }
143
- }
144
-
145
- export class MeshNode extends Object3DNode {
146
- constructor(props, app) {
147
- super('mesh', props, app);
148
- }
149
-
150
- get geometry() {
151
- return this.children.find((c) => c.isGeometry) ?? null;
152
- }
153
-
154
- get material() {
155
- return this.children.find((c) => c.isMaterial) ?? null;
156
- }
157
- }
158
-
159
- export class GroupNode extends Object3DNode {
160
- constructor(props, app) {
161
- super('group', props, app);
162
- }
163
- }
164
-
165
- /** `<pointLight position={[4, 5, 3]} intensity={1} />` and friends. */
166
- export class LightNode extends Object3DNode {
167
- constructor(kind, props, app) {
168
- super(kind, props, app);
169
- }
170
-
171
- get isLight() {
172
- return true;
173
- }
174
- }
175
-
176
- /** `<boxGeometry args={[1, 1, 1]} />`, `<bufferGeometry position={…} />` */
177
- export class GeometryNode extends Object3DNode {
178
- constructor(kind, props, app) {
179
- super(kind, props, app);
180
- this.version = 0;
181
- this._data = null;
182
- }
183
-
184
- get isObject3D() {
185
- return false;
186
- }
187
-
188
- get isGeometry() {
189
- return true;
190
- }
191
-
192
- applyProps(newProps) {
193
- const before = this.props;
194
- this.props = newProps;
195
- if (!sameGeometryProps(before, newProps)) {
196
- // a new shape: drop the cached arrays and the server-side list
197
- this._data = null;
198
- this.version++;
199
- }
200
- this.surface?.requestFrame();
201
- }
202
-
203
- /** { positions, normals, uvs, index } — memoized per prop version. */
204
- data() {
205
- if (!this._data) {
206
- const build = GEOMETRY_BUILDERS[this.kind];
207
- this._data = build
208
- ? build(this.props.args ?? [])
209
- : buildBufferGeometry(this.props);
210
- }
211
- return this._data;
212
- }
213
- }
214
-
215
- function sameGeometryProps(a, b) {
216
- if (a === b) return true;
217
- const keys = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
218
- for (const key of keys) {
219
- const av = a?.[key];
220
- const bv = b?.[key];
221
- if (av === bv) continue;
222
- if (Array.isArray(av) && Array.isArray(bv)) {
223
- if (av.length !== bv.length || av.some((v, i) => v !== bv[i]))
224
- return false;
225
- continue;
226
- }
227
- return false;
228
- }
229
- return true;
230
- }
231
-
232
- /** `<meshBasicMaterial color="#2980b9" wireframe />` */
233
- export class MaterialNode extends Object3DNode {
234
- constructor(kind, props, app) {
235
- super(kind, props, app);
236
- }
237
-
238
- get isObject3D() {
239
- return false;
240
- }
241
-
242
- get isMaterial() {
243
- return true;
244
- }
245
- }
246
-
247
- export function createSceneNode(kind, props, app) {
248
- if (kind === 'mesh') return new MeshNode(props, app);
249
- if (kind === 'group') return new GroupNode(props, app);
250
- if (LIGHT_KINDS.has(kind)) return new LightNode(kind, props, app);
251
- if (GEOMETRY_KINDS.has(kind)) return new GeometryNode(kind, props, app);
252
- if (MATERIAL_KINDS.has(kind)) return new MaterialNode(kind, props, app);
253
- return null;
254
- }
255
-
256
- // ---------------------------------------------------------------------------
257
- // rendering
258
-
259
- const DEFAULT_CAMERA = {
260
- position: [0, 0, 5],
261
- target: [0, 0, 0],
262
- up: [0, 1, 0],
263
- fov: 50,
264
- near: 0.1,
265
- far: 1000,
266
- zoom: 1,
267
- };
268
-
269
- export function cameraMatrices(spec, { width, height }) {
270
- const camera = { ...DEFAULT_CAMERA, ...spec };
271
- const aspect = width / Math.max(1, height);
272
- const projection = camera.orthographic
273
- ? orthographic(
274
- -aspect / camera.zoom,
275
- aspect / camera.zoom,
276
- -1 / camera.zoom,
277
- 1 / camera.zoom,
278
- camera.near,
279
- camera.far,
280
- )
281
- : perspective(camera.fov, aspect, camera.near, camera.far);
282
- return {
283
- projection,
284
- view: lookAt(camera.position, camera.target, camera.up),
285
- };
286
- }
287
-
288
- const rgba = (color, fallback = [1, 1, 1, 1]) => {
289
- if (!color) return fallback;
290
- if (Array.isArray(color)) return color.length === 4 ? color : [...color, 1];
291
- return cssColor(color) ?? fallback;
292
- };
293
-
294
- /** Lights with their world matrices, in tree order. */
295
- function collectLights(nodes, parentMatrix, out = []) {
296
- for (const node of nodes) {
297
- if (!node.isObject3D || !node.visible) continue;
298
- const world = multiply(parentMatrix, node.localMatrix());
299
- if (node.isLight) out.push({ node, world });
300
- collectLights(node.children, world, out);
301
- }
302
- return out;
303
- }
304
-
305
- const scaled = ([r, g, b], intensity) => [
306
- r * intensity,
307
- g * intensity,
308
- b * intensity,
309
- 1,
310
- ];
311
-
312
- /**
313
- * Per-surface GL bookkeeping: the display list of every geometry, and the
314
- * material state last sent (so an unchanged material costs nothing).
315
- */
316
- export class SceneRenderer {
317
- constructor(surface) {
318
- this.surface = surface;
319
- this.lists = new Map(); // GeometryNode -> { id, version }
320
- this.nextList = 1;
321
- this.textures = new Map(); // image -> texture id
322
- this.nextTexture = 1;
323
- this.materialKey = null;
324
- this.initialized = false;
325
- this.enabledLights = 0;
326
- this.lit = false;
327
- this.warnedLightLimit = false;
328
- }
329
-
330
- /** Draw the scene under `surface`; false when there is nothing 3D to draw. */
331
- render(gl, info) {
332
- const roots = this.surface.children.filter((c) => c.isObject3D);
333
- if (roots.length === 0) return false;
334
-
335
- if (!this.initialized) {
336
- this.initialized = true;
337
- gl.Enable(gl.DEPTH_TEST);
338
- gl.Enable(gl.NORMALIZE); // scaled meshes keep unit-length normals
339
- gl.ShadeModel(gl.SMOOTH);
340
- }
341
- this.materialKey = null;
342
-
343
- const { projection, view } = cameraMatrices(
344
- this.surface.props.camera,
345
- info,
346
- );
347
- // kept for picking: rays are cast against the frame that is on screen
348
- this.camera = { projection, view, width: info.width, height: info.height };
349
- gl.MatrixMode(gl.PROJECTION);
350
- gl.LoadIdentity();
351
- gl.MultMatrixf(projection);
352
- gl.MatrixMode(gl.MODELVIEW);
353
- gl.LoadIdentity();
354
- gl.MultMatrixf(view);
355
-
356
- // light positions are transformed by the modelview matrix in force when
357
- // they are sent, and it is the view matrix right now — so world-space
358
- // positions land in eye space exactly as GL expects
359
- this.applyLights(gl, collectLights(roots, identity()));
360
-
361
- for (const node of roots) this.drawObject(gl, node, identity());
362
- return true;
363
- }
364
-
365
- drawObject(gl, node, parentWorld) {
366
- if (!node.visible) return;
367
- // the world matrix is recorded rather than recomputed later: picking
368
- // rays are transformed by exactly what was drawn
369
- const world = multiply(parentWorld, node.localMatrix());
370
- node._world = world;
371
- gl.PushMatrix();
372
- gl.MultMatrixf(node.localMatrix());
373
- if (node.kind === 'mesh') this.drawMesh(gl, node);
374
- for (const child of node.children) {
375
- if (child.isObject3D) this.drawObject(gl, child, world);
376
- }
377
- gl.PopMatrix();
378
- }
379
-
380
- drawMesh(gl, mesh) {
381
- const geometry = mesh.geometry;
382
- if (!geometry) return;
383
- this.applyMaterial(gl, mesh.material);
384
- gl.CallList(this.listFor(gl, geometry));
385
- }
386
-
387
- /**
388
- * Upload once, bind per frame. Texture pixels are the same problem as
389
- * geometry — kilobytes that must not cross the wire twice — so an ntk
390
- * `Image` is uploaded on first use and only rebound afterwards.
391
- * `map` is an ntk Image, or anything with { width, height, data } in
392
- * RGBA byte order.
393
- */
394
- textureFor(gl, image) {
395
- const cached = this.textures.get(image);
396
- if (cached) return cached;
397
- const id = this.nextTexture++;
398
- gl.BindTexture(gl.TEXTURE_2D, id);
399
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
400
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
401
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
402
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
403
- gl.TexImage2D(
404
- gl.TEXTURE_2D,
405
- 0,
406
- gl.RGBA,
407
- image.width,
408
- image.height,
409
- 0,
410
- gl.RGBA,
411
- gl.UNSIGNED_BYTE,
412
- image.data,
413
- );
414
- this.textures.set(image, id);
415
- return id;
416
- }
417
-
418
- /** Compile once, replay forever — this is the whole point of the design. */
419
- listFor(gl, geometry) {
420
- const cached = this.lists.get(geometry);
421
- if (cached && cached.version === geometry.version) return cached.id;
422
- const id = cached ? cached.id : this.nextList++;
423
- const { positions, normals, uvs, index } = geometry.data();
424
- const count = index ? index.length : positions.length / 3;
425
-
426
- gl.NewList(id, gl.COMPILE);
427
- gl.Begin(gl.TRIANGLES);
428
- for (let i = 0; i < count; i++) {
429
- const v = index ? index[i] : i;
430
- if (normals && normals.length > v * 3 + 2) {
431
- gl.Normal3f(normals[v * 3], normals[v * 3 + 1], normals[v * 3 + 2]);
432
- }
433
- if (uvs && uvs.length > v * 2 + 1) {
434
- gl.TexCoord2f(uvs[v * 2], uvs[v * 2 + 1]);
435
- }
436
- gl.Vertex3f(positions[v * 3], positions[v * 3 + 1], positions[v * 3 + 2]);
437
- }
438
- gl.End();
439
- gl.EndList();
440
-
441
- this.lists.set(geometry, { id, version: geometry.version });
442
- return id;
443
- }
444
-
445
- /**
446
- * Fixed-function GL has eight light units. `<ambientLight>` has no unit of
447
- * its own — its colour is summed into the first light's GL_AMBIENT term
448
- * (and into a dummy unit when it is the only light in the scene).
449
- */
450
- applyLights(gl, lights) {
451
- let ambient = [0, 0, 0];
452
- const units = [];
453
- for (const light of lights) {
454
- const { node } = light;
455
- const intensity = node.props.intensity ?? 1;
456
- const [r, g, b] = rgba(node.props.color, [1, 1, 1, 1]);
457
- if (node.kind === 'ambientLight') {
458
- ambient = [
459
- ambient[0] + r * intensity,
460
- ambient[1] + g * intensity,
461
- ambient[2] + b * intensity,
462
- ];
463
- continue;
464
- }
465
- units.push({ node, intensity, color: [r, g, b] });
466
- }
467
-
468
- if (units.length > MAX_LIGHTS && !this.warnedLightLimit) {
469
- this.warnedLightLimit = true;
470
- console.warn(
471
- `react-x11: ${units.length} lights in the scene; fixed-function GL ` +
472
- `has ${MAX_LIGHTS} light units, so the rest are ignored.`,
473
- );
474
- }
475
- const used = units.slice(0, MAX_LIGHTS);
476
- this.lit = used.length > 0 || ambient.some((c) => c > 0);
477
-
478
- if (!this.lit) {
479
- for (let i = 0; i < this.enabledLights; i++) gl.Disable(gl.LIGHT0 + i);
480
- this.enabledLights = 0;
481
- return;
482
- }
483
-
484
- // ambient-only scenes still need one unit, to carry the ambient term
485
- const count = Math.max(used.length, 1);
486
- for (let i = 0; i < count; i++) {
487
- const unit = gl.LIGHT0 + i;
488
- gl.Enable(unit);
489
- gl.Lightfv(unit, gl.AMBIENT, i === 0 ? [...ambient, 1] : [0, 0, 0, 1]);
490
- const light = used[i];
491
- if (!light) {
492
- // the dummy unit for a scene lit only by <ambientLight>
493
- gl.Lightfv(unit, gl.DIFFUSE, [0, 0, 0, 1]);
494
- gl.Lightfv(unit, gl.SPECULAR, [0, 0, 0, 1]);
495
- gl.Lightfv(unit, gl.POSITION, [0, 0, 1, 0]);
496
- continue;
497
- }
498
- const { node, intensity, color } = light;
499
- const world = lights.find((l) => l.node === node).world;
500
- const position = [world[12], world[13], world[14]];
501
- gl.Lightfv(unit, gl.DIFFUSE, scaled(color, intensity));
502
- gl.Lightfv(
503
- unit,
504
- gl.SPECULAR,
505
- scaled(color, node.props.specular === false ? 0 : intensity),
506
- );
507
-
508
- if (node.kind === 'directionalLight') {
509
- // w = 0: a direction, pointing from the scene toward the light
510
- gl.Lightfv(unit, gl.POSITION, [...position, 0]);
511
- gl.Lightfv(unit, gl.SPOT_DIRECTION, [0, 0, -1, 0]);
512
- gl.Lightfv(unit, gl.SPOT_CUTOFF, 180, 0, 0, 0);
513
- } else {
514
- gl.Lightfv(unit, gl.POSITION, [...position, 1]);
515
- const distance = node.props.distance ?? 0;
516
- // three.js decay=2 is physical falloff; map it onto GL attenuation
517
- const decay = node.props.decay ?? 0;
518
- gl.Lightfv(unit, gl.CONSTANT_ATTENUATION, 1, 0, 0, 0);
519
- gl.Lightfv(
520
- unit,
521
- gl.LINEAR_ATTENUATION,
522
- distance > 0 ? 1 / distance : 0,
523
- 0,
524
- 0,
525
- 0,
526
- );
527
- gl.Lightfv(
528
- unit,
529
- gl.QUADRATIC_ATTENUATION,
530
- decay > 1 && distance > 0 ? 1 / (distance * distance) : 0,
531
- 0,
532
- 0,
533
- 0,
534
- );
535
- if (node.kind === 'spotLight') {
536
- const target = node.props.target ?? [0, 0, 0];
537
- const dir = [
538
- target[0] - position[0],
539
- target[1] - position[1],
540
- target[2] - position[2],
541
- ];
542
- const len = Math.hypot(dir[0], dir[1], dir[2]) || 1;
543
- gl.Lightfv(
544
- unit,
545
- gl.SPOT_DIRECTION,
546
- dir[0] / len,
547
- dir[1] / len,
548
- dir[2] / len,
549
- 0,
550
- );
551
- const angle = node.props.angle ?? Math.PI / 6;
552
- gl.Lightfv(
553
- unit,
554
- gl.SPOT_CUTOFF,
555
- Math.min(90, (angle * 180) / Math.PI),
556
- 0,
557
- 0,
558
- 0,
559
- );
560
- gl.Lightfv(
561
- unit,
562
- gl.SPOT_EXPONENT,
563
- (node.props.penumbra ?? 0) * 128,
564
- 0,
565
- 0,
566
- 0,
567
- );
568
- } else {
569
- gl.Lightfv(unit, gl.SPOT_CUTOFF, 180, 0, 0, 0);
570
- }
571
- }
572
- }
573
- for (let i = count; i < this.enabledLights; i++) gl.Disable(gl.LIGHT0 + i);
574
- this.enabledLights = count;
575
- // the material state depends on whether lighting is on
576
- this.materialKey = null;
577
- }
578
-
579
- /** Material state is per frame, so one geometry list can serve many meshes. */
580
- applyMaterial(gl, material) {
581
- const kind = material?.kind ?? 'meshBasicMaterial';
582
- const props = material?.props ?? {};
583
- const [r, g, b, a] = rgba(props.color);
584
- const opacity = props.opacity ?? 1;
585
- const alpha = a * opacity;
586
- // <meshBasicMaterial> is unlit by definition, and a lit material with no
587
- // light in the scene would render black — fall back to flat colour
588
- const lit = kind !== 'meshBasicMaterial' && this.lit;
589
- // texturing is state like everything else here, so it is part of the key
590
- const map = props.map ?? null;
591
- const key = [
592
- kind,
593
- lit,
594
- r,
595
- g,
596
- b,
597
- alpha,
598
- props.wireframe,
599
- props.side,
600
- props.transparent,
601
- props.shininess,
602
- props.specular,
603
- props.emissive,
604
- map ? (this.textures.get(map) ?? 'new') : 'none',
605
- ].join('|');
606
- if (key === this.materialKey) return;
607
- this.materialKey = key;
608
-
609
- const blended = alpha < 1 || props.transparent;
610
- if (blended) {
611
- gl.Enable(gl.BLEND);
612
- gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
613
- } else {
614
- gl.Disable(gl.BLEND);
615
- }
616
-
617
- if (lit) {
618
- gl.Enable(gl.LIGHTING);
619
- // colour reaches the shading equation as the surface reflectance
620
- gl.Materialfv(gl.FRONT_AND_BACK, gl.AMBIENT_AND_DIFFUSE, [
621
- r,
622
- g,
623
- b,
624
- alpha,
625
- ]);
626
- gl.Materialfv(
627
- gl.FRONT_AND_BACK,
628
- gl.EMISSION,
629
- rgba(props.emissive, [0, 0, 0, 1]),
630
- );
631
- if (kind === 'meshPhongMaterial') {
632
- gl.Materialfv(
633
- gl.FRONT_AND_BACK,
634
- gl.SPECULAR,
635
- rgba(props.specular, [1, 1, 1, 1]),
636
- );
637
- gl.Materialf(gl.FRONT_AND_BACK, gl.SHININESS, props.shininess ?? 30);
638
- } else {
639
- // Lambert: diffuse only
640
- gl.Materialfv(gl.FRONT_AND_BACK, gl.SPECULAR, [0, 0, 0, 1]);
641
- gl.Materialf(gl.FRONT_AND_BACK, gl.SHININESS, 0);
642
- }
643
- // both faces are shaded when both are drawn
644
- gl.LightModelf(gl.LIGHT_MODEL_TWO_SIDE, props.side === 'double' ? 1 : 0);
645
- } else {
646
- gl.Disable(gl.LIGHTING);
647
- if (blended) gl.Color4f(r, g, b, alpha);
648
- else gl.Color3f(r, g, b);
649
- }
650
-
651
- if (map?.width && map?.data) {
652
- gl.Enable(gl.TEXTURE_2D);
653
- gl.BindTexture(gl.TEXTURE_2D, this.textureFor(gl, map));
654
- // MODULATE: the texture is tinted by the colour and the lighting
655
- gl.TexEnvi(gl.TEXTURE_ENV, gl.TEXTURE_ENV_MODE, gl.MODULATE);
656
- } else {
657
- gl.Disable(gl.TEXTURE_2D);
658
- }
659
-
660
- gl.PolygonMode(gl.FRONT_AND_BACK, props.wireframe ? gl.LINE : gl.FILL);
661
- if (props.side === 'double') {
662
- gl.Disable(gl.CULL_FACE);
663
- } else {
664
- gl.Enable(gl.CULL_FACE);
665
- gl.CullFace(props.side === 'back' ? gl.FRONT : gl.BACK);
666
- }
667
- }
668
-
669
- /** Drop a removed geometry's list so the id can be reused. */
670
- forget(gl, node) {
671
- const entry = this.lists.get(node);
672
- if (!entry) return;
673
- this.lists.delete(node);
674
- gl?.DeleteLists?.(entry.id, 1);
675
- }
676
-
677
- dispose(gl) {
678
- for (const { id } of this.lists.values()) gl?.DeleteLists?.(id, 1);
679
- this.lists.clear();
680
- if (this.textures.size) gl?.DeleteTextures?.([...this.textures.values()]);
681
- this.textures.clear();
682
- }
683
- }