@expofp/renderer 3.42.0 → 3.43.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/dist/Renderer.js +26 -1
- package/dist/events/pointer.d.ts +18 -1
- package/dist/events/pointer.js +43 -0
- package/dist/space/Scene.js +4 -0
- package/dist/space/clipping.d.ts +17 -0
- package/dist/space/clipping.js +3 -2
- package/dist/types/events.d.ts +4 -2
- package/package.json +2 -2
package/dist/Renderer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { StrictMode, Suspense } from 'react';
|
|
2
|
+
import { StrictMode, Suspense, useEffect, useRef, } from 'react';
|
|
3
3
|
import { Controls } from './controls';
|
|
4
4
|
import { CanvasWithDebug, DebugOverlay } from './debug';
|
|
5
5
|
import { PointerMissed } from './events/PointerMissed';
|
|
@@ -27,6 +27,27 @@ export function Renderer(props) {
|
|
|
27
27
|
const { debug, scene, memoryLimit, minZoom, maxZoom, minPitch, maxPitch, bounds, smoothTime, controls, lighting, visibleRect, onPointerMissed, gl, requestFrame, onFrame, children, ...canvasProps } = props;
|
|
28
28
|
const cameraProps = { minZoom, maxZoom, minPitch, maxPitch, bounds, smoothTime };
|
|
29
29
|
const external = gl !== undefined;
|
|
30
|
+
// The context this mount created, released here rather than left to R3F. `<Canvas>`'s teardown
|
|
31
|
+
// (`unmountComponentAtNode`) defers `forceContextLoss()` behind a 500 ms `setTimeout`, inside a
|
|
32
|
+
// `try` that disconnects events first — so a host that unmounts the renderer holds a full
|
|
33
|
+
// viewport of GPU memory for half a second after it thought the surface was gone, and holds it
|
|
34
|
+
// forever if anything ahead of the loss in that block throws. An SDK `destroy()` that returns
|
|
35
|
+
// while the plan still owns a WebGL2 context is the same bug on a timer, and a plan swap pays it
|
|
36
|
+
// as a real transient: the incoming plan's context is created while the outgoing one still
|
|
37
|
+
// holds its own. R3F's deferred pass still runs afterwards and is unharmed — its remaining work
|
|
38
|
+
// (`renderLists.dispose()`, `dispose(scene)`) is no-ops on a lost context.
|
|
39
|
+
//
|
|
40
|
+
// Safe as a plain unmount effect only because `<StrictMode>` starts *below* here, wrapping the
|
|
41
|
+
// canvas rather than this component: nothing simulates a remount at this level, so the cleanup
|
|
42
|
+
// runs when the renderer is actually going away. External mode never fills the ref — the host
|
|
43
|
+
// owns that context, and `ExternalCanvas` neuters `forceContextLoss` for the same reason.
|
|
44
|
+
const rendererRef = useRef(undefined);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return () => {
|
|
47
|
+
rendererRef.current?.forceContextLoss();
|
|
48
|
+
rendererRef.current = undefined;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
30
51
|
// The one tree, whichever canvas it lands on. The mount decides nothing about it; the tree
|
|
31
52
|
// leaves out what external mode has no object for — the controls here, the camera inside Scene.
|
|
32
53
|
const tree = (_jsx(DebugOverlay, { enabled: debug, children: _jsxs(Scene, { sceneDef: scene, external: external, memoryLimit: memoryLimit, lighting: lighting, visibleRect: visibleRect, ...cameraProps, children: [!external && _jsx(Controls, { config: controls }), _jsx(PointerMissed, { onPointerMissed: onPointerMissed }), children] }) }));
|
|
@@ -59,7 +80,11 @@ export function Renderer(props) {
|
|
|
59
80
|
// a compile synchronous: 17 programs cost 64 ms of main thread on a large scene, in
|
|
60
81
|
// blocks of up to 12 ms, and they land in the frames where the layers are still mounting.
|
|
61
82
|
// Debug keeps the check, because a broken shader has to report itself somewhere.
|
|
83
|
+
//
|
|
84
|
+
// The root state is also this mount's one handle on the renderer, which the unmount
|
|
85
|
+
// effect above needs to release its context.
|
|
62
86
|
onCreated: (state) => {
|
|
63
87
|
state.gl.debug.checkShaderErrors = debug ?? false;
|
|
88
|
+
rendererRef.current = state.gl;
|
|
64
89
|
}, frameloop: debug ? 'always' : 'demand', resize: { debounce: 50 }, children: tree }) }) }));
|
|
65
90
|
}
|
package/dist/events/pointer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ThreeEvent } from '@react-three/fiber';
|
|
1
|
+
import { type RootState, type ThreeEvent } from '@react-three/fiber';
|
|
2
2
|
import { type Intersection, type Object3D, type Ray, type Raycaster } from 'three';
|
|
3
3
|
import { type PlanPoint, type Space } from '../space/Space';
|
|
4
4
|
import { type LayerDef, type RenderableDef } from '../types/defs';
|
|
@@ -100,6 +100,23 @@ export declare function resolveHit(hit: Intersection): RenderableDef | undefined
|
|
|
100
100
|
* @returns The nearest geometry under the pointer, or — where there is none — the floor beneath it.
|
|
101
101
|
*/
|
|
102
102
|
export declare function missedPoint(raycaster: Raycaster, scene: Object3D, space: Space): PlanPoint;
|
|
103
|
+
/**
|
|
104
|
+
* Drops the hits that geometry with no handlers hides, so a booth behind a wall or a model, opaque
|
|
105
|
+
* or translucent, is not picked. R3F's `events.filter`, which runs on its nearest-first hits.
|
|
106
|
+
*
|
|
107
|
+
* R3F raycasts only interactive layers, so it cannot see what stands in front of them. This casts
|
|
108
|
+
* the same ray at the whole scene and cuts the hits at the nearest object no interactive layer
|
|
109
|
+
* holds. A wall and a logo on a booth's roof differ only in distance: the logo lies on the roof, and
|
|
110
|
+
* the wall stands apart from it. So the cut keeps a hit up to {@link ELEVATION_PRECISION} behind
|
|
111
|
+
* the occluder, the least separation at which the depth buffer draws two surfaces in order. Nearer
|
|
112
|
+
* than that, the occluder is drawn on the hit, not in front of it. A click with every hit dropped is
|
|
113
|
+
* a miss, as a click on the occluder was before.
|
|
114
|
+
* @param hits - R3F's hits, nearest first.
|
|
115
|
+
* @param state - The root's state, holding the ray already aimed at the pointer.
|
|
116
|
+
* @param space - The scene's space, whose scale turns the plan-unit separation into a world one.
|
|
117
|
+
* @returns The hits in front of the nearest occluder.
|
|
118
|
+
*/
|
|
119
|
+
export declare function occludeHits(hits: Intersection[], state: RootState, space: Space): Intersection[];
|
|
103
120
|
/**
|
|
104
121
|
* Where a ray crosses the plan plane — the floor under the pointer, whatever is drawn above it.
|
|
105
122
|
*
|
package/dist/events/pointer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ELEVATION_PRECISION } from '../space/clipping';
|
|
1
2
|
import { useSpace } from '../space/Space';
|
|
2
3
|
/**
|
|
3
4
|
* The props that make a layer's group interactive: one dispatching handler per handler the def
|
|
@@ -128,6 +129,35 @@ export function missedPoint(raycaster, scene, space) {
|
|
|
128
129
|
return space.worldToPlan(nearest.point);
|
|
129
130
|
return planePoint(raycaster.ray, space);
|
|
130
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Drops the hits that geometry with no handlers hides, so a booth behind a wall or a model, opaque
|
|
134
|
+
* or translucent, is not picked. R3F's `events.filter`, which runs on its nearest-first hits.
|
|
135
|
+
*
|
|
136
|
+
* R3F raycasts only interactive layers, so it cannot see what stands in front of them. This casts
|
|
137
|
+
* the same ray at the whole scene and cuts the hits at the nearest object no interactive layer
|
|
138
|
+
* holds. A wall and a logo on a booth's roof differ only in distance: the logo lies on the roof, and
|
|
139
|
+
* the wall stands apart from it. So the cut keeps a hit up to {@link ELEVATION_PRECISION} behind
|
|
140
|
+
* the occluder, the least separation at which the depth buffer draws two surfaces in order. Nearer
|
|
141
|
+
* than that, the occluder is drawn on the hit, not in front of it. A click with every hit dropped is
|
|
142
|
+
* a miss, as a click on the occluder was before.
|
|
143
|
+
* @param hits - R3F's hits, nearest first.
|
|
144
|
+
* @param state - The root's state, holding the ray already aimed at the pointer.
|
|
145
|
+
* @param space - The scene's space, whose scale turns the plan-unit separation into a world one.
|
|
146
|
+
* @returns The hits in front of the nearest occluder.
|
|
147
|
+
*/
|
|
148
|
+
export function occludeHits(hits, state, space) {
|
|
149
|
+
if (hits.length === 0)
|
|
150
|
+
return hits;
|
|
151
|
+
const { interaction } = state.internal;
|
|
152
|
+
const occluder = state.raycaster
|
|
153
|
+
.intersectObject(state.scene, true)
|
|
154
|
+
.find((hit) => !isInteractive(hit.object, interaction));
|
|
155
|
+
if (!occluder)
|
|
156
|
+
return hits;
|
|
157
|
+
// Along the ray, a pitched view only lengthens the gap between two surfaces, never shortens it.
|
|
158
|
+
const cut = occluder.distance + ELEVATION_PRECISION * space.scale;
|
|
159
|
+
return hits.filter((hit) => hit.distance <= cut);
|
|
160
|
+
}
|
|
131
161
|
/**
|
|
132
162
|
* Where a ray crosses the plan plane — the floor under the pointer, whatever is drawn above it.
|
|
133
163
|
*
|
|
@@ -155,6 +185,19 @@ export function planePoint(ray, space) {
|
|
|
155
185
|
function isDrag(event) {
|
|
156
186
|
return event.delta * event.delta > DRAG_SLOP_SQ_PX;
|
|
157
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Whether an object sits in an interactive layer: it, or an ancestor, carries R3F handlers.
|
|
190
|
+
* @param object - The object hit.
|
|
191
|
+
* @param interaction - R3F's objects with handlers.
|
|
192
|
+
* @returns Whether R3F raycasts it for events.
|
|
193
|
+
*/
|
|
194
|
+
function isInteractive(object, interaction) {
|
|
195
|
+
for (let node = object; node; node = node.parent) {
|
|
196
|
+
if (interaction.includes(node))
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
158
201
|
/**
|
|
159
202
|
* Squared pointer travel, in px², above which a release is a drag and not a click — so ~3.9 px of
|
|
160
203
|
* slop. Squared because that is how the imperative renderer's `dragThreshold` compared it, and this
|
package/dist/space/Scene.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useThree } from '@react-three/fiber';
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import { LayoutDriver } from '../events/LayoutDriver';
|
|
5
|
+
import { occludeHits } from '../events/pointer';
|
|
5
6
|
import { Background } from '../objects/Background';
|
|
6
7
|
import { TextureBudget, TextureBudgetContext } from '../objects/images/texture-budget';
|
|
7
8
|
import { Layer } from '../objects/layer/index';
|
|
@@ -83,6 +84,7 @@ function SceneGraph(props) {
|
|
|
83
84
|
const { viewbox, staticTransform, rootLayer } = sceneDef;
|
|
84
85
|
const { width, height, dpr } = useCanvasSize();
|
|
85
86
|
const invalidate = useThree((state) => state.invalidate);
|
|
87
|
+
const setEvents = useThree((state) => state.setEvents);
|
|
86
88
|
const space = useSpace();
|
|
87
89
|
const [scheduler] = useState(createMountScheduler);
|
|
88
90
|
// One budget for the scene's lifetime; the limit is synced rather than the budget rebuilt, so an
|
|
@@ -117,6 +119,8 @@ function SceneGraph(props) {
|
|
|
117
119
|
if (!memoryLimit)
|
|
118
120
|
logger.debug('no memory limit set, image atlases upload at full size');
|
|
119
121
|
}, [budget, memoryLimit]);
|
|
122
|
+
// Here rather than on either mount, so the renderer's own canvas and a host's both pick through it.
|
|
123
|
+
useEffect(() => setEvents({ filter: (hits, state) => occludeHits(hits, state, space) }), [setEvents, space]);
|
|
120
124
|
useEffect(() => {
|
|
121
125
|
printScene(sceneDef, logger.debug);
|
|
122
126
|
}, [sceneDef]);
|
package/dist/space/clipping.d.ts
CHANGED
|
@@ -52,4 +52,21 @@ export declare function nearPlane(distance: number, scale: number): number | und
|
|
|
52
52
|
* @returns The far plane in world units.
|
|
53
53
|
*/
|
|
54
54
|
export declare function farPlane(maxDistance: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Minimum z-axis separation between overlapping surfaces, in **plan** units. Picking reads it too:
|
|
57
|
+
* geometry closer than this to a hit is drawn on it, so it does not occlude it (see `occludeHits`).
|
|
58
|
+
*
|
|
59
|
+
* **For plan data providers:** shapes sharing an x/y footprint — an elevated platform over a floor
|
|
60
|
+
* area, layered decorations at one position — must be separated by at least this much on z to be
|
|
61
|
+
* ordered correctly. No minimum horizontal separation is required: screen-pixel density and depth
|
|
62
|
+
* precision both scale with camera distance and with the scene's scale, so any separation visible on
|
|
63
|
+
* screen already has depth margin, at any viewport size, zoom, scale or camera rotation.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately the renderer's own constant rather than `@expofp/geometry`'s `ELEVATION_TOLERANCE`,
|
|
66
|
+
* which happens to hold the same number. They answer different questions — geometry's is "are these
|
|
67
|
+
* two elevations the same point?", this one is "must these two elevations still draw in the right
|
|
68
|
+
* order?" — and importing it would let a tuning of the geometry predicate silently re-tune the depth
|
|
69
|
+
* buffer.
|
|
70
|
+
*/
|
|
71
|
+
export declare const ELEVATION_PRECISION = 0.1;
|
|
55
72
|
//# sourceMappingURL=clipping.d.ts.map
|
package/dist/space/clipping.js
CHANGED
|
@@ -72,7 +72,8 @@ export function farPlane(maxDistance) {
|
|
|
72
72
|
return Math.max(maxDistance, 0.1) * 2;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* Minimum z-axis separation between overlapping surfaces, in **plan** units.
|
|
75
|
+
* Minimum z-axis separation between overlapping surfaces, in **plan** units. Picking reads it too:
|
|
76
|
+
* geometry closer than this to a hit is drawn on it, so it does not occlude it (see `occludeHits`).
|
|
76
77
|
*
|
|
77
78
|
* **For plan data providers:** shapes sharing an x/y footprint — an elevated platform over a floor
|
|
78
79
|
* area, layered decorations at one position — must be separated by at least this much on z to be
|
|
@@ -86,7 +87,7 @@ export function farPlane(maxDistance) {
|
|
|
86
87
|
* order?" — and importing it would let a tuning of the geometry predicate silently re-tune the depth
|
|
87
88
|
* buffer.
|
|
88
89
|
*/
|
|
89
|
-
const ELEVATION_PRECISION = 0.1;
|
|
90
|
+
export const ELEVATION_PRECISION = 0.1;
|
|
90
91
|
/**
|
|
91
92
|
* Fraction of the camera's distance kept clear of the near plane at close and default zoom, so
|
|
92
93
|
* depth-buffer levels are not spent on the empty space between the camera and the plan.
|
package/dist/types/events.d.ts
CHANGED
|
@@ -70,8 +70,10 @@ export interface PointerMissedEventData {
|
|
|
70
70
|
* defs, arrive in the order their slots were claimed (mount order), not paint order;
|
|
71
71
|
* `Pointer/Overlap` pins that.
|
|
72
72
|
*
|
|
73
|
-
* **
|
|
74
|
-
*
|
|
73
|
+
* **Everything else occludes.** Geometry in a layer with no handlers is never hit itself, but it
|
|
74
|
+
* hides the defs behind it, opaque or translucent: a booth behind a wall or a model receives no
|
|
75
|
+
* event, and a click there is a miss. Geometry coplanar with a def, such as its label, does not
|
|
76
|
+
* hide it.
|
|
75
77
|
*
|
|
76
78
|
* **Handlers return nothing.** One that mutates defs calls `update()` afterward, like any other code.
|
|
77
79
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/renderer",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.43.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/expofp/efp-app.git",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"troika-three-text": "^0.52.5",
|
|
43
43
|
"tslib": "^2.3.0",
|
|
44
44
|
"webgl-memory": "^1.1.3",
|
|
45
|
-
"@expofp/geometry": "3.
|
|
45
|
+
"@expofp/geometry": "3.43.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@react-three/test-renderer": "^9.1.0",
|