@expofp/renderer 0.0.0-experimental.2d6c681
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 +145 -0
- package/dist/Renderer.d.ts +102 -0
- package/dist/Renderer.js +96 -0
- package/dist/controls/CameraControls.d.ts +108 -0
- package/dist/controls/CameraControls.js +203 -0
- package/dist/controls/Controls.d.ts +53 -0
- package/dist/controls/Controls.js +54 -0
- package/dist/controls/camera-api.d.ts +226 -0
- package/dist/controls/camera-api.js +179 -0
- package/dist/controls/gestures.d.ts +71 -0
- package/dist/controls/gestures.js +98 -0
- package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
- package/dist/controls/handlers/DoubleTapZoom.js +82 -0
- package/dist/controls/handlers/Inertia.d.ts +60 -0
- package/dist/controls/handlers/Inertia.js +222 -0
- package/dist/controls/handlers/Pan.d.ts +23 -0
- package/dist/controls/handlers/Pan.js +212 -0
- package/dist/controls/handlers/Pitch.d.ts +19 -0
- package/dist/controls/handlers/Pitch.js +200 -0
- package/dist/controls/handlers/Roll.d.ts +30 -0
- package/dist/controls/handlers/Roll.js +196 -0
- package/dist/controls/handlers/Zoom.d.ts +28 -0
- package/dist/controls/handlers/Zoom.js +34 -0
- package/dist/controls/hooks.d.ts +136 -0
- package/dist/controls/hooks.js +200 -0
- package/dist/controls/index.d.ts +9 -0
- package/dist/controls/index.js +7 -0
- package/dist/controls/zoom-to.d.ts +89 -0
- package/dist/controls/zoom-to.js +91 -0
- package/dist/core/InstanceUniforms.d.ts +108 -0
- package/dist/core/InstanceUniforms.js +298 -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 +155 -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 +228 -0
- package/dist/core/lines/BatchedLines.d.ts +137 -0
- package/dist/core/lines/BatchedLines.js +215 -0
- package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
- package/dist/core/lines/FrustumCullingLines.js +85 -0
- package/dist/core/lines/RaycastingLines.d.ts +45 -0
- package/dist/core/lines/RaycastingLines.js +91 -0
- package/dist/core/lines/lines-material.d.ts +47 -0
- package/dist/core/lines/lines-material.js +124 -0
- package/dist/core/shader-source.d.ts +70 -0
- package/dist/core/shader-source.js +110 -0
- package/dist/core/sprites/BatchedSprites.d.ts +115 -0
- package/dist/core/sprites/BatchedSprites.js +173 -0
- package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
- package/dist/core/sprites/RaycastingSprites.js +71 -0
- package/dist/core/sprites/sprite-material.d.ts +59 -0
- package/dist/core/sprites/sprite-material.js +192 -0
- package/dist/core/text/BatchedText.d.ts +233 -0
- package/dist/core/text/BatchedText.js +473 -0
- package/dist/core/text/MemberTexture.d.ts +90 -0
- package/dist/core/text/MemberTexture.js +289 -0
- package/dist/core/text/Text.d.ts +88 -0
- package/dist/core/text/Text.js +230 -0
- package/dist/core/text/batched-text-material.d.ts +56 -0
- package/dist/core/text/batched-text-material.js +123 -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 +50 -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 +22 -0
- package/dist/debug/components/Gui.js +42 -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 +104 -0
- package/dist/debug/components/Stats.d.ts +9 -0
- package/dist/debug/components/Stats.js +24 -0
- package/dist/debug/debug-chunk.d.ts +26 -0
- package/dist/debug/debug-chunk.js +32 -0
- package/dist/debug/debug-context.d.ts +41 -0
- package/dist/debug/debug-context.js +59 -0
- package/dist/debug/index.d.ts +41 -0
- package/dist/debug/index.js +43 -0
- package/dist/events/LayoutDriver.d.ts +36 -0
- package/dist/events/LayoutDriver.js +192 -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 +151 -0
- package/dist/events/layout-targets.js +397 -0
- package/dist/events/placement/box.d.ts +59 -0
- package/dist/events/placement/box.js +96 -0
- package/dist/events/placement/grid.d.ts +161 -0
- package/dist/events/placement/grid.js +235 -0
- package/dist/events/placement/index.d.ts +2 -0
- package/dist/events/placement/index.js +1 -0
- package/dist/events/placement/pass.d.ts +70 -0
- package/dist/events/placement/pass.js +617 -0
- package/dist/events/pointer.d.ts +194 -0
- package/dist/events/pointer.js +356 -0
- package/dist/external/ExternalCanvas.d.ts +56 -0
- package/dist/external/ExternalCanvas.js +177 -0
- package/dist/external/external-camera.d.ts +18 -0
- package/dist/external/external-camera.js +103 -0
- package/dist/external/external-events.d.ts +14 -0
- package/dist/external/external-events.js +33 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/objects/Background.d.ts +31 -0
- package/dist/objects/Background.js +37 -0
- package/dist/objects/containers/SlotContainer.d.ts +312 -0
- package/dist/objects/containers/SlotContainer.js +311 -0
- package/dist/objects/containers/hooks.d.ts +61 -0
- package/dist/objects/containers/hooks.js +196 -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 +33 -0
- package/dist/objects/containers/props.js +1 -0
- package/dist/objects/images/ImageInstance.d.ts +29 -0
- package/dist/objects/images/ImageInstance.js +152 -0
- package/dist/objects/images/Images.d.ts +49 -0
- package/dist/objects/images/Images.js +394 -0
- package/dist/objects/images/atlas-packer.d.ts +129 -0
- package/dist/objects/images/atlas-packer.js +287 -0
- package/dist/objects/images/image-container.d.ts +74 -0
- package/dist/objects/images/image-container.js +100 -0
- package/dist/objects/images/index.d.ts +2 -0
- package/dist/objects/images/index.js +1 -0
- package/dist/objects/images/pack-sources.d.ts +190 -0
- package/dist/objects/images/pack-sources.js +272 -0
- package/dist/objects/images/texture-budget.d.ts +120 -0
- package/dist/objects/images/texture-budget.js +215 -0
- package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
- package/dist/objects/images/worker/atlas-worker.js +123 -0
- package/dist/objects/images/worker/client.d.ts +62 -0
- package/dist/objects/images/worker/client.js +249 -0
- package/dist/objects/images/worker/index.d.ts +10 -0
- package/dist/objects/images/worker/index.js +1 -0
- package/dist/objects/layer/Layer.d.ts +28 -0
- package/dist/objects/layer/Layer.js +134 -0
- package/dist/objects/layer/index.d.ts +2 -0
- package/dist/objects/layer/index.js +1 -0
- package/dist/objects/layer/layer-context.d.ts +74 -0
- package/dist/objects/layer/layer-context.js +82 -0
- package/dist/objects/layer/mount-scheduler.d.ts +131 -0
- package/dist/objects/layer/mount-scheduler.js +289 -0
- package/dist/objects/layer/visibility-gui.d.ts +19 -0
- package/dist/objects/layer/visibility-gui.js +93 -0
- package/dist/objects/lighting/Lighting.d.ts +92 -0
- package/dist/objects/lighting/Lighting.js +384 -0
- package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
- package/dist/objects/lighting/OcclusionPasses.js +271 -0
- package/dist/objects/lighting/index.d.ts +11 -0
- package/dist/objects/lighting/index.js +10 -0
- package/dist/objects/lighting/lighting-gui.d.ts +24 -0
- package/dist/objects/lighting/lighting-gui.js +114 -0
- package/dist/objects/lighting/occluder.d.ts +34 -0
- package/dist/objects/lighting/occluder.js +62 -0
- package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
- package/dist/objects/lighting/occlusion-targets.js +276 -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 +31 -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 +92 -0
- package/dist/objects/materials/ColorMaterial.js +124 -0
- package/dist/objects/materials/LineMaterial.d.ts +44 -0
- package/dist/objects/materials/LineMaterial.js +74 -0
- package/dist/objects/materials/ModelMaterial.d.ts +53 -0
- package/dist/objects/materials/ModelMaterial.js +111 -0
- package/dist/objects/materials/TextureMaterial.d.ts +33 -0
- package/dist/objects/materials/TextureMaterial.js +48 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
- package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
- package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
- package/dist/objects/materials/effects/ao/index.d.ts +129 -0
- package/dist/objects/materials/effects/ao/index.js +72 -0
- package/dist/objects/materials/effects/color-dim.d.ts +115 -0
- package/dist/objects/materials/effects/color-dim.js +206 -0
- package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
- package/dist/objects/materials/effects/contact-shadow.js +167 -0
- package/dist/objects/materials/effects/effects-context.d.ts +53 -0
- package/dist/objects/materials/effects/effects-context.js +24 -0
- package/dist/objects/materials/shared.d.ts +24 -0
- package/dist/objects/materials/shared.js +72 -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 +186 -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 +123 -0
- package/dist/objects/models/model-container.js +207 -0
- package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
- package/dist/objects/shapes/ShapeInstance.js +39 -0
- package/dist/objects/shapes/Shapes.d.ts +21 -0
- package/dist/objects/shapes/Shapes.js +131 -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 +141 -0
- package/dist/objects/shapes/shape-container.js +242 -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 +43 -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 +121 -0
- package/dist/objects/text/text-container.js +229 -0
- package/dist/objects/text/text-layout.d.ts +92 -0
- package/dist/objects/text/text-layout.js +180 -0
- package/dist/space/Camera.d.ts +103 -0
- package/dist/space/Camera.js +321 -0
- package/dist/space/Scene.d.ts +47 -0
- package/dist/space/Scene.js +127 -0
- package/dist/space/Space.d.ts +325 -0
- package/dist/space/Space.js +359 -0
- package/dist/space/VisibleRect.d.ts +45 -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 +69 -0
- package/dist/space/clipping.js +130 -0
- package/dist/space/layout-measure.d.ts +67 -0
- package/dist/space/layout-measure.js +175 -0
- package/dist/space/viewport.d.ts +112 -0
- package/dist/space/viewport.js +67 -0
- package/dist/types/defs.d.ts +234 -0
- package/dist/types/defs.js +1 -0
- package/dist/types/events.d.ts +198 -0
- package/dist/types/events.js +1 -0
- package/dist/types/guards.d.ts +21 -0
- package/dist/types/guards.js +52 -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 +76 -0
- package/dist/update/store.d.ts +41 -0
- package/dist/update/store.js +104 -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/hot-state.d.ts +41 -0
- package/dist/utils/hot-state.js +37 -0
- package/dist/utils/logging.d.ts +29 -0
- package/dist/utils/logging.js +55 -0
- package/dist/utils/math.d.ts +52 -0
- package/dist/utils/math.js +76 -0
- package/package.json +79 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { Point } from '@expofp/geometry';
|
|
2
|
+
import { createContext, use } from 'react';
|
|
3
|
+
import { Matrix4, Plane, Ray, Raycaster, Vector2, Vector3, Vector4, } from 'three';
|
|
4
|
+
import { createLogger } from '../utils/logging';
|
|
5
|
+
import { axisLength, clamp } from '../utils/math';
|
|
6
|
+
import { projectsFromFrustum } from './camera-kind';
|
|
7
|
+
const logger = createLogger('space');
|
|
8
|
+
/**
|
|
9
|
+
* The scene's space.
|
|
10
|
+
* @returns The space {@link Scene} published, or the identity one when rendered outside a scene.
|
|
11
|
+
*/
|
|
12
|
+
export function useSpace() {
|
|
13
|
+
return use(SpaceContext);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The mapping between the plan's own coordinates and the world the camera is placed in, and the
|
|
17
|
+
* conversions that ride on it.
|
|
18
|
+
*
|
|
19
|
+
* The mapping is one matrix. Without a `staticTransform` it is the **viewbox fit**: centre the
|
|
20
|
+
* viewbox on the world origin and scale it uniformly to best fill the drawing buffer. That fit is
|
|
21
|
+
* what makes one world unit one device pixel when the plan exactly fills the canvas, which in turn
|
|
22
|
+
* is what lets the camera's zoom math be written without reference to the scene at all — see
|
|
23
|
+
* {@link Camera}.
|
|
24
|
+
*
|
|
25
|
+
* With a `staticTransform` the matrix is whatever the caller supplied, and the fit is not computed:
|
|
26
|
+
* a host like maplibre places the plan itself, in its own space.
|
|
27
|
+
*
|
|
28
|
+
* Either way the matrix ends with {@link HANDEDNESS_FLIP}, which is what makes the space three's
|
|
29
|
+
* rather than the plan's or a host's — a positive elevation then lands nearer the camera.
|
|
30
|
+
*
|
|
31
|
+
* **Stable for the scene's lifetime.** A resize does not build a new space — {@link Scene} refits
|
|
32
|
+
* this one in place — so every holder reads the current fit through the same object and none of
|
|
33
|
+
* them re-render for it. What makes that safe is a reading discipline: render output must never be
|
|
34
|
+
* derived from these fields, because no render follows a refit. Read them at the moment of use —
|
|
35
|
+
* in event handlers, effects and the frame loop, which always run against the current fit. `Scene`
|
|
36
|
+
* itself is the one exception, and it re-applies the world matrix from the same effect that refits.
|
|
37
|
+
*/
|
|
38
|
+
export class Space {
|
|
39
|
+
/** Plan → world. */
|
|
40
|
+
matrix = new Matrix4();
|
|
41
|
+
/** World → plan. */
|
|
42
|
+
matrixInverse = new Matrix4();
|
|
43
|
+
_scale = 1;
|
|
44
|
+
_isExternal = false;
|
|
45
|
+
/** The canvas width in css pixels, which the canvas conversions measure against. */
|
|
46
|
+
width = 0;
|
|
47
|
+
/** The canvas height in css pixels. */
|
|
48
|
+
height = 0;
|
|
49
|
+
/**
|
|
50
|
+
* @param options - How to map the plan onto the canvas. Omit them entirely for the identity
|
|
51
|
+
* space, in which world coordinates already read as plan coordinates; the individual fields are
|
|
52
|
+
* not separately optional, because a fit needs all of them.
|
|
53
|
+
*/
|
|
54
|
+
constructor(options) {
|
|
55
|
+
if (options)
|
|
56
|
+
this.refit(options);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* World units per plan unit: the geometric mean of the two transformed axes, rather than the
|
|
60
|
+
* length of one of them.
|
|
61
|
+
*
|
|
62
|
+
* Both paths that build a matrix produce a similarity — the fit scales x and y alike, and the
|
|
63
|
+
* maplibre solver forces uniform scale and no shear — so the two axes agree and the mean is
|
|
64
|
+
* simply their common length. It differs only for a transform this renderer does not produce, and
|
|
65
|
+
* there no single number is right anyway: an anisotropic plan covers different amounts of plan per
|
|
66
|
+
* pixel along each axis, and the mean is the one that preserves area.
|
|
67
|
+
*/
|
|
68
|
+
get scale() {
|
|
69
|
+
return this._scale;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether an external host placed the plan itself, by supplying a valid `staticTransform`,
|
|
73
|
+
* rather than this renderer fitting the viewbox to the canvas — external mode, where the host
|
|
74
|
+
* owns the canvas, the camera and the frames.
|
|
75
|
+
*/
|
|
76
|
+
get isExternal() {
|
|
77
|
+
return this._isExternal;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Refits the plan onto the canvas in place, leaving every held reference reading the new fit.
|
|
81
|
+
* @param options - How to map the plan onto the canvas.
|
|
82
|
+
*/
|
|
83
|
+
refit(options) {
|
|
84
|
+
this.width = options.width;
|
|
85
|
+
this.height = options.height;
|
|
86
|
+
this._isExternal = this.validateStaticTransform(options.staticTransform);
|
|
87
|
+
if (this._isExternal) {
|
|
88
|
+
this.matrix.fromArray(options.staticTransform);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const fit = fitScale(options.viewbox, this.width * options.dpr, this.height * options.dpr);
|
|
92
|
+
const { x: centerX, y: centerY } = options.viewbox.center;
|
|
93
|
+
// three composes a matrix as T·R·S, so the plan-space translation has to be pre-scaled for the
|
|
94
|
+
// product to come out as scale∘translate — the order the imperative renderer built it in.
|
|
95
|
+
this.matrix
|
|
96
|
+
.makeTranslation(-centerX * fit, -centerY * fit, 0)
|
|
97
|
+
.scale(new Vector3(fit, fit, fit));
|
|
98
|
+
}
|
|
99
|
+
// Both paths land in a host's space or ours; this is what makes either one three's.
|
|
100
|
+
this.matrix.premultiply(HANDEDNESS_FLIP);
|
|
101
|
+
this.matrixInverse.copy(this.matrix).invert();
|
|
102
|
+
this._scale = Math.sqrt(axisLength(this.matrix, 0) * axisLength(this.matrix, 1));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Converts a point from plan to world coordinates.
|
|
106
|
+
* @param point - The plan-space point; a missing `z` is read as `0`.
|
|
107
|
+
* @param out - Where to write the result.
|
|
108
|
+
* @returns The world-space point.
|
|
109
|
+
*/
|
|
110
|
+
planToWorld(point, out = new Vector3()) {
|
|
111
|
+
return out.set(point.x, point.y, point.z ?? 0).applyMatrix4(this.matrix);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Converts a point from world to plan coordinates.
|
|
115
|
+
* @param point - The world-space point.
|
|
116
|
+
* @param out - Where to write the result.
|
|
117
|
+
* @returns The plan-space point.
|
|
118
|
+
*/
|
|
119
|
+
worldToPlan(point, out = new Point()) {
|
|
120
|
+
_world.set(point.x, point.y, point.z).applyMatrix4(this.matrixInverse);
|
|
121
|
+
out.set(_world.x, _world.y, _world.z);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Converts a point on the canvas — css pixels from its top-left corner — to normalized device
|
|
126
|
+
* coordinates, clamped to the canvas.
|
|
127
|
+
* @param point - The canvas-space point.
|
|
128
|
+
* @param out - Where to write the result.
|
|
129
|
+
* @returns The point in normalized device coordinates.
|
|
130
|
+
*/
|
|
131
|
+
canvasToNdc(point, out = new Vector2()) {
|
|
132
|
+
if (this.width <= 0 || this.height <= 0) {
|
|
133
|
+
logger.warn('canvasToNdc: the canvas has no size');
|
|
134
|
+
return out.set(0, 0);
|
|
135
|
+
}
|
|
136
|
+
const x = (point.x / this.width) * 2 - 1;
|
|
137
|
+
// NDC y grows upward where canvas y grows downward.
|
|
138
|
+
const y = -(point.y / this.height) * 2 + 1;
|
|
139
|
+
return out.set(clamp(x, -1, 1), clamp(y, -1, 1));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Where a point in normalized device coordinates lands on the canvas, in css pixels from its
|
|
143
|
+
* top-left corner.
|
|
144
|
+
*
|
|
145
|
+
* **Unclamped, where {@link canvasToNdc} clamps**, and the asymmetry is the point: clamping on the
|
|
146
|
+
* way in keeps a pointer event on the canvas it was dispatched from, while clamping on the way out
|
|
147
|
+
* would erase the one thing a caller asking about an off-screen point wants to know.
|
|
148
|
+
*
|
|
149
|
+
* No guard on a canvas with no size either, where `canvasToNdc` needs one — this multiplies by the
|
|
150
|
+
* size where that divides by it, so an unsized canvas gives an honest zero rather than a `NaN`.
|
|
151
|
+
* @param ndc - The point in normalized device coordinates.
|
|
152
|
+
* @param out - Where to write the result.
|
|
153
|
+
* @returns The canvas-space point.
|
|
154
|
+
*/
|
|
155
|
+
ndcToCanvas(ndc, out = new Point()) {
|
|
156
|
+
// Canvas y grows downward where NDC y grows upward. Zero elevation, always: a point on the
|
|
157
|
+
// canvas has none, and an `out` that carried one from an earlier use must not keep it.
|
|
158
|
+
return out.set(((ndc.x + 1) / 2) * this.width, ((1 - ndc.y) / 2) * this.height, 0);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Where a ray crosses the plan, in plan units.
|
|
162
|
+
*
|
|
163
|
+
* The ray is taken into plan space and crossed with plan `z = 0` there, rather than crossed with
|
|
164
|
+
* the plan's image in world space and converted back. Both agree for the viewbox fit; only this
|
|
165
|
+
* one stays correct for a `staticTransform` that rotates or lifts the plan off world `z = 0`.
|
|
166
|
+
* @param ray - The ray, in world space.
|
|
167
|
+
* @param out - Where to write the result.
|
|
168
|
+
* @returns The crossing, or `undefined` when the ray runs parallel to the plan.
|
|
169
|
+
*/
|
|
170
|
+
rayToPlan(ray, out = new Point()) {
|
|
171
|
+
_ray.copy(ray).applyMatrix4(this.matrixInverse);
|
|
172
|
+
if (!_ray.intersectPlane(PLAN_PLANE, _world))
|
|
173
|
+
return undefined;
|
|
174
|
+
return out.set(_world.x, _world.y, 0);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Where a point in normalized device coordinates lands on the plan.
|
|
178
|
+
* @param ndc - The point in normalized device coordinates.
|
|
179
|
+
* @param camera - The camera the canvas is drawn through.
|
|
180
|
+
* @param out - Where to write the result.
|
|
181
|
+
* @returns The plan position, or `undefined` when the camera never meets the plan.
|
|
182
|
+
*/
|
|
183
|
+
ndcToPlan(ndc, camera, out) {
|
|
184
|
+
castRay(_raycaster, ndc, camera);
|
|
185
|
+
return this.rayToPlan(_raycaster.ray, out);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Where a point on the plan lands in normalized device coordinates — the inverse of
|
|
189
|
+
* {@link ndcToPlan}, and the projection every question about "where is this on screen" is built
|
|
190
|
+
* from.
|
|
191
|
+
*
|
|
192
|
+
* Carried through **clip space** rather than through three's `Vector3.project`, which divides by
|
|
193
|
+
* `w` without looking at its sign: a point behind the camera comes back mirrored through the
|
|
194
|
+
* origin, and so lands inside the canvas as convincingly as a point in front of it. Reading `w`
|
|
195
|
+
* first is what separates the two.
|
|
196
|
+
*
|
|
197
|
+
* The camera's view offset rides in `projectionMatrix`, so a scene framed inside a `visibleRect`
|
|
198
|
+
* projects through this with no seam — the same way picking and culling follow it.
|
|
199
|
+
* @param point - The plan-space point; a missing `z` is read as `0`.
|
|
200
|
+
* @param camera - The camera the canvas is drawn through.
|
|
201
|
+
* @param out - Where to write the result.
|
|
202
|
+
* @returns The point in normalized device coordinates, unclamped, or `undefined` when it sits
|
|
203
|
+
* behind the camera and has no image on the canvas at all.
|
|
204
|
+
*/
|
|
205
|
+
planToNdc(point, camera, out = new Vector2()) {
|
|
206
|
+
this.planToWorld(point, _projected);
|
|
207
|
+
_clip
|
|
208
|
+
.set(_projected.x, _projected.y, _projected.z, 1)
|
|
209
|
+
.applyMatrix4(camera.matrixWorldInverse)
|
|
210
|
+
.applyMatrix4(camera.projectionMatrix);
|
|
211
|
+
if (_clip.w <= 0)
|
|
212
|
+
return undefined;
|
|
213
|
+
return out.set(_clip.x / _clip.w, _clip.y / _clip.w);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Where a point on the canvas — css pixels from its top-left corner — lands on the plan.
|
|
217
|
+
* @param point - The canvas-space point.
|
|
218
|
+
* @param camera - The camera the canvas is drawn through.
|
|
219
|
+
* @param out - Where to write the result.
|
|
220
|
+
* @returns The plan position, or `undefined` when the camera never meets the plan.
|
|
221
|
+
*/
|
|
222
|
+
canvasToPlan(point, camera, out) {
|
|
223
|
+
return this.ndcToPlan(this.canvasToNdc(point, _ndc), camera, out);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Where a point on the plan lands on the canvas, in css pixels from its top-left corner — the
|
|
227
|
+
* inverse of {@link canvasToPlan}, composed from the same two halves the other way round.
|
|
228
|
+
* @param point - The plan-space point; a missing `z` is read as `0`.
|
|
229
|
+
* @param camera - The camera the canvas is drawn through.
|
|
230
|
+
* @param out - Where to write the result.
|
|
231
|
+
* @returns The canvas position, unclamped, or `undefined` when the point sits behind the camera.
|
|
232
|
+
*/
|
|
233
|
+
planToCanvas(point, camera, out) {
|
|
234
|
+
const ndc = this.planToNdc(point, camera, _screen);
|
|
235
|
+
if (!ndc)
|
|
236
|
+
return undefined;
|
|
237
|
+
return this.ndcToCanvas(ndc, out);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether a caller supplied a usable plan → world matrix, warning when they supplied an unusable
|
|
241
|
+
* one rather than silently falling back.
|
|
242
|
+
* @param staticTransform - What the caller supplied, if anything.
|
|
243
|
+
* @returns Whether it should be used as the matrix.
|
|
244
|
+
*/
|
|
245
|
+
validateStaticTransform(staticTransform) {
|
|
246
|
+
if (staticTransform === undefined)
|
|
247
|
+
return false;
|
|
248
|
+
if (staticTransform.length !== 16) {
|
|
249
|
+
logger.warn(`staticTransform must be 16 elements long, got ${staticTransform.length}`);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The scale that fits a viewbox inside a drawing buffer, contain-style: the constraining axis fills
|
|
257
|
+
* and the other one keeps slack.
|
|
258
|
+
*
|
|
259
|
+
* Exported because it is also what tells a caller how large the plan is drawn: `VisibleRect` needs
|
|
260
|
+
* the plan's footprint in device pixels, and deriving it from this rather than from `Space.scale`
|
|
261
|
+
* keeps it free of the commit-ordering hazard the class documents — the fit is a function of the
|
|
262
|
+
* props it already holds.
|
|
263
|
+
* @param viewbox - The region to frame, in plan units.
|
|
264
|
+
* @param bufferWidth - The drawing buffer's width in device pixels.
|
|
265
|
+
* @param bufferHeight - The drawing buffer's height in device pixels.
|
|
266
|
+
* @returns World units per plan unit, or `1` while either is degenerate.
|
|
267
|
+
*/
|
|
268
|
+
export function fitScale(viewbox, bufferWidth, bufferHeight) {
|
|
269
|
+
const { width, height } = viewbox.size;
|
|
270
|
+
if (width <= 0 || height <= 0 || bufferWidth <= 0 || bufferHeight <= 0)
|
|
271
|
+
return 1;
|
|
272
|
+
return Math.min(bufferWidth / width, bufferHeight / height);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The scene's space, published by {@link Scene}. The default is the identity space, so a component
|
|
276
|
+
* rendered without one still converts — it just converts nothing.
|
|
277
|
+
*/
|
|
278
|
+
export const SpaceContext = createContext(new Space());
|
|
279
|
+
/**
|
|
280
|
+
* Points a raycaster from a camera through a point in normalized device coordinates.
|
|
281
|
+
*
|
|
282
|
+
* The fallback is for a camera whose projection was handed to it rather than derived from a frustum
|
|
283
|
+
* — an external host like maplibre supplies one — which three's own `setFromCamera` refuses. It unprojects the
|
|
284
|
+
* same screen point on the near and far planes; both lie on the true ray whatever the matrix folds,
|
|
285
|
+
* where anchoring the origin at the eye would need a camera transform such a camera does not carry.
|
|
286
|
+
*
|
|
287
|
+
* Exported for the external event system, which must aim R3F's raycaster the same way; the space's own
|
|
288
|
+
* conversions go through the module's private raycaster.
|
|
289
|
+
* @param raycaster - The raycaster to aim.
|
|
290
|
+
* @param ndc - The point in normalized device coordinates.
|
|
291
|
+
* @param camera - The camera to cast from.
|
|
292
|
+
*/
|
|
293
|
+
export function castRay(raycaster, ndc, camera) {
|
|
294
|
+
if (projectsFromFrustum(camera)) {
|
|
295
|
+
raycaster.setFromCamera(_cast.set(ndc.x, ndc.y), camera);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
raycaster.ray.origin.set(ndc.x, ndc.y, -1).unproject(camera);
|
|
299
|
+
raycaster.ray.direction
|
|
300
|
+
.set(ndc.x, ndc.y, 1)
|
|
301
|
+
.unproject(camera)
|
|
302
|
+
.sub(raycaster.ray.origin)
|
|
303
|
+
.normalize();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The last step of both paths into world space: mirror z.
|
|
307
|
+
*
|
|
308
|
+
* Plan space is left-handed — x right, y **down**, z up — where three's world is right-handed, so
|
|
309
|
+
* one axis has to flip and z is it: the plan's up is world `-z`, the side the camera is on.
|
|
310
|
+
*
|
|
311
|
+
* The flip belongs in the plan → world matrix and nowhere else. three reads a face's winding from
|
|
312
|
+
* that matrix's determinant alone (`matrixWorld.determinantAffine() < 0`) and never from the
|
|
313
|
+
* projection, so a reflection anywhere else is invisible to it: every `DoubleSide` lit surface
|
|
314
|
+
* negates its normal and shades from behind.
|
|
315
|
+
*
|
|
316
|
+
* **Assumes a host's world shares the plan's handedness**, which mercator does — x east, y south,
|
|
317
|
+
* z up — resolving the clash inside its own projection instead (`MercatorTransform._calcMatrices`
|
|
318
|
+
* scales by `[1, -1, 1]`). So the same flip serves both paths, and external mode undoes it on the
|
|
319
|
+
* host's own matrix (`setExternalCameraProjection`). A host with a right-handed world would
|
|
320
|
+
* need it skipped on both — a `leftHandedWorld` flag on the external contract is where that choice
|
|
321
|
+
* would go.
|
|
322
|
+
*/
|
|
323
|
+
export const HANDEDNESS_FLIP = new Matrix4().makeScale(1, 1, -1);
|
|
324
|
+
/**
|
|
325
|
+
* The plan's y-down convention, mirrored for a consumer that reads y as up.
|
|
326
|
+
*
|
|
327
|
+
* One value, two unrelated roles. In `ImageInstance.tsx` it is the plan-to-view y mirror composed
|
|
328
|
+
* into a screen-space quad's own 2D linear map, so the quad's offset reads correctly once the
|
|
329
|
+
* shader plants it in the view basis. In `MemberTexture.ts` it is the camera's own rest pose
|
|
330
|
+
* relative to the plan. Its job there is to stop that pose being applied twice, once baked into a
|
|
331
|
+
* member's own matrix and once more by the ordinary view transform at draw time. That role does
|
|
332
|
+
* not depend on the sign of a member's own `scale.y`. The requirement is that the billboard matrix
|
|
333
|
+
* equal the flat matrix at the rest pose, and the scale cancels from both sides of that equality.
|
|
334
|
+
*
|
|
335
|
+
* The two roles share a value by coincidence of geometry, not by a shared derivation. Check both
|
|
336
|
+
* call sites before changing this constant for either reason.
|
|
337
|
+
*/
|
|
338
|
+
export const Y_DOWN_FLIP = new Matrix4().makeScale(1, -1, 1);
|
|
339
|
+
/** The plan's own plane in plan space, which every pointer position is reported on. */
|
|
340
|
+
const PLAN_PLANE = new Plane(new Vector3(0, 0, 1), 0);
|
|
341
|
+
/**
|
|
342
|
+
* One raycaster for the module, not one per space and deliberately not R3F's.
|
|
343
|
+
*
|
|
344
|
+
* `Space` is React-free, so it cannot reach `state.raycaster`; and it should not, because R3F sets
|
|
345
|
+
* that one up for the pointer event it is dispatching — a coordinate query that overwrote its ray
|
|
346
|
+
* mid-dispatch would corrupt the hit test in flight.
|
|
347
|
+
*/
|
|
348
|
+
const _raycaster = new Raycaster();
|
|
349
|
+
const _ray = new Ray();
|
|
350
|
+
const _ndc = new Vector2();
|
|
351
|
+
/** The screen point {@link castRay} hands three's `setFromCamera`. */
|
|
352
|
+
const _cast = new Vector2();
|
|
353
|
+
const _world = new Vector3();
|
|
354
|
+
/** The world point {@link Space.planToNdc} projects, kept apart from `_world` so the two never alias. */
|
|
355
|
+
const _projected = new Vector3();
|
|
356
|
+
/** Where that projection lands before the perspective divide, which is where `w` can still be read. */
|
|
357
|
+
const _clip = new Vector4();
|
|
358
|
+
/** The projected point {@link Space.planToCanvas} hands on to {@link Space.ndcToCanvas}. */
|
|
359
|
+
const _screen = new Vector2();
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
//# 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
|
+
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;
|