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,158 @@
1
+ // Pointer events for the 3D scene: X pointer events on the <glarea> window,
2
+ // raycast against the CPU-side geometry (raycast3d.js), dispatched to the
3
+ // mesh they hit and bubbled up its ancestors.
4
+ //
5
+ // The surface owns one X window, so this is a second, small event pipeline
6
+ // beside the 2D EventManager rather than part of it: the hit test is a ray,
7
+ // not a rect, and the events carry 3D information.
8
+ import {
9
+ runWithPriority,
10
+ DiscreteEventPriority,
11
+ ContinuousEventPriority,
12
+ } from './priority.js';
13
+ import { hasPointerHandlers, raycast } from './raycast3d.js';
14
+
15
+ /** Does anything in this subtree listen for pointer events? */
16
+ export function sceneWantsPointer(nodes) {
17
+ for (const node of nodes) {
18
+ if (!node.isObject3D) continue;
19
+ if (hasPointerHandlers(node)) return true;
20
+ if (sceneWantsPointer(node.children)) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /** The dispatch path: the mesh that was hit, then its ancestors. */
26
+ function bubblePath(node, surface) {
27
+ const path = [];
28
+ for (let n = node; n && n !== surface; n = n.parent) path.push(n);
29
+ return path;
30
+ }
31
+
32
+ export class ScenePointer {
33
+ constructor(surface) {
34
+ this.surface = surface;
35
+ this.hovered = null;
36
+ this.pressed = null;
37
+ this.attached = false;
38
+ }
39
+
40
+ attach(wnd) {
41
+ if (this.attached || typeof wnd.on !== 'function') return;
42
+ this.attached = true;
43
+ wnd.on('mousedown', (ev) => this._onPointer('pointerdown', ev));
44
+ wnd.on('mouseup', (ev) => this._onPointer('pointerup', ev));
45
+ wnd.on('mousemove', (ev) => this._onPointer('pointermove', ev));
46
+ wnd.on('mouseout', () => this._onLeave());
47
+ }
48
+
49
+ /** The nearest hit under the pointer, or null. */
50
+ _pick(ev) {
51
+ const camera = this.surface.scene.camera;
52
+ if (!camera) return null;
53
+ const [hit] = raycast(this.surface, ev.x, ev.y, camera);
54
+ return hit ?? null;
55
+ }
56
+
57
+ _makeEvent(type, hit, native, target) {
58
+ let stopped = false;
59
+ return {
60
+ type,
61
+ target,
62
+ object: hit?.object ?? null,
63
+ point: hit?.point ?? null,
64
+ distance: hit?.distance ?? null,
65
+ face: hit?.face ?? null,
66
+ uv: hit?.uv ?? null,
67
+ x: native?.x,
68
+ y: native?.y,
69
+ nativeEvent: native,
70
+ surface: this.surface,
71
+ stopPropagation() {
72
+ stopped = true;
73
+ },
74
+ get propagationStopped() {
75
+ return stopped;
76
+ },
77
+ };
78
+ }
79
+
80
+ /** Call `on<Name>` along the bubble path until one stops propagation. */
81
+ _dispatch(name, hit, native, node = hit?.object) {
82
+ if (!node) return null;
83
+ const prop = `on${name[0].toUpperCase()}${name.slice(1)}`;
84
+ let event = null;
85
+ for (const target of bubblePath(node, this.surface)) {
86
+ const handler = target.props?.[prop];
87
+ if (typeof handler !== 'function') continue;
88
+ event = event ?? this._makeEvent(name.toLowerCase(), hit, native, target);
89
+ event.target = target;
90
+ handler(event);
91
+ if (event.propagationStopped) break;
92
+ }
93
+ return event;
94
+ }
95
+
96
+ _onPointer(type, native) {
97
+ const priority =
98
+ type === 'pointermove' ? ContinuousEventPriority : DiscreteEventPriority;
99
+ runWithPriority(priority, () => {
100
+ const hit = this._pick(native);
101
+ if (type === 'pointermove') {
102
+ this._updateHover(hit, native);
103
+ if (hit) this._dispatch('pointerMove', hit, native);
104
+ return;
105
+ }
106
+ if (type === 'pointerdown') {
107
+ this.pressed = hit?.object ?? null;
108
+ if (hit) this._dispatch('pointerDown', hit, native);
109
+ else
110
+ this.surface.props.onPointerMissed?.(
111
+ this._makeEvent('pointermissed', null, native, null),
112
+ );
113
+ return;
114
+ }
115
+ // pointerup: a click is down and up on the same object
116
+ if (hit) this._dispatch('pointerUp', hit, native);
117
+ if (hit && this.pressed === hit.object) {
118
+ this._dispatch('click', hit, native);
119
+ }
120
+ this.pressed = null;
121
+ });
122
+ }
123
+
124
+ /** enter/leave, diffed against the object hovered last time. */
125
+ _updateHover(hit, native) {
126
+ const next = hit?.object ?? null;
127
+ if (next === this.hovered) return;
128
+ const previous = this.hovered;
129
+ this.hovered = next;
130
+ if (previous && !previous.destroyed) {
131
+ this._dispatch('pointerOut', null, native, previous);
132
+ }
133
+ if (next) this._dispatch('pointerOver', hit, native);
134
+ this._applyCursor(next);
135
+ }
136
+
137
+ _applyCursor(node) {
138
+ const wnd = this.surface.window;
139
+ if (typeof wnd?.setCursor !== 'function') return;
140
+ const cursor = node?.props?.cursor ?? null;
141
+ if (cursor === this._cursor) return;
142
+ this._cursor = cursor;
143
+ wnd.setCursor(cursor);
144
+ }
145
+
146
+ _onLeave() {
147
+ if (!this.hovered) return;
148
+ runWithPriority(ContinuousEventPriority, () => {
149
+ this._updateHover(null, null);
150
+ });
151
+ }
152
+
153
+ /** A node left the tree: drop references to it. */
154
+ forget(node) {
155
+ if (this.hovered === node) this.hovered = null;
156
+ if (this.pressed === node) this.pressed = null;
157
+ }
158
+ }
@@ -0,0 +1,39 @@
1
+ // Shared update-priority state between the host config and the event
2
+ // dispatcher. react-reconciler is CJS; default-import and destructure so the
3
+ // named bindings work regardless of cjs-module-lexer's view of the package.
4
+ import ReactReconcilerConstants from 'react-reconciler/constants.js';
5
+
6
+ export const {
7
+ ConcurrentRoot,
8
+ DefaultEventPriority,
9
+ DiscreteEventPriority,
10
+ ContinuousEventPriority,
11
+ NoEventPriority,
12
+ } = ReactReconcilerConstants;
13
+
14
+ let currentUpdatePriority = NoEventPriority;
15
+
16
+ export function getCurrentUpdatePriority() {
17
+ return currentUpdatePriority;
18
+ }
19
+
20
+ export function setCurrentUpdatePriority(priority) {
21
+ currentUpdatePriority = priority;
22
+ }
23
+
24
+ export function resolveUpdatePriority() {
25
+ return currentUpdatePriority !== NoEventPriority
26
+ ? currentUpdatePriority
27
+ : DefaultEventPriority;
28
+ }
29
+
30
+ /** Run fn (an event handler batch) at the given update priority. */
31
+ export function runWithPriority(priority, fn) {
32
+ const previous = currentUpdatePriority;
33
+ currentUpdatePriority = priority;
34
+ try {
35
+ return fn();
36
+ } finally {
37
+ currentUpdatePriority = previous;
38
+ }
39
+ }
@@ -0,0 +1,146 @@
1
+ // Client-side picking for the 3D scene.
2
+ //
3
+ // There is no GPU picking here: reading pixels back over the protocol is a
4
+ // round trip per event, and on XQuartz GL output is not even readable
5
+ // through GetImage. So the ray is cast against the CPU-side geometry — the
6
+ // same arrays the display lists were compiled from — the way three.js does
7
+ // it. Only meshes that (or whose ancestors) have pointer handlers take part,
8
+ // which is r3f's one optimization that matters.
9
+ import {
10
+ invert,
11
+ multiply,
12
+ transformDirection,
13
+ transformPoint,
14
+ } from './mat4.js';
15
+
16
+ const EPSILON = 1e-8;
17
+
18
+ function normalize([x, y, z]) {
19
+ const len = Math.hypot(x, y, z) || 1;
20
+ return [x / len, y / len, z / len];
21
+ }
22
+
23
+ /**
24
+ * Möller–Trumbore. Returns the ray parameter at the intersection, or null.
25
+ * `dir` need not be unit length: `t` is then in the same units as `dir`,
26
+ * which is what keeps object-space hits comparable in world space.
27
+ */
28
+ function intersectTriangle(origin, dir, a, b, c) {
29
+ const e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
30
+ const e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
31
+ const p = [
32
+ dir[1] * e2[2] - dir[2] * e2[1],
33
+ dir[2] * e2[0] - dir[0] * e2[2],
34
+ dir[0] * e2[1] - dir[1] * e2[0],
35
+ ];
36
+ const det = e1[0] * p[0] + e1[1] * p[1] + e1[2] * p[2];
37
+ if (Math.abs(det) < EPSILON) return null;
38
+ const inv = 1 / det;
39
+ const t0 = [origin[0] - a[0], origin[1] - a[1], origin[2] - a[2]];
40
+ const u = (t0[0] * p[0] + t0[1] * p[1] + t0[2] * p[2]) * inv;
41
+ if (u < 0 || u > 1) return null;
42
+ const q = [
43
+ t0[1] * e1[2] - t0[2] * e1[1],
44
+ t0[2] * e1[0] - t0[0] * e1[2],
45
+ t0[0] * e1[1] - t0[1] * e1[0],
46
+ ];
47
+ const v = (dir[0] * q[0] + dir[1] * q[1] + dir[2] * q[2]) * inv;
48
+ if (v < 0 || u + v > 1) return null;
49
+ const t = (e2[0] * q[0] + e2[1] * q[1] + e2[2] * q[2]) * inv;
50
+ return t > EPSILON ? { t, u, v } : null;
51
+ }
52
+
53
+ /**
54
+ * The world-space ray through a pixel of the surface.
55
+ * @returns {{origin: number[], direction: number[]}}
56
+ */
57
+ export function rayThrough(x, y, { width, height, projection, view }) {
58
+ const inverse = invert(multiply(projection, view));
59
+ if (!inverse) return null;
60
+ const ndcX = (2 * x) / width - 1;
61
+ const ndcY = 1 - (2 * y) / height;
62
+ const near = transformPoint(inverse, [ndcX, ndcY, -1]);
63
+ const far = transformPoint(inverse, [ndcX, ndcY, 1]);
64
+ return {
65
+ origin: near,
66
+ direction: normalize([
67
+ far[0] - near[0],
68
+ far[1] - near[1],
69
+ far[2] - near[2],
70
+ ]),
71
+ };
72
+ }
73
+
74
+ const POINTER_PROPS = [
75
+ 'onClick',
76
+ 'onPointerDown',
77
+ 'onPointerUp',
78
+ 'onPointerMove',
79
+ 'onPointerOver',
80
+ 'onPointerOut',
81
+ ];
82
+
83
+ export const hasPointerHandlers = (node) =>
84
+ POINTER_PROPS.some((name) => typeof node.props?.[name] === 'function');
85
+
86
+ /** Meshes worth testing: they, or an ancestor, listen for pointer events. */
87
+ function pickable(nodes, inherited, out = []) {
88
+ for (const node of nodes) {
89
+ if (!node.isObject3D || !node.visible) continue;
90
+ const listening = inherited || hasPointerHandlers(node);
91
+ if (listening && node.kind === 'mesh' && node.geometry) out.push(node);
92
+ pickable(node.children, listening, out);
93
+ }
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Intersect the scene under `surface` with the ray through pixel (x, y).
99
+ * `world` matrices come from the last rendered frame.
100
+ * @returns {Array<{object, distance, point, face}>} nearest first
101
+ */
102
+ export function raycast(surface, x, y, camera) {
103
+ const ray = rayThrough(x, y, camera);
104
+ if (!ray) return [];
105
+ const hits = [];
106
+
107
+ for (const mesh of pickable(surface.children, false)) {
108
+ const world = mesh._world;
109
+ const toObject = world ? invert(world) : null;
110
+ if (!toObject) continue;
111
+ const origin = transformPoint(toObject, ray.origin);
112
+ const direction = transformDirection(toObject, ray.direction);
113
+ const { positions, index } = mesh.geometry.data();
114
+ const count = index ? index.length : positions.length / 3;
115
+ const vertex = (i) => {
116
+ const v = index ? index[i] : i;
117
+ return [positions[v * 3], positions[v * 3 + 1], positions[v * 3 + 2]];
118
+ };
119
+
120
+ let best = null;
121
+ for (let i = 0; i + 2 < count; i += 3) {
122
+ const hit = intersectTriangle(
123
+ origin,
124
+ direction,
125
+ vertex(i),
126
+ vertex(i + 1),
127
+ vertex(i + 2),
128
+ );
129
+ if (hit && (!best || hit.t < best.t)) best = { ...hit, face: i / 3 };
130
+ }
131
+ if (!best) continue;
132
+ hits.push({
133
+ object: mesh,
134
+ distance: best.t,
135
+ point: [
136
+ ray.origin[0] + ray.direction[0] * best.t,
137
+ ray.origin[1] + ray.direction[1] * best.t,
138
+ ray.origin[2] + ray.direction[2] * best.t,
139
+ ],
140
+ face: best.face,
141
+ uv: [best.u, best.v],
142
+ });
143
+ }
144
+
145
+ return hits.sort((a, b) => a.distance - b.distance);
146
+ }