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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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
|
-
}
|