@expofp/renderer 3.2.5 → 3.27.1
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 +111 -0
- package/dist/Renderer.d.ts +88 -0
- package/dist/Renderer.js +53 -0
- package/dist/controls/CameraControls.d.ts +102 -0
- package/dist/controls/CameraControls.js +239 -0
- package/dist/controls/camera-api.d.ts +206 -0
- package/dist/controls/camera-api.js +181 -0
- package/dist/controls/camera-move.d.ts +13 -0
- package/dist/controls/camera-move.js +18 -0
- package/dist/controls/camera-state.d.ts +40 -0
- package/dist/controls/camera-state.js +65 -0
- package/dist/controls/gestures.d.ts +70 -0
- package/dist/controls/gestures.js +84 -0
- package/dist/controls/handlers/Inertia.d.ts +111 -0
- package/dist/controls/handlers/Inertia.js +215 -0
- package/dist/controls/handlers/Pan.d.ts +23 -0
- package/dist/controls/handlers/Pan.js +32 -0
- package/dist/controls/handlers/Pitch.d.ts +94 -0
- package/dist/controls/handlers/Pitch.js +191 -0
- package/dist/controls/handlers/Roll.d.ts +103 -0
- package/dist/controls/handlers/Roll.js +182 -0
- package/dist/controls/handlers/Zoom.d.ts +15 -0
- package/dist/controls/handlers/Zoom.js +26 -0
- package/dist/controls/index.d.ts +56 -0
- package/dist/controls/index.js +58 -0
- package/dist/controls/zoom-to.d.ts +89 -0
- package/dist/controls/zoom-to.js +91 -0
- package/dist/core/InstanceUniforms.d.ts +93 -0
- package/dist/core/InstanceUniforms.js +266 -0
- package/dist/core/SquareDataTexture.d.ts +277 -0
- package/dist/core/SquareDataTexture.js +476 -0
- package/dist/core/batch/BatchedMesh.d.ts +74 -0
- package/dist/core/batch/BatchedMesh.js +149 -0
- package/dist/core/instanced/FrustumCulling.d.ts +141 -0
- package/dist/core/instanced/FrustumCulling.js +246 -0
- package/dist/core/instanced/InstancedMesh.d.ts +302 -0
- package/dist/core/instanced/InstancedMesh.js +497 -0
- package/dist/core/instanced/Instances.d.ts +119 -0
- package/dist/core/instanced/Instances.js +230 -0
- package/dist/core/instanced/Raycasting.d.ts +118 -0
- package/dist/core/instanced/Raycasting.js +134 -0
- package/dist/core/instanced/instanced-material.d.ts +77 -0
- package/dist/core/instanced/instanced-material.js +224 -0
- package/dist/core/lines/BatchedLines.d.ts +135 -0
- package/dist/core/lines/BatchedLines.js +213 -0
- package/dist/core/lines/FrustumCulling.d.ts +43 -0
- package/dist/core/lines/FrustumCulling.js +85 -0
- package/dist/core/lines/Raycasting.d.ts +44 -0
- package/dist/core/lines/Raycasting.js +91 -0
- package/dist/core/lines/lines-material.d.ts +47 -0
- package/dist/core/lines/lines-material.js +115 -0
- package/dist/core/shader-source.d.ts +50 -0
- package/dist/core/shader-source.js +84 -0
- package/dist/core/text/BatchedText.d.ts +227 -0
- package/dist/core/text/BatchedText.js +467 -0
- package/dist/core/text/MemberTexture.d.ts +90 -0
- package/dist/core/text/MemberTexture.js +194 -0
- package/dist/core/text/Text.d.ts +73 -0
- package/dist/core/text/Text.js +215 -0
- package/dist/core/text/batched-text-material.d.ts +56 -0
- package/dist/core/text/batched-text-material.js +121 -0
- package/dist/core/text/glyph-packing.d.ts +17 -0
- package/dist/core/text/glyph-packing.js +61 -0
- package/dist/core/typed-array.d.ts +16 -0
- package/dist/core/typed-array.js +20 -0
- package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
- package/dist/debug/components/DebugPanelProvider.js +38 -0
- package/dist/debug/components/DebugTools.d.ts +22 -0
- package/dist/debug/components/DebugTools.js +28 -0
- package/dist/debug/components/Gui.d.ts +20 -0
- package/dist/debug/components/Gui.js +32 -0
- package/dist/debug/components/MemoryInfo.d.ts +22 -0
- package/dist/debug/components/MemoryInfo.js +42 -0
- package/dist/debug/components/Panel.d.ts +31 -0
- package/dist/debug/components/Panel.js +98 -0
- package/dist/debug/components/Stats.d.ts +9 -0
- package/dist/debug/components/Stats.js +24 -0
- package/dist/debug/debug-context.d.ts +41 -0
- package/dist/debug/debug-context.js +59 -0
- package/dist/debug/index.d.ts +51 -0
- package/dist/debug/index.js +64 -0
- package/dist/events/LayoutDriver.d.ts +31 -0
- package/dist/events/LayoutDriver.js +88 -0
- package/dist/events/PointerMissed.d.ts +23 -0
- package/dist/events/PointerMissed.js +58 -0
- package/dist/events/def-culling.d.ts +46 -0
- package/dist/events/def-culling.js +69 -0
- package/dist/events/layout-targets.d.ts +104 -0
- package/dist/events/layout-targets.js +245 -0
- package/dist/events/pointer.d.ts +118 -0
- package/dist/events/pointer.js +168 -0
- package/dist/external/ExternalCanvas.d.ts +56 -0
- package/dist/external/ExternalCanvas.js +178 -0
- package/dist/external/external-camera.d.ts +17 -0
- package/dist/external/external-camera.js +82 -0
- package/dist/external/external-events.d.ts +14 -0
- package/dist/external/external-events.js +33 -0
- package/dist/index.d.ts +11 -783
- package/dist/index.js +12 -8031
- package/dist/objects/Background.d.ts +31 -0
- package/dist/objects/Background.js +37 -0
- package/dist/objects/Lighting.d.ts +20 -0
- package/dist/objects/Lighting.js +85 -0
- package/dist/objects/containers/SlotContainer.d.ts +266 -0
- package/dist/objects/containers/SlotContainer.js +251 -0
- package/dist/objects/containers/hooks.d.ts +46 -0
- package/dist/objects/containers/hooks.js +118 -0
- package/dist/objects/containers/index.d.ts +6 -0
- package/dist/objects/containers/index.js +4 -0
- package/dist/objects/containers/keys.d.ts +10 -0
- package/dist/objects/containers/keys.js +18 -0
- package/dist/objects/containers/mesh-writes.d.ts +106 -0
- package/dist/objects/containers/mesh-writes.js +97 -0
- package/dist/objects/containers/props.d.ts +23 -0
- package/dist/objects/containers/props.js +1 -0
- package/dist/objects/images/ImageInstance.d.ts +29 -0
- package/dist/objects/images/ImageInstance.js +83 -0
- package/dist/objects/images/Images.d.ts +37 -0
- package/dist/objects/images/Images.js +204 -0
- package/dist/objects/images/atlas-packer.d.ts +128 -0
- package/dist/objects/images/atlas-packer.js +315 -0
- package/dist/objects/images/image-container.d.ts +51 -0
- package/dist/objects/images/image-container.js +70 -0
- package/dist/objects/images/index.d.ts +2 -0
- package/dist/objects/images/index.js +1 -0
- package/dist/objects/images/texture-budget.d.ts +120 -0
- package/dist/objects/images/texture-budget.js +215 -0
- package/dist/objects/layer/Layer.d.ts +27 -0
- package/dist/objects/layer/Layer.js +114 -0
- package/dist/objects/layer/index.d.ts +2 -0
- package/dist/objects/layer/index.js +1 -0
- package/dist/objects/layer/mount-scheduler.d.ts +102 -0
- package/dist/objects/layer/mount-scheduler.js +145 -0
- package/dist/objects/layer/visibility-gui.d.ts +19 -0
- package/dist/objects/layer/visibility-gui.js +93 -0
- package/dist/objects/lines/LineInstance.d.ts +21 -0
- package/dist/objects/lines/LineInstance.js +73 -0
- package/dist/objects/lines/Lines.d.ts +17 -0
- package/dist/objects/lines/Lines.js +29 -0
- package/dist/objects/lines/index.d.ts +2 -0
- package/dist/objects/lines/index.js +1 -0
- package/dist/objects/lines/line-container.d.ts +35 -0
- package/dist/objects/lines/line-container.js +40 -0
- package/dist/objects/materials/ColorMaterial.d.ts +85 -0
- package/dist/objects/materials/ColorMaterial.js +89 -0
- package/dist/objects/materials/LineMaterial.d.ts +44 -0
- package/dist/objects/materials/LineMaterial.js +72 -0
- package/dist/objects/materials/ModelMaterial.d.ts +41 -0
- package/dist/objects/materials/ModelMaterial.js +84 -0
- package/dist/objects/materials/TextureMaterial.d.ts +30 -0
- package/dist/objects/materials/TextureMaterial.js +29 -0
- package/dist/objects/materials/dim.d.ts +123 -0
- package/dist/objects/materials/dim.js +213 -0
- package/dist/objects/materials/shared.d.ts +6 -0
- package/dist/objects/materials/shared.js +51 -0
- package/dist/objects/models/ModelInstance.d.ts +22 -0
- package/dist/objects/models/ModelInstance.js +33 -0
- package/dist/objects/models/Models.d.ts +22 -0
- package/dist/objects/models/Models.js +177 -0
- package/dist/objects/models/gltf-cache.d.ts +17 -0
- package/dist/objects/models/gltf-cache.js +50 -0
- package/dist/objects/models/index.d.ts +2 -0
- package/dist/objects/models/index.js +1 -0
- package/dist/objects/models/model-asset.d.ts +48 -0
- package/dist/objects/models/model-asset.js +77 -0
- package/dist/objects/models/model-container.d.ts +139 -0
- package/dist/objects/models/model-container.js +227 -0
- package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
- package/dist/objects/shapes/ShapeInstance.js +40 -0
- package/dist/objects/shapes/Shapes.d.ts +21 -0
- package/dist/objects/shapes/Shapes.js +120 -0
- package/dist/objects/shapes/index.d.ts +2 -0
- package/dist/objects/shapes/index.js +1 -0
- package/dist/objects/shapes/shape-container.d.ts +165 -0
- package/dist/objects/shapes/shape-container.js +272 -0
- package/dist/objects/shapes/shape-geometry.d.ts +27 -0
- package/dist/objects/shapes/shape-geometry.js +94 -0
- package/dist/objects/text/TextInstance.d.ts +22 -0
- package/dist/objects/text/TextInstance.js +95 -0
- package/dist/objects/text/Texts.d.ts +30 -0
- package/dist/objects/text/Texts.js +41 -0
- package/dist/objects/text/index.d.ts +2 -0
- package/dist/objects/text/index.js +1 -0
- package/dist/objects/text/text-container.d.ts +105 -0
- package/dist/objects/text/text-container.js +206 -0
- package/dist/objects/text/text-layout.d.ts +83 -0
- package/dist/objects/text/text-layout.js +154 -0
- package/dist/space/Camera.d.ts +103 -0
- package/dist/space/Camera.js +321 -0
- package/dist/space/Scene.d.ts +44 -0
- package/dist/space/Scene.js +112 -0
- package/dist/space/Space.d.ts +288 -0
- package/dist/space/Space.js +329 -0
- package/dist/space/VisibleRect.d.ts +72 -0
- package/dist/space/VisibleRect.js +129 -0
- package/dist/space/camera-handle.d.ts +67 -0
- package/dist/space/camera-handle.js +63 -0
- package/dist/space/camera-kind.d.ts +26 -0
- package/dist/space/camera-kind.js +30 -0
- package/dist/space/canvas-size.d.ts +24 -0
- package/dist/space/canvas-size.js +20 -0
- package/dist/space/clipping.d.ts +55 -0
- package/dist/space/clipping.js +108 -0
- package/dist/space/layout-measure.d.ts +65 -0
- package/dist/space/layout-measure.js +173 -0
- package/dist/space/viewport.d.ts +112 -0
- package/dist/space/viewport.js +67 -0
- package/dist/types/defs.d.ts +169 -0
- package/dist/types/defs.js +1 -0
- package/dist/types/events.d.ts +183 -0
- package/dist/types/events.js +1 -0
- package/dist/types/guards.d.ts +14 -0
- package/dist/types/guards.js +42 -0
- package/dist/update/index.d.ts +3 -0
- package/dist/update/index.js +2 -0
- package/dist/update/slot-writers.d.ts +30 -0
- package/dist/update/slot-writers.js +72 -0
- package/dist/update/store.d.ts +41 -0
- package/dist/update/store.js +98 -0
- package/dist/utils/arrays.d.ts +13 -0
- package/dist/utils/arrays.js +20 -0
- package/dist/utils/colors.d.ts +19 -0
- package/dist/utils/colors.js +27 -0
- package/dist/utils/logging.d.ts +26 -0
- package/dist/utils/logging.js +47 -0
- package/dist/utils/math.d.ts +24 -0
- package/dist/utils/math.js +27 -0
- package/package.json +67 -19
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Box } from '@expofp/geometry';
|
|
2
|
+
/** Props for {@link VisibleRect}. */
|
|
3
|
+
export interface VisibleRectProps {
|
|
4
|
+
/**
|
|
5
|
+
* The part of the canvas the plan should be framed inside, in css pixels from the canvas's
|
|
6
|
+
* top-left corner.
|
|
7
|
+
*
|
|
8
|
+
* Required, because there is no such thing as this component with nothing to do: a caller who
|
|
9
|
+
* named no rect does not render it at all, which is what keeps the default path free of a
|
|
10
|
+
* component whose every effect would return early.
|
|
11
|
+
*/
|
|
12
|
+
rect: Box;
|
|
13
|
+
/** The scene's viewbox, in plan units: what "the plan fits" is measured against. */
|
|
14
|
+
viewbox: Box;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Frames the plan inside a sub-region of the canvas, for a host whose own UI covers the rest.
|
|
18
|
+
*
|
|
19
|
+
* **The plan is still drawn across the whole canvas.** Nothing is clipped and nothing is cropped:
|
|
20
|
+
* the frustum is *expanded*, so the strips behind a header or a bottom sheet show more of the plan
|
|
21
|
+
* than they would otherwise, while the viewbox itself is guaranteed to land inside the clear
|
|
22
|
+
* region. That is the point of doing this in the projection rather than with `gl.viewport`.
|
|
23
|
+
*
|
|
24
|
+
* **Two calls, and nothing else in the renderer knows.** The transform is a uniform scale and a
|
|
25
|
+
* translate in clip space, which is exactly what {@link PerspectiveCamera.setViewOffset} expresses
|
|
26
|
+
* when its virtual screen is *smaller* than the rendered window — the reverse of the tiled-render
|
|
27
|
+
* case it is usually reached for. Because it lands in `projectionMatrix`, picking, R3F's pointer
|
|
28
|
+
* events, frustum culling, `Space`'s coordinate conversions and the pixel-size measurement all follow it with
|
|
29
|
+
* no seam: three re-derives `view` inside every `updateProjectionMatrix()`, so it also survives
|
|
30
|
+
* R3F's per-resize `aspect` write and `Camera`'s near and far updates.
|
|
31
|
+
*
|
|
32
|
+
* The second call is for `camera-controls`, which is the one thing that does *not* read the
|
|
33
|
+
* projection: it converts a pixel drag to a world offset through `fov` and the element's height,
|
|
34
|
+
* and finds the cursor's world position for `dollyToCursor` the same way. `setViewport` exists for
|
|
35
|
+
* exactly that — it tells the controls which part of the element the image occupies — and handed
|
|
36
|
+
* the landing box it makes every one of those conversions correct, with no fork patch and no
|
|
37
|
+
* speed fudge. The box keeps the canvas's aspect by construction, which is what leaves
|
|
38
|
+
* `camera.aspect` right inside the `dollyToCursor` arithmetic.
|
|
39
|
+
*
|
|
40
|
+
* **The scale is measured against the plan, not the canvas** — see {@link framingScale}.
|
|
41
|
+
* @param props - Where to frame the plan, and what to measure the framing against.
|
|
42
|
+
* @returns Nothing; this renders no scene node.
|
|
43
|
+
*/
|
|
44
|
+
export declare function VisibleRect(props: VisibleRectProps): null;
|
|
45
|
+
/**
|
|
46
|
+
* How far the plan has to shrink to fit inside the rect: `1` when it already does.
|
|
47
|
+
*
|
|
48
|
+
* **Measured against the plan's own footprint, not the canvas's.** Fitting the canvas into the rect
|
|
49
|
+
* would compose two contain-fits — plan into canvas, then canvas into rect — and a product of two
|
|
50
|
+
* independent minima is not the minimum the imperative renderer took over the same pair. The two
|
|
51
|
+
* agree only when the same axis binds both, and disagree in the commonest configuration there is: a
|
|
52
|
+
* wide hall on a portrait phone, where the plan already fits the clear band and fitting the canvas
|
|
53
|
+
* into it would shrink the plan by two thirds for nothing.
|
|
54
|
+
*
|
|
55
|
+
* Measured against the plan, the factorization telescopes back to what the imperative renderer
|
|
56
|
+
* computed in one step:
|
|
57
|
+
*
|
|
58
|
+
* ```
|
|
59
|
+
* fitScale · framingScale = min(W/a, H/b) · min(w/(a·fit), h/(b·fit)) = min(w/a, h/b)
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* The clamp only guards a rect larger than the canvas; for a rect inside it the result is already
|
|
63
|
+
* at most `1`, because the fit leaves the plan touching at least one edge.
|
|
64
|
+
* @param rect - The region to frame the plan inside, in css pixels.
|
|
65
|
+
* @param viewbox - The scene's viewbox, in plan units.
|
|
66
|
+
* @param width - The canvas width in css pixels.
|
|
67
|
+
* @param height - The canvas height in css pixels.
|
|
68
|
+
* @param dpr - Device pixels per css pixel.
|
|
69
|
+
* @returns The scale, in `(0, 1]`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function framingScale(rect: Box, viewbox: Box, width: number, height: number, dpr: number): number;
|
|
72
|
+
//# sourceMappingURL=VisibleRect.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useThree } from '@react-three/fiber';
|
|
2
|
+
import { useLayoutEffect } from 'react';
|
|
3
|
+
import { useCameraControls } from '../controls';
|
|
4
|
+
import { isPerspective } from './camera-kind';
|
|
5
|
+
import { useCanvasSize } from './canvas-size';
|
|
6
|
+
import { fitScale } from './Space';
|
|
7
|
+
/**
|
|
8
|
+
* Frames the plan inside a sub-region of the canvas, for a host whose own UI covers the rest.
|
|
9
|
+
*
|
|
10
|
+
* **The plan is still drawn across the whole canvas.** Nothing is clipped and nothing is cropped:
|
|
11
|
+
* the frustum is *expanded*, so the strips behind a header or a bottom sheet show more of the plan
|
|
12
|
+
* than they would otherwise, while the viewbox itself is guaranteed to land inside the clear
|
|
13
|
+
* region. That is the point of doing this in the projection rather than with `gl.viewport`.
|
|
14
|
+
*
|
|
15
|
+
* **Two calls, and nothing else in the renderer knows.** The transform is a uniform scale and a
|
|
16
|
+
* translate in clip space, which is exactly what {@link PerspectiveCamera.setViewOffset} expresses
|
|
17
|
+
* when its virtual screen is *smaller* than the rendered window — the reverse of the tiled-render
|
|
18
|
+
* case it is usually reached for. Because it lands in `projectionMatrix`, picking, R3F's pointer
|
|
19
|
+
* events, frustum culling, `Space`'s coordinate conversions and the pixel-size measurement all follow it with
|
|
20
|
+
* no seam: three re-derives `view` inside every `updateProjectionMatrix()`, so it also survives
|
|
21
|
+
* R3F's per-resize `aspect` write and `Camera`'s near and far updates.
|
|
22
|
+
*
|
|
23
|
+
* The second call is for `camera-controls`, which is the one thing that does *not* read the
|
|
24
|
+
* projection: it converts a pixel drag to a world offset through `fov` and the element's height,
|
|
25
|
+
* and finds the cursor's world position for `dollyToCursor` the same way. `setViewport` exists for
|
|
26
|
+
* exactly that — it tells the controls which part of the element the image occupies — and handed
|
|
27
|
+
* the landing box it makes every one of those conversions correct, with no fork patch and no
|
|
28
|
+
* speed fudge. The box keeps the canvas's aspect by construction, which is what leaves
|
|
29
|
+
* `camera.aspect` right inside the `dollyToCursor` arithmetic.
|
|
30
|
+
*
|
|
31
|
+
* **The scale is measured against the plan, not the canvas** — see {@link framingScale}.
|
|
32
|
+
* @param props - Where to frame the plan, and what to measure the framing against.
|
|
33
|
+
* @returns Nothing; this renders no scene node.
|
|
34
|
+
*/
|
|
35
|
+
export function VisibleRect(props) {
|
|
36
|
+
const { rect, viewbox } = props;
|
|
37
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
38
|
+
// Held rather than identified. `Camera` keeps only its camera's uuid because it *assigns* to
|
|
39
|
+
// fields of it and the React Compiler refuses to lower that; everything here is a method call,
|
|
40
|
+
// which the compiler is content with — so the dependency can be the camera itself and the effect
|
|
41
|
+
// needs no second read to find it.
|
|
42
|
+
const camera = useThree((state) => state.camera);
|
|
43
|
+
const controls = useCameraControls();
|
|
44
|
+
const { width, height, dpr } = useCanvasSize();
|
|
45
|
+
// Derived during render and depended on as three numbers, so a caller handing over a fresh `Box`
|
|
46
|
+
// each render — a MobX computed, say — does not re-place the framing for an unchanged rect.
|
|
47
|
+
const scale = framingScale(rect, viewbox, width, height, dpr);
|
|
48
|
+
const centerX = rect.center.x;
|
|
49
|
+
const centerY = rect.center.y;
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
if (!isPerspective(camera))
|
|
52
|
+
return undefined;
|
|
53
|
+
if (width <= 0 || height <= 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
// Nothing to place: a rect the plan already fills, sat in the middle of the canvas, works out to
|
|
56
|
+
// the identity, and leaving the camera untouched says so.
|
|
57
|
+
if (scale === 1 && centerX === width / 2 && centerY === height / 2)
|
|
58
|
+
return undefined;
|
|
59
|
+
placeFraming(camera, controls, scale, centerX, centerY, width, height);
|
|
60
|
+
invalidate();
|
|
61
|
+
return () => {
|
|
62
|
+
camera.clearViewOffset();
|
|
63
|
+
controls?.setViewport(null, 0, 0, 0);
|
|
64
|
+
invalidate();
|
|
65
|
+
};
|
|
66
|
+
}, [scale, centerX, centerY, width, height, camera, controls, invalidate]);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* How far the plan has to shrink to fit inside the rect: `1` when it already does.
|
|
71
|
+
*
|
|
72
|
+
* **Measured against the plan's own footprint, not the canvas's.** Fitting the canvas into the rect
|
|
73
|
+
* would compose two contain-fits — plan into canvas, then canvas into rect — and a product of two
|
|
74
|
+
* independent minima is not the minimum the imperative renderer took over the same pair. The two
|
|
75
|
+
* agree only when the same axis binds both, and disagree in the commonest configuration there is: a
|
|
76
|
+
* wide hall on a portrait phone, where the plan already fits the clear band and fitting the canvas
|
|
77
|
+
* into it would shrink the plan by two thirds for nothing.
|
|
78
|
+
*
|
|
79
|
+
* Measured against the plan, the factorization telescopes back to what the imperative renderer
|
|
80
|
+
* computed in one step:
|
|
81
|
+
*
|
|
82
|
+
* ```
|
|
83
|
+
* fitScale · framingScale = min(W/a, H/b) · min(w/(a·fit), h/(b·fit)) = min(w/a, h/b)
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* The clamp only guards a rect larger than the canvas; for a rect inside it the result is already
|
|
87
|
+
* at most `1`, because the fit leaves the plan touching at least one edge.
|
|
88
|
+
* @param rect - The region to frame the plan inside, in css pixels.
|
|
89
|
+
* @param viewbox - The scene's viewbox, in plan units.
|
|
90
|
+
* @param width - The canvas width in css pixels.
|
|
91
|
+
* @param height - The canvas height in css pixels.
|
|
92
|
+
* @param dpr - Device pixels per css pixel.
|
|
93
|
+
* @returns The scale, in `(0, 1]`.
|
|
94
|
+
*/
|
|
95
|
+
export function framingScale(rect, viewbox, width, height, dpr) {
|
|
96
|
+
const fit = fitScale(viewbox, width * dpr, height * dpr);
|
|
97
|
+
// The plan's footprint on the canvas at zoom 1, in device pixels — the viewbox fit sizes one
|
|
98
|
+
// world unit to one device pixel, so this is the viewbox scaled and nothing more.
|
|
99
|
+
const planWidth = viewbox.size.width * fit;
|
|
100
|
+
const planHeight = viewbox.size.height * fit;
|
|
101
|
+
if (!(planWidth > 0) || !(planHeight > 0))
|
|
102
|
+
return 1;
|
|
103
|
+
return Math.min((rect.size.width * dpr) / planWidth, (rect.size.height * dpr) / planHeight, 1);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Writes the framing onto the camera and the controls.
|
|
107
|
+
*
|
|
108
|
+
* A function taking both rather than two statements in the effect, which is what lets the caller
|
|
109
|
+
* hold the controls from a hook: the React Compiler rejects a write to a *field of* a hook's return
|
|
110
|
+
* value and is content once that value is an argument someone else was passed.
|
|
111
|
+
*
|
|
112
|
+
* The camera call reads: the natural frustum maps to a virtual screen of `scale · canvas` css
|
|
113
|
+
* pixels, positioned so its centre lands on the rect's centre, and the canvas is the window onto
|
|
114
|
+
* it. The controls call names the same box in the element's own coordinates, `y` measured from the
|
|
115
|
+
* bottom as `setViewport` takes it.
|
|
116
|
+
* @param camera - The camera to place the framing on.
|
|
117
|
+
* @param controls - The scene's controls, when they have mounted.
|
|
118
|
+
* @param scale - How far the plan shrinks; see {@link framingScale}.
|
|
119
|
+
* @param centerX - The rect's centre, in css pixels from the canvas's left edge.
|
|
120
|
+
* @param centerY - The rect's centre, in css pixels from the canvas's top edge.
|
|
121
|
+
* @param width - The canvas width in css pixels.
|
|
122
|
+
* @param height - The canvas height in css pixels.
|
|
123
|
+
*/
|
|
124
|
+
function placeFraming(camera, controls, scale, centerX, centerY, width, height) {
|
|
125
|
+
const boxWidth = scale * width;
|
|
126
|
+
const boxHeight = scale * height;
|
|
127
|
+
camera.setViewOffset(boxWidth, boxHeight, boxWidth / 2 - centerX, boxHeight / 2 - centerY, width, height);
|
|
128
|
+
controls?.setViewport(centerX - boxWidth / 2, height - boxHeight / 2 - centerY, boxWidth, boxHeight);
|
|
129
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The camera's zoom, as the rest of the renderer asks about it.
|
|
3
|
+
*
|
|
4
|
+
* Zoom is a factor, not a distance: `1` frames the scene's viewbox exactly, `2` shows half of it.
|
|
5
|
+
* The camera stores a distance, and the two convert through the buffer height — which is why this
|
|
6
|
+
* is a handle published by {@link Camera} rather than arithmetic a caller could do itself.
|
|
7
|
+
*/
|
|
8
|
+
export interface CameraHandle {
|
|
9
|
+
/** The zoom factor the camera is currently at. */
|
|
10
|
+
readonly zoomFactor: number;
|
|
11
|
+
/**
|
|
12
|
+
* The camera distance that shows the plan at a zoom factor.
|
|
13
|
+
* @param zoomFactor - The factor to convert.
|
|
14
|
+
* @returns The distance in world units.
|
|
15
|
+
*/
|
|
16
|
+
zoomFactorToDistance(zoomFactor: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* The zoom factor a camera distance shows the plan at.
|
|
19
|
+
* @param distance - The distance in world units.
|
|
20
|
+
* @returns The factor.
|
|
21
|
+
*/
|
|
22
|
+
distanceToZoomFactor(distance: number): number;
|
|
23
|
+
}
|
|
24
|
+
/** The scene's camera handle, published by {@link Camera}. */
|
|
25
|
+
export declare const CameraContext: import("react").Context<CameraHandle | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* The scene's camera handle, once {@link Camera} has published it.
|
|
28
|
+
* @returns The handle, or `undefined` when rendered outside a scene.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useCamera(): CameraHandle | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The zoom handle {@link Camera} publishes.
|
|
33
|
+
*
|
|
34
|
+
* A plain object rather than anything reactive: every member is derived from the buffer height it
|
|
35
|
+
* was built against and whatever the camera holds when it is asked, so there is no state here to go
|
|
36
|
+
* stale — only a factor and the two conversions that turn it into a distance and back.
|
|
37
|
+
*
|
|
38
|
+
* Takes a reading rather than the canvas state: the one live number it needs is how far the camera
|
|
39
|
+
* is from the plan, and asking for that instead of for the store keeps this file pure arithmetic —
|
|
40
|
+
* with no view on where a distance comes from, and nothing to import from `controls`.
|
|
41
|
+
* @param bufferHeight - The drawing buffer's height in device pixels.
|
|
42
|
+
* @param getDistance - How far the camera currently is from the plan, in world units, read at the
|
|
43
|
+
* moment of asking.
|
|
44
|
+
* @returns The handle.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createCameraHandle(bufferHeight: number, getDistance: () => number): CameraHandle;
|
|
47
|
+
/**
|
|
48
|
+
* The camera distance at which the drawing buffer's height covers exactly that many world units —
|
|
49
|
+
* zoom factor `1`.
|
|
50
|
+
*
|
|
51
|
+
* Paired with the viewbox fit, which sizes a world unit to a device pixel, this is what "the plan
|
|
52
|
+
* fills the canvas" means as a number.
|
|
53
|
+
* @param bufferHeight - The drawing buffer's height in device pixels.
|
|
54
|
+
* @param fov - The camera's vertical field of view in degrees.
|
|
55
|
+
* @returns The distance in world units, or `0` while the buffer has no height.
|
|
56
|
+
*/
|
|
57
|
+
export declare function zoomIdentityDistance(bufferHeight: number, fov: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* The camera's vertical field of view, in degrees — `2·atan(1/3)`, the value the imperative renderer
|
|
60
|
+
* took from Mapbox.
|
|
61
|
+
*
|
|
62
|
+
* Narrow on purpose. The plan is laid out against a single pixel-to-plan factor per frame (see
|
|
63
|
+
* `readPixelSize`), which under perspective is only ever an approximation across a tilted view; the
|
|
64
|
+
* narrower the field, the closer that approximation holds.
|
|
65
|
+
*/
|
|
66
|
+
export declare const CAMERA_FOV = 36.87;
|
|
67
|
+
//# sourceMappingURL=camera-handle.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { degToRad } from '@expofp/geometry';
|
|
2
|
+
import { createContext, use } from 'react';
|
|
3
|
+
/** The scene's camera handle, published by {@link Camera}. */
|
|
4
|
+
export const CameraContext = createContext(undefined);
|
|
5
|
+
/**
|
|
6
|
+
* The scene's camera handle, once {@link Camera} has published it.
|
|
7
|
+
* @returns The handle, or `undefined` when rendered outside a scene.
|
|
8
|
+
*/
|
|
9
|
+
export function useCamera() {
|
|
10
|
+
return use(CameraContext);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The zoom handle {@link Camera} publishes.
|
|
14
|
+
*
|
|
15
|
+
* A plain object rather than anything reactive: every member is derived from the buffer height it
|
|
16
|
+
* was built against and whatever the camera holds when it is asked, so there is no state here to go
|
|
17
|
+
* stale — only a factor and the two conversions that turn it into a distance and back.
|
|
18
|
+
*
|
|
19
|
+
* Takes a reading rather than the canvas state: the one live number it needs is how far the camera
|
|
20
|
+
* is from the plan, and asking for that instead of for the store keeps this file pure arithmetic —
|
|
21
|
+
* with no view on where a distance comes from, and nothing to import from `controls`.
|
|
22
|
+
* @param bufferHeight - The drawing buffer's height in device pixels.
|
|
23
|
+
* @param getDistance - How far the camera currently is from the plan, in world units, read at the
|
|
24
|
+
* moment of asking.
|
|
25
|
+
* @returns The handle.
|
|
26
|
+
*/
|
|
27
|
+
export function createCameraHandle(bufferHeight, getDistance) {
|
|
28
|
+
const identity = zoomIdentityDistance(bufferHeight, CAMERA_FOV);
|
|
29
|
+
const toZoomFactor = (distance) => identity > 0 && distance > 0 ? identity / distance : 1;
|
|
30
|
+
return {
|
|
31
|
+
get zoomFactor() {
|
|
32
|
+
return toZoomFactor(getDistance());
|
|
33
|
+
},
|
|
34
|
+
zoomFactorToDistance(zoomFactor) {
|
|
35
|
+
return zoomFactor > 0 ? identity / zoomFactor : identity;
|
|
36
|
+
},
|
|
37
|
+
distanceToZoomFactor: toZoomFactor,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The camera distance at which the drawing buffer's height covers exactly that many world units —
|
|
42
|
+
* zoom factor `1`.
|
|
43
|
+
*
|
|
44
|
+
* Paired with the viewbox fit, which sizes a world unit to a device pixel, this is what "the plan
|
|
45
|
+
* fills the canvas" means as a number.
|
|
46
|
+
* @param bufferHeight - The drawing buffer's height in device pixels.
|
|
47
|
+
* @param fov - The camera's vertical field of view in degrees.
|
|
48
|
+
* @returns The distance in world units, or `0` while the buffer has no height.
|
|
49
|
+
*/
|
|
50
|
+
export function zoomIdentityDistance(bufferHeight, fov) {
|
|
51
|
+
if (bufferHeight <= 0)
|
|
52
|
+
return 0;
|
|
53
|
+
return bufferHeight / 2 / Math.tan(degToRad(fov) / 2);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The camera's vertical field of view, in degrees — `2·atan(1/3)`, the value the imperative renderer
|
|
57
|
+
* took from Mapbox.
|
|
58
|
+
*
|
|
59
|
+
* Narrow on purpose. The plan is laid out against a single pixel-to-plan factor per frame (see
|
|
60
|
+
* `readPixelSize`), which under perspective is only ever an approximation across a tilted view; the
|
|
61
|
+
* narrower the field, the closer that approximation holds.
|
|
62
|
+
*/
|
|
63
|
+
export const CAMERA_FOV = 36.87;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Camera, type OrthographicCamera, type PerspectiveCamera } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a camera projects perspectively.
|
|
4
|
+
*
|
|
5
|
+
* Three's own `isPerspectiveCamera` brand rather than `instanceof`, so a camera from a second copy
|
|
6
|
+
* of three — a host's, under external mode — answers the same as one of ours.
|
|
7
|
+
* @param camera - The camera to test.
|
|
8
|
+
* @returns Whether it is a perspective camera.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isPerspective(camera: Camera): camera is PerspectiveCamera;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a camera projects orthographically.
|
|
13
|
+
* @param camera - The camera to test.
|
|
14
|
+
* @returns Whether it is an orthographic camera.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isOrthographic(camera: Camera): camera is OrthographicCamera;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a camera derives its own projection from a frustum, rather than carrying one handed to it.
|
|
19
|
+
*
|
|
20
|
+
* The question three's `Raycaster.setFromCamera` asks before it refuses a camera, and the one
|
|
21
|
+
* `Camera.tsx` and `VisibleRect.tsx` ask before writing frustum fields nothing else has.
|
|
22
|
+
* @param camera - The camera to test.
|
|
23
|
+
* @returns Whether it projects either standard way.
|
|
24
|
+
*/
|
|
25
|
+
export declare function projectsFromFrustum(camera: Camera): boolean;
|
|
26
|
+
//# sourceMappingURL=camera-kind.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a camera projects perspectively.
|
|
3
|
+
*
|
|
4
|
+
* Three's own `isPerspectiveCamera` brand rather than `instanceof`, so a camera from a second copy
|
|
5
|
+
* of three — a host's, under external mode — answers the same as one of ours.
|
|
6
|
+
* @param camera - The camera to test.
|
|
7
|
+
* @returns Whether it is a perspective camera.
|
|
8
|
+
*/
|
|
9
|
+
export function isPerspective(camera) {
|
|
10
|
+
return camera.isPerspectiveCamera === true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether a camera projects orthographically.
|
|
14
|
+
* @param camera - The camera to test.
|
|
15
|
+
* @returns Whether it is an orthographic camera.
|
|
16
|
+
*/
|
|
17
|
+
export function isOrthographic(camera) {
|
|
18
|
+
return camera.isOrthographicCamera === true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a camera derives its own projection from a frustum, rather than carrying one handed to it.
|
|
22
|
+
*
|
|
23
|
+
* The question three's `Raycaster.setFromCamera` asks before it refuses a camera, and the one
|
|
24
|
+
* `Camera.tsx` and `VisibleRect.tsx` ask before writing frustum fields nothing else has.
|
|
25
|
+
* @param camera - The camera to test.
|
|
26
|
+
* @returns Whether it projects either standard way.
|
|
27
|
+
*/
|
|
28
|
+
export function projectsFromFrustum(camera) {
|
|
29
|
+
return isPerspective(camera) || isOrthographic(camera);
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** How large the canvas is, and how densely it draws. */
|
|
2
|
+
export interface CanvasSize {
|
|
3
|
+
/** Width in css pixels. */
|
|
4
|
+
readonly width: number;
|
|
5
|
+
/** Height in css pixels. */
|
|
6
|
+
readonly height: number;
|
|
7
|
+
/** Device pixels per css pixel. Multiply by it for drawing-buffer sizes. */
|
|
8
|
+
readonly dpr: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The canvas's size, subscribed to one number at a time.
|
|
12
|
+
*
|
|
13
|
+
* Never read `state.size` as an object. R3F re-runs its canvas configuration on every render of the
|
|
14
|
+
* `<Canvas>` element and replaces that object as it goes, dimensions unchanged or not — so a
|
|
15
|
+
* component selecting it re-renders on renders that changed nothing, and anything memoized against
|
|
16
|
+
* it is rebuilt. Selecting the numbers is immune, because the numbers only move when the canvas
|
|
17
|
+
* really does.
|
|
18
|
+
*
|
|
19
|
+
* The returned object is a fresh literal, which costs nothing: it is not what the subscription
|
|
20
|
+
* compares, and the React Compiler keeps its identity stable across renders where the numbers hold.
|
|
21
|
+
* @returns The canvas's css size and device pixel ratio.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useCanvasSize(): CanvasSize;
|
|
24
|
+
//# sourceMappingURL=canvas-size.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useThree } from '@react-three/fiber';
|
|
2
|
+
/**
|
|
3
|
+
* The canvas's size, subscribed to one number at a time.
|
|
4
|
+
*
|
|
5
|
+
* Never read `state.size` as an object. R3F re-runs its canvas configuration on every render of the
|
|
6
|
+
* `<Canvas>` element and replaces that object as it goes, dimensions unchanged or not — so a
|
|
7
|
+
* component selecting it re-renders on renders that changed nothing, and anything memoized against
|
|
8
|
+
* it is rebuilt. Selecting the numbers is immune, because the numbers only move when the canvas
|
|
9
|
+
* really does.
|
|
10
|
+
*
|
|
11
|
+
* The returned object is a fresh literal, which costs nothing: it is not what the subscription
|
|
12
|
+
* compares, and the React Compiler keeps its identity stable across renders where the numbers hold.
|
|
13
|
+
* @returns The canvas's css size and device pixel ratio.
|
|
14
|
+
*/
|
|
15
|
+
export function useCanvasSize() {
|
|
16
|
+
const width = useThree((state) => state.size.width);
|
|
17
|
+
const height = useThree((state) => state.size.height);
|
|
18
|
+
const dpr = useThree((state) => state.viewport.dpr);
|
|
19
|
+
return { width, height, dpr };
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steepest the plan may **ever** be pitched away from face-on, in degrees.
|
|
3
|
+
*
|
|
4
|
+
* A hard cap, not a setting. The near plane below is sized against this angle: pitching the camera
|
|
5
|
+
* foreshortens an elevation along the view axis, and this is the steepest view the depth guarantee
|
|
6
|
+
* must survive, so a steeper view is one it never allowed for. `Camera` clamps `maxPitch` to it,
|
|
7
|
+
* and defaults to it.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately **not** what the polygon offsets in `objects/materials` are sized against, though the
|
|
10
|
+
* imperative renderer's were (`⌈tan(maxPolarAngle)⌉`): a slope-scaled `polygonOffsetFactor` reads
|
|
11
|
+
* the actual slope per draw, so no worst-case angle enters that calculation — see `sharedParams`.
|
|
12
|
+
*
|
|
13
|
+
* In degrees, like every pitch the renderer's API talks about; the cosine the arithmetic actually
|
|
14
|
+
* wants is {@link MIN_PITCH_COSINE}, so the conversion happens once, here, and nowhere else.
|
|
15
|
+
*
|
|
16
|
+
* The imperative renderer's own limit, and the angle its depth guarantee was stated against.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_PITCH = 85;
|
|
19
|
+
/**
|
|
20
|
+
* The near clipping plane that keeps depth precision at the plan constant as the camera dollies.
|
|
21
|
+
*
|
|
22
|
+
* Two constraints set it, and the larger wins so precision is never traded away for headroom:
|
|
23
|
+
*
|
|
24
|
+
* - **Headroom.** `distance × (1 − HEADROOM_RATIO)`. Dominates at close and default zoom, where it
|
|
25
|
+
* keeps the near plane a fixed fraction of the way to the plan and precision is already better
|
|
26
|
+
* than asked for.
|
|
27
|
+
* - **Precision.** `distance² / (levels × precision)`, the distance a depth buffer of that many
|
|
28
|
+
* levels can still resolve at the plan. Dominates far out, where it buys the guarantee back at
|
|
29
|
+
* the cost of some headroom.
|
|
30
|
+
*
|
|
31
|
+
* The guarantee is stated in **plan** units — see {@link ELEVATION_PRECISION} — so it is scaled
|
|
32
|
+
* into world units before the depth buffer is asked about it. The imperative renderer did not do
|
|
33
|
+
* that: it applied the plan-unit constant to world distances, which quietly loosened the guarantee
|
|
34
|
+
* on any plan drawn smaller than world scale. On a 60 000-unit plan in a 1600-pixel buffer that is a
|
|
35
|
+
* factor of 2.7 at default zoom, enough for two surfaces separated by exactly the documented minimum
|
|
36
|
+
* to z-fight.
|
|
37
|
+
*
|
|
38
|
+
* Sized against {@link MAX_PITCH} rather than against whatever pitch a scene currently allows, which
|
|
39
|
+
* is the conservative choice on purpose: the worst case is the only one that is always true, and a
|
|
40
|
+
* near plane computed from the pitch *in force* would have to be recomputed whenever that moved and
|
|
41
|
+
* would silently weaken the guarantee wherever nothing was enforcing a limit at all. What it costs
|
|
42
|
+
* is headroom, and only in the regime where {@link MIN_HEADROOM_RATIO} has already capped it.
|
|
43
|
+
* @param distance - Camera distance to the plan, in world units.
|
|
44
|
+
* @param scale - World units per plan unit, from the scene's {@link Space}.
|
|
45
|
+
* @returns The near plane in world units, or `undefined` while the camera sits on the plan.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nearPlane(distance: number, scale: number): number | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* The far plane for a camera that may pull back that far, with a factor of two spare so content
|
|
50
|
+
* behind the plan — anything at a negative elevation — is not clipped at full zoom-out.
|
|
51
|
+
* @param maxDistance - The furthest the camera may get from the plan, in world units.
|
|
52
|
+
* @returns The far plane in world units.
|
|
53
|
+
*/
|
|
54
|
+
export declare function farPlane(maxDistance: number): number;
|
|
55
|
+
//# sourceMappingURL=clipping.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the camera's frustum begins and ends.
|
|
3
|
+
*
|
|
4
|
+
* Both planes answer the same question from opposite sides — what the camera can still draw — and
|
|
5
|
+
* they are set on different clocks: the near plane follows the camera's distance, every frame it
|
|
6
|
+
* moves, because that is what depth precision at the plan depends on; the far plane is fixed by how
|
|
7
|
+
* far the zoom bounds let the camera retreat, so it changes only when those do.
|
|
8
|
+
*/
|
|
9
|
+
import { degToRad } from '@expofp/geometry';
|
|
10
|
+
/**
|
|
11
|
+
* The steepest the plan may **ever** be pitched away from face-on, in degrees.
|
|
12
|
+
*
|
|
13
|
+
* A hard cap, not a setting. The near plane below is sized against this angle: pitching the camera
|
|
14
|
+
* foreshortens an elevation along the view axis, and this is the steepest view the depth guarantee
|
|
15
|
+
* must survive, so a steeper view is one it never allowed for. `Camera` clamps `maxPitch` to it,
|
|
16
|
+
* and defaults to it.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately **not** what the polygon offsets in `objects/materials` are sized against, though the
|
|
19
|
+
* imperative renderer's were (`⌈tan(maxPolarAngle)⌉`): a slope-scaled `polygonOffsetFactor` reads
|
|
20
|
+
* the actual slope per draw, so no worst-case angle enters that calculation — see `sharedParams`.
|
|
21
|
+
*
|
|
22
|
+
* In degrees, like every pitch the renderer's API talks about; the cosine the arithmetic actually
|
|
23
|
+
* wants is {@link MIN_PITCH_COSINE}, so the conversion happens once, here, and nowhere else.
|
|
24
|
+
*
|
|
25
|
+
* The imperative renderer's own limit, and the angle its depth guarantee was stated against.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_PITCH = 85;
|
|
28
|
+
/**
|
|
29
|
+
* The near clipping plane that keeps depth precision at the plan constant as the camera dollies.
|
|
30
|
+
*
|
|
31
|
+
* Two constraints set it, and the larger wins so precision is never traded away for headroom:
|
|
32
|
+
*
|
|
33
|
+
* - **Headroom.** `distance × (1 − HEADROOM_RATIO)`. Dominates at close and default zoom, where it
|
|
34
|
+
* keeps the near plane a fixed fraction of the way to the plan and precision is already better
|
|
35
|
+
* than asked for.
|
|
36
|
+
* - **Precision.** `distance² / (levels × precision)`, the distance a depth buffer of that many
|
|
37
|
+
* levels can still resolve at the plan. Dominates far out, where it buys the guarantee back at
|
|
38
|
+
* the cost of some headroom.
|
|
39
|
+
*
|
|
40
|
+
* The guarantee is stated in **plan** units — see {@link ELEVATION_PRECISION} — so it is scaled
|
|
41
|
+
* into world units before the depth buffer is asked about it. The imperative renderer did not do
|
|
42
|
+
* that: it applied the plan-unit constant to world distances, which quietly loosened the guarantee
|
|
43
|
+
* on any plan drawn smaller than world scale. On a 60 000-unit plan in a 1600-pixel buffer that is a
|
|
44
|
+
* factor of 2.7 at default zoom, enough for two surfaces separated by exactly the documented minimum
|
|
45
|
+
* to z-fight.
|
|
46
|
+
*
|
|
47
|
+
* Sized against {@link MAX_PITCH} rather than against whatever pitch a scene currently allows, which
|
|
48
|
+
* is the conservative choice on purpose: the worst case is the only one that is always true, and a
|
|
49
|
+
* near plane computed from the pitch *in force* would have to be recomputed whenever that moved and
|
|
50
|
+
* would silently weaken the guarantee wherever nothing was enforcing a limit at all. What it costs
|
|
51
|
+
* is headroom, and only in the regime where {@link MIN_HEADROOM_RATIO} has already capped it.
|
|
52
|
+
* @param distance - Camera distance to the plan, in world units.
|
|
53
|
+
* @param scale - World units per plan unit, from the scene's {@link Space}.
|
|
54
|
+
* @returns The near plane in world units, or `undefined` while the camera sits on the plan.
|
|
55
|
+
*/
|
|
56
|
+
export function nearPlane(distance, scale) {
|
|
57
|
+
if (distance <= 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
// Pitching the camera foreshortens an elevation along the view axis, so the separation the depth
|
|
60
|
+
// buffer actually sees is the plan one times the cosine.
|
|
61
|
+
const precision = ELEVATION_PRECISION * scale * MIN_PITCH_COSINE;
|
|
62
|
+
const near = (distance * distance) / (DEPTH_BUFFER_LEVELS * precision);
|
|
63
|
+
return Math.min(Math.max(near, distance * (1 - HEADROOM_RATIO)), distance * (1 - MIN_HEADROOM_RATIO));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The far plane for a camera that may pull back that far, with a factor of two spare so content
|
|
67
|
+
* behind the plan — anything at a negative elevation — is not clipped at full zoom-out.
|
|
68
|
+
* @param maxDistance - The furthest the camera may get from the plan, in world units.
|
|
69
|
+
* @returns The far plane in world units.
|
|
70
|
+
*/
|
|
71
|
+
export function farPlane(maxDistance) {
|
|
72
|
+
return Math.max(maxDistance, 0.1) * 2;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Minimum z-axis separation between overlapping surfaces, in **plan** units.
|
|
76
|
+
*
|
|
77
|
+
* **For plan data providers:** shapes sharing an x/y footprint — an elevated platform over a floor
|
|
78
|
+
* area, layered decorations at one position — must be separated by at least this much on z to be
|
|
79
|
+
* ordered correctly. No minimum horizontal separation is required: screen-pixel density and depth
|
|
80
|
+
* precision both scale with camera distance and with the scene's scale, so any separation visible on
|
|
81
|
+
* screen already has depth margin, at any viewport size, zoom, scale or camera rotation.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately the renderer's own constant rather than `@expofp/geometry`'s `ELEVATION_TOLERANCE`,
|
|
84
|
+
* which happens to hold the same number. They answer different questions — geometry's is "are these
|
|
85
|
+
* two elevations the same point?", this one is "must these two elevations still draw in the right
|
|
86
|
+
* order?" — and importing it would let a tuning of the geometry predicate silently re-tune the depth
|
|
87
|
+
* buffer.
|
|
88
|
+
*/
|
|
89
|
+
const ELEVATION_PRECISION = 0.1;
|
|
90
|
+
/**
|
|
91
|
+
* Fraction of the camera's distance kept clear of the near plane at close and default zoom, so
|
|
92
|
+
* depth-buffer levels are not spent on the empty space between the camera and the plan.
|
|
93
|
+
*/
|
|
94
|
+
const HEADROOM_RATIO = 0.98;
|
|
95
|
+
/**
|
|
96
|
+
* Least headroom the precision constraint may leave. Without it a plan drawn very small — a viewbox
|
|
97
|
+
* hundreds of times the buffer — asks for a near plane beyond the camera's own target and blanks the
|
|
98
|
+
* canvas. Past this point precision degrades instead, which is visible only where two surfaces are
|
|
99
|
+
* within the documented minimum of each other.
|
|
100
|
+
*/
|
|
101
|
+
const MIN_HEADROOM_RATIO = 0.5;
|
|
102
|
+
/** 24-bit depth buffer: 2²⁴ discrete levels, which covers 99%+ of devices. */
|
|
103
|
+
const DEPTH_BUFFER_LEVELS = 2 ** 24;
|
|
104
|
+
/**
|
|
105
|
+
* How much of an elevation survives being viewed at {@link MAX_PITCH} — the least of it the depth
|
|
106
|
+
* buffer ever has to resolve, and so the figure the near plane is sized against.
|
|
107
|
+
*/
|
|
108
|
+
const MIN_PITCH_COSINE = Math.cos(degToRad(MAX_PITCH));
|