@expofp/renderer 3.44.7 → 3.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/Renderer.js +7 -1
  2. package/dist/controls/hooks.d.ts +2 -1
  3. package/dist/core/sprites/BatchedSprites.js +2 -34
  4. package/dist/core/text/MemberTexture.js +49 -8
  5. package/dist/events/LayoutDriver.d.ts +5 -0
  6. package/dist/events/LayoutDriver.js +136 -29
  7. package/dist/events/layout-targets.d.ts +45 -3
  8. package/dist/events/layout-targets.js +129 -9
  9. package/dist/events/placement/box.d.ts +59 -0
  10. package/dist/events/placement/box.js +96 -0
  11. package/dist/events/placement/grid.d.ts +161 -0
  12. package/dist/events/placement/grid.js +235 -0
  13. package/dist/events/placement/index.d.ts +2 -0
  14. package/dist/events/placement/index.js +1 -0
  15. package/dist/events/placement/pass.d.ts +70 -0
  16. package/dist/events/placement/pass.js +617 -0
  17. package/dist/objects/images/ImageInstance.js +1 -0
  18. package/dist/objects/images/Images.js +2 -2
  19. package/dist/objects/images/image-container.d.ts +12 -0
  20. package/dist/objects/images/image-container.js +20 -4
  21. package/dist/objects/lighting/Lighting.js +5 -6
  22. package/dist/objects/materials/TextureMaterial.d.ts +11 -8
  23. package/dist/objects/materials/TextureMaterial.js +24 -17
  24. package/dist/objects/materials/effects/ao/index.d.ts +1 -1
  25. package/dist/objects/materials/effects/ao/index.js +3 -3
  26. package/dist/objects/materials/effects/contact-shadow.js +2 -2
  27. package/dist/objects/text/text-container.d.ts +16 -1
  28. package/dist/objects/text/text-container.js +23 -1
  29. package/dist/space/layout-measure.d.ts +12 -10
  30. package/dist/space/layout-measure.js +18 -16
  31. package/dist/types/defs.d.ts +157 -147
  32. package/dist/types/events.d.ts +5 -1
  33. package/dist/types/guards.d.ts +7 -0
  34. package/dist/types/guards.js +10 -0
  35. package/dist/utils/math.d.ts +18 -1
  36. package/dist/utils/math.js +35 -0
  37. package/package.json +2 -2
@@ -10,6 +10,12 @@ import { DIM_SCHEMA } from '../materials/effects/color-dim';
10
10
  * `uv * zw + xy` maps the quad's unit uv onto the region.
11
11
  */
12
12
  export const UV_OFFSET_UNIFORM = 'uvOffset';
13
+ /**
14
+ * The per-instance uniform each atlas mesh reads its opacity from. The container owns the name and
15
+ * hands it to the mesh's `TextureMaterial` as the `opacityUniform` prop, so the material patches its shader
16
+ * without knowing the name itself.
17
+ */
18
+ export const OPACITY_UNIFORM = 'instanceOpacity';
13
19
  /**
14
20
  * One atlas's mesh bookkeeping.
15
21
  *
@@ -51,6 +57,15 @@ export class ImageContainer extends SlotContainer {
51
57
  get facesCamera() {
52
58
  return isBatchedSprites(this.mesh);
53
59
  }
60
+ /**
61
+ * Writes a def's opacity into its slot, clamped, treating an absent value as fully opaque.
62
+ * @param slot - The instance's slot.
63
+ * @param opacity - The def's opacity, or `undefined` for the default.
64
+ */
65
+ setOpacityAt(slot, opacity) {
66
+ const value = opacity === undefined ? 1 : Math.min(Math.max(opacity, 0), 1);
67
+ this.mesh.setUniformAt(slot, OPACITY_UNIFORM, value);
68
+ }
54
69
  addInstance(packed) {
55
70
  // `addInstances` grows the buffers itself if the atlas ever overflows its sized capacity, so
56
71
  // there is nothing to pre-scan; the id is captured from the creation callback.
@@ -70,14 +85,15 @@ export class ImageContainer extends SlotContainer {
70
85
  }
71
86
  }
72
87
  /**
73
- * The schema the mesh declares (via `addUniforms`) so it can hold a {@link UV_OFFSET_UNIFORM} and a
74
- * dim per slot.
88
+ * The schema the mesh declares (via `addUniforms`) so it can hold a {@link UV_OFFSET_UNIFORM}, a
89
+ * {@link OPACITY_UNIFORM} and a dim per slot. The opacity float keeps the schema at two texels: a flat
90
+ * mesh is `vec4` + `float` + `float` = 6 floats, and a sprite mesh's own `anchor` brings it to 8.
75
91
  *
76
- * Both in one call although the two come from different places: `InstanceUniforms.add` merges and
92
+ * All in one call although they come from different places: `InstanceUniforms.add` merges and
77
93
  * rebuilds the backing texture, so declaring them separately would build it twice per bind.
78
94
  */
79
95
  const IMAGE_SCHEMA = {
80
- vertex: { [UV_OFFSET_UNIFORM]: 'vec4', ...DIM_SCHEMA.vertex },
96
+ vertex: { [UV_OFFSET_UNIFORM]: 'vec4', [OPACITY_UNIFORM]: 'float', ...DIM_SCHEMA.vertex },
81
97
  };
82
98
  // Scratch for the one uv-region upload a claim performs, filled and read within a single synchronous
83
99
  // statement.
@@ -349,15 +349,14 @@ function litWallPeak(directionalIntensity, pitchRadians, sky, ground, directiona
349
349
  /**
350
350
  * The rig this design agreed, and what each mount starts from.
351
351
  *
352
- * `contrast` is `0.7`, measured at the default `60°` pitch with these colors: the roof lands on
353
- * `1.00`, the lit wall on `0.92`, the wall facing away on `0.66`, the underside on `0.49`. At
354
- * `contrast = 1` the roof and the lit wall both sit on `1.00`, and the separation section 03 of the
355
- * design target asks for disappears.
352
+ * At any `contrast` the roof lands on `1.00`. At `contrast = 1` the lit wall reaches `1.00` as well,
353
+ * and the separation section 03 of the design target asks for disappears, so the default stays well
354
+ * under it.
356
355
  */
357
356
  const LIGHT_DEFAULTS = {
358
357
  trackPitch: false,
359
- pitchDegrees: 60,
360
- contrast: 0.7,
358
+ pitchDegrees: 38,
359
+ contrast: 0.3,
361
360
  skyColor: '#ffffff',
362
361
  groundColor: '#b9c1cc',
363
362
  directionalColor: '#fff3e6',
@@ -7,22 +7,25 @@ export interface TextureMaterialProps {
7
7
  */
8
8
  map: Texture;
9
9
  /**
10
- * The name of a per-instance `vec4` uniform to source the texture region from — for a texture
11
- * atlas on an instanced/batched mesh that declares and writes it. The mesh owns the name; the
12
- * material only patches its shader to sample `map` through it. Omit it for a plain textured
13
- * material: the shader is left unpatched, so the material draws its whole `map` and works in any
14
- * context, not only an instanced one.
10
+ * The name of a per-instance `vec4` uniform to source the texture region from, for an atlas on an
11
+ * instanced/batched mesh that declares and writes it. The mesh owns the name; the material only
12
+ * patches its shader to sample `map` through it. Omit it to draw the whole `map` unpatched, in any
13
+ * context.
15
14
  */
16
15
  uvOffset?: string;
16
+ /**
17
+ * The name of a per-instance `float` uniform holding the instance's opacity, for a mesh that
18
+ * declares and writes one. Omit it to leave the fragment shader unpatched.
19
+ */
20
+ opacityUniform?: string;
17
21
  }
18
22
  /**
19
23
  * The material used to render a texture, optionally through a per-instance uv-offset uniform so an
20
24
  * instanced/batched mesh can pack many regions into one atlas.
21
25
  *
22
26
  * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
23
- * `<uv_vertex>` has run, so the instance's atlas region replaces whatever the stock chunk computed.
24
- * Instanced meshes wrap this `onBeforeCompile` rather than replacing it, and inject their uniform
25
- * fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
27
+ * `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
28
+ * and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
26
29
  * @param props - The material's texture and optional uv-offset uniform name.
27
30
  * @returns The material element.
28
31
  */
@@ -8,34 +8,41 @@ import { overlayParams, sharedParams } from './shared';
8
8
  * instanced/batched mesh can pack many regions into one atlas.
9
9
  *
10
10
  * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
11
- * `<uv_vertex>` has run, so the instance's atlas region replaces whatever the stock chunk computed.
12
- * Instanced meshes wrap this `onBeforeCompile` rather than replacing it, and inject their uniform
13
- * fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
11
+ * `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
12
+ * and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
14
13
  * @param props - The material's texture and optional uv-offset uniform name.
15
14
  * @returns The material element.
16
15
  */
17
16
  export function TextureMaterial(props) {
18
- const { map, uvOffset } = props;
17
+ const { map, uvOffset, opacityUniform } = props;
19
18
  const applyDim = useDimPatch();
20
19
  const overlay = useLayerOverlay();
21
20
  function onBeforeCompile(parameters) {
22
21
  applyDim(parameters);
23
- if (!uvOffset)
24
- return;
25
- parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>',
26
- /*glsl*/ `
22
+ if (uvOffset) {
23
+ parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>',
24
+ /*glsl*/ `
27
25
  #include <uv_vertex>
28
26
  vMapUv = uv * ${uvOffset}.zw + ${uvOffset}.xy;
29
27
  `.trim());
28
+ }
29
+ if (opacityUniform) {
30
+ // All four channels: the blend is premultiplied, so rgb already carries the texel's alpha and
31
+ // must carry the opacity too. Scaling `a` alone leaves a bright halo instead of a faded pin.
32
+ //
33
+ // After `colorspace_fragment`, so rgb is already sRGB-encoded and blends with the same weight
34
+ // as alpha. Before the encode, `0.5` would read about 73% opaque. Dim patches an earlier chunk
35
+ // (`tonemapping_fragment`), so the two never share an anchor.
36
+ parameters.fragmentShader = parameters.fragmentShader.replace('#include <colorspace_fragment>',
37
+ /*glsl*/ `
38
+ #include <colorspace_fragment>
39
+ gl_FragColor *= ${opacityUniform};
40
+ `.trim());
41
+ }
30
42
  }
31
- // An atlas texture is premultiplied (see `makeAtlasTexture`'s `premultiplyAlpha` comment), so
32
- // this material needs the premultiplied blend, not `sharedParams`'s straight-alpha one.
33
- // `blendSrc: OneFactor` is the only change: the source term becomes `1 * (c * a)`, which already
34
- // equals the straight blend's own `c * a` source term, so the composited color comes out the
35
- // same either way. `blendDst` stays `OneMinusSrcAlphaFactor` from `sharedParams`, since that half
36
- // does not depend on how the source is stored.
37
- //
38
- // The override lives here, not in `sharedParams`, because `ColorMaterial` and `LineMaterial`
39
- // share that object and their colors are never premultiplied.
43
+ // An atlas texture is premultiplied (see `makeAtlasTexture`), so this needs `blendSrc: OneFactor`
44
+ // rather than `sharedParams`'s straight-alpha source term; `blendDst` is unaffected. The override
45
+ // lives here because `ColorMaterial` and `LineMaterial` share `sharedParams` and are never
46
+ // premultiplied.
40
47
  return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
41
48
  }
@@ -103,7 +103,7 @@ export declare const OCCLUSION_DEFAULTS: {
103
103
  * How far the term looks for an occluder, in plan units.
104
104
  *
105
105
  * Chosen against the story plans, whose booths run about a hundred plan units across. That makes
106
- * a band about a tenth of a booth wide, where a junction's shadow reads as shading rather than as
106
+ * a band under a tenth of a booth wide, where a junction's shadow reads as shading rather than as
107
107
  * an outline.
108
108
  */
109
109
  radius: number;
@@ -47,12 +47,12 @@ export const OCCLUSION_DEFAULTS = {
47
47
  * How far the term looks for an occluder, in plan units.
48
48
  *
49
49
  * Chosen against the story plans, whose booths run about a hundred plan units across. That makes
50
- * a band about a tenth of a booth wide, where a junction's shadow reads as shading rather than as
50
+ * a band under a tenth of a booth wide, where a junction's shadow reads as shading rather than as
51
51
  * an outline.
52
52
  */
53
- radius: 12,
53
+ radius: 8,
54
54
  /** How far the read blends an occluded fragment toward the shadow color, from `0` to `1`. */
55
- strength: 0.5,
55
+ strength: 0.35,
56
56
  /**
57
57
  * The blur kernel's side length in texels. It starts at the side that covers the rotation tile,
58
58
  * see {@link AmbientOcclusion.aoBlur}.
@@ -75,9 +75,9 @@ export function createContactShadow() {
75
75
  */
76
76
  export const CONTACT_SHADOW_DEFAULTS = {
77
77
  /** How far above a volume's base the shadow reaches, in plan units. */
78
- distance: 3,
78
+ distance: 1.5,
79
79
  /** How far the base is blended toward the shadow color, from `0` to `1`. */
80
- strength: 0.35,
80
+ strength: 0.2,
81
81
  /** The shade the shadow blends toward. */
82
82
  color: '#4a5260',
83
83
  };
@@ -48,6 +48,20 @@ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl
48
48
  * the lifetime of a canvas, so capturing it here is safe.
49
49
  */
50
50
  constructor(invalidate: () => void);
51
+ /**
52
+ * Binds the batch, and asks for a frame each time it finishes a sync.
53
+ *
54
+ * {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
55
+ * frame callback — a layout rule's, or a placement report's drained before the render — flags the
56
+ * batch before that frame renders, so the batch's own render hook starts the sync, with no
57
+ * callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
58
+ * after the last frame anyone asked for, and the old text stays on screen until something else
59
+ * happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
60
+ * three has uploaded the batch's attributes by then. The batch's own completion is the one signal
61
+ * both paths share.
62
+ * @param mesh - The mounted batch, or the ref-cleanup `null`.
63
+ */
64
+ bind(mesh: BatchedTextImpl | null): void;
51
65
  /**
52
66
  * The members backing one def.
53
67
  * @param slot - The def's group.
@@ -61,7 +75,8 @@ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl
61
75
  * and parses the font — and under on-demand rendering a sync started during a frame has nothing to
62
76
  * request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
63
77
  * what closes that loop; without it new text appears only when something else happens to
64
- * invalidate.
78
+ * invalidate. A sync this did not start is covered by the batch's own completion, which
79
+ * {@link bind} listens for.
65
80
  */
66
81
  scheduleSync(): void;
67
82
  claim(def: RenderableDef, lineCount: number): number;
@@ -45,6 +45,27 @@ export class TextContainer extends SlotContainer {
45
45
  super();
46
46
  this.invalidate = invalidate;
47
47
  }
48
+ /**
49
+ * Binds the batch, and asks for a frame each time it finishes a sync.
50
+ *
51
+ * {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
52
+ * frame callback — a layout rule's, or a placement report's drained before the render — flags the
53
+ * batch before that frame renders, so the batch's own render hook starts the sync, with no
54
+ * callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
55
+ * after the last frame anyone asked for, and the old text stays on screen until something else
56
+ * happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
57
+ * three has uploaded the batch's attributes by then. The batch's own completion is the one signal
58
+ * both paths share.
59
+ * @param mesh - The mounted batch, or the ref-cleanup `null`.
60
+ */
61
+ bind(mesh) {
62
+ // `mesh` reads the bound batch, which is undefined until the first bind; the base ignores a
63
+ // re-bind, so the listener is added once too.
64
+ const first = mesh !== null && this.mesh === undefined;
65
+ super.bind(mesh);
66
+ if (first)
67
+ mesh.addEventListener('synccomplete', () => this.invalidate());
68
+ }
48
69
  /**
49
70
  * The members backing one def.
50
71
  * @param slot - The def's group.
@@ -60,7 +81,8 @@ export class TextContainer extends SlotContainer {
60
81
  * and parses the font — and under on-demand rendering a sync started during a frame has nothing to
61
82
  * request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
62
83
  * what closes that loop; without it new text appears only when something else happens to
63
- * invalidate.
84
+ * invalidate. A sync this did not start is covered by the batch's own completion, which
85
+ * {@link bind} listens for.
64
86
  */
65
87
  scheduleSync() {
66
88
  if (this.syncScheduled)
@@ -2,7 +2,7 @@ import { type Camera } from 'three';
2
2
  import { type CameraControlsImpl } from '../controls';
3
3
  import { type Space } from './Space';
4
4
  /**
5
- * How much of the plan one device pixel spans, in plan units — what everything sized in pixels is
5
+ * How much of the plan one CSS pixel spans, in plan units — what everything sized in pixels is
6
6
  * authored against, and what the imperative renderer published as `ptScale`.
7
7
  *
8
8
  * **One number for a view that has many.** Perspective foreshortens a pitched plan, so no single
@@ -21,16 +21,18 @@ import { type Space } from './Space';
21
21
  * Neither path knows a `visibleRect` exists: its framing rides in `projectionMatrix`, which one
22
22
  * reads `m00` out of and the other projects through.
23
23
  *
24
- * **Device pixels, not css pixels** — a plan at dpr 2 reports half the pixel size of the same plan
25
- * at dpr 1, as the imperative renderer did measuring `canvas.height`.
24
+ * **CSS pixels, not device pixels** — a plan reports the same pixel size at every device ratio. The
25
+ * renderer's public surface is CSS pixels, because a caller authors in CSS pixels and the ratio of
26
+ * the drawing buffer is the renderer's own business; the caller hands over the canvas's width in
27
+ * CSS pixels and the ratio never enters here.
26
28
  * @param camera - The camera in use.
27
29
  * @param space - The scene's space.
28
- * @param bufferWidth - The drawing buffer's width in device pixels.
30
+ * @param cssWidth - The canvas's width in CSS pixels.
29
31
  * @param controls - The camera controls, when mounted; they own the distance to the target.
30
- * @returns Plan units per device pixel, or `undefined` while the camera misses the plan or the
31
- * buffer has no width.
32
+ * @returns Plan units per CSS pixel, or `undefined` while the camera misses the plan or the canvas
33
+ * has no width.
32
34
  */
33
- export declare function measurePixelSize(camera: Camera, space: Space, bufferWidth: number, controls?: CameraControlsImpl): number | undefined;
35
+ export declare function measurePixelSize(camera: Camera, space: Space, cssWidth: number, controls?: CameraControlsImpl): number | undefined;
34
36
  /**
35
37
  * Which way the plan is turned, in radians clockwise from north — {@link measurePixelSize}'s
36
38
  * counterpart on the roll axis, split the same way.
@@ -46,12 +48,12 @@ export declare function measurePixelSize(camera: Camera, space: Space, bufferWid
46
48
  * imperative renderer used, and with it the question of how far apart to place it.
47
49
  * @param camera - The camera in use.
48
50
  * @param space - The scene's space.
49
- * @param bufferWidth - The drawing buffer's width in device pixels.
50
- * @param bufferHeight - Its height, which weights the two NDC axes back into pixels.
51
+ * @param cssWidth - The canvas's width in CSS pixels.
52
+ * @param cssHeight - Its height in CSS pixels, which weights the two NDC axes back into pixels.
51
53
  * @param controls - The camera controls, when mounted; they hold the angle outright.
52
54
  * @returns The roll in `[0, 2π)`, or `undefined` while the camera misses the plan.
53
55
  */
54
- export declare function measureRoll(camera: Camera, space: Space, bufferWidth: number, bufferHeight: number, controls?: CameraControlsImpl): number | undefined;
56
+ export declare function measureRoll(camera: Camera, space: Space, cssWidth: number, cssHeight: number, controls?: CameraControlsImpl): number | undefined;
55
57
  /**
56
58
  * Whether a new pixel size differs enough from the last one to re-lay out on.
57
59
  *
@@ -4,7 +4,7 @@ import { bearing } from '../controls';
4
4
  import { euclideanModulo } from '../utils/math';
5
5
  import { isOrthographic, isPerspective } from './camera-kind';
6
6
  /**
7
- * How much of the plan one device pixel spans, in plan units — what everything sized in pixels is
7
+ * How much of the plan one CSS pixel spans, in plan units — what everything sized in pixels is
8
8
  * authored against, and what the imperative renderer published as `ptScale`.
9
9
  *
10
10
  * **One number for a view that has many.** Perspective foreshortens a pitched plan, so no single
@@ -23,24 +23,26 @@ import { isOrthographic, isPerspective } from './camera-kind';
23
23
  * Neither path knows a `visibleRect` exists: its framing rides in `projectionMatrix`, which one
24
24
  * reads `m00` out of and the other projects through.
25
25
  *
26
- * **Device pixels, not css pixels** — a plan at dpr 2 reports half the pixel size of the same plan
27
- * at dpr 1, as the imperative renderer did measuring `canvas.height`.
26
+ * **CSS pixels, not device pixels** — a plan reports the same pixel size at every device ratio. The
27
+ * renderer's public surface is CSS pixels, because a caller authors in CSS pixels and the ratio of
28
+ * the drawing buffer is the renderer's own business; the caller hands over the canvas's width in
29
+ * CSS pixels and the ratio never enters here.
28
30
  * @param camera - The camera in use.
29
31
  * @param space - The scene's space.
30
- * @param bufferWidth - The drawing buffer's width in device pixels.
32
+ * @param cssWidth - The canvas's width in CSS pixels.
31
33
  * @param controls - The camera controls, when mounted; they own the distance to the target.
32
- * @returns Plan units per device pixel, or `undefined` while the camera misses the plan or the
33
- * buffer has no width.
34
+ * @returns Plan units per CSS pixel, or `undefined` while the camera misses the plan or the canvas
35
+ * has no width.
34
36
  */
35
- export function measurePixelSize(camera, space, bufferWidth, controls) {
36
- if (!(bufferWidth > 0) || !(space.scale > 0))
37
+ export function measurePixelSize(camera, space, cssWidth, controls) {
38
+ if (!(cssWidth > 0) || !(space.scale > 0))
37
39
  return undefined;
38
40
  if (controls) {
39
41
  const worldWidth = visibleWorldWidth(camera, controls.distance);
40
42
  if (!(worldWidth > 0))
41
43
  return undefined;
42
44
  // The camera measures the world, and only the fit says how much plan that world is.
43
- return worldWidth / bufferWidth / space.scale;
45
+ return worldWidth / cssWidth / space.scale;
44
46
  }
45
47
  const aim = aimedAt(camera, space);
46
48
  if (!aim)
@@ -56,7 +58,7 @@ export function measurePixelSize(camera, space, bufferWidth, controls) {
56
58
  const planLength = Math.hypot(right.x - left.x, right.y - left.y);
57
59
  if (!(planLength > 0))
58
60
  return undefined;
59
- return planLength / bufferWidth;
61
+ return planLength / cssWidth;
60
62
  }
61
63
  /**
62
64
  * Which way the plan is turned, in radians clockwise from north — {@link measurePixelSize}'s
@@ -73,12 +75,12 @@ export function measurePixelSize(camera, space, bufferWidth, controls) {
73
75
  * imperative renderer used, and with it the question of how far apart to place it.
74
76
  * @param camera - The camera in use.
75
77
  * @param space - The scene's space.
76
- * @param bufferWidth - The drawing buffer's width in device pixels.
77
- * @param bufferHeight - Its height, which weights the two NDC axes back into pixels.
78
+ * @param cssWidth - The canvas's width in CSS pixels.
79
+ * @param cssHeight - Its height in CSS pixels, which weights the two NDC axes back into pixels.
78
80
  * @param controls - The camera controls, when mounted; they hold the angle outright.
79
81
  * @returns The roll in `[0, 2π)`, or `undefined` while the camera misses the plan.
80
82
  */
81
- export function measureRoll(camera, space, bufferWidth, bufferHeight, controls) {
83
+ export function measureRoll(camera, space, cssWidth, cssHeight, controls) {
82
84
  if (controls)
83
85
  return bearing(controls);
84
86
  const aim = aimedAt(camera, space);
@@ -93,8 +95,8 @@ export function measureRoll(camera, space, bufferWidth, bufferHeight, controls)
93
95
  // d(ndc)/dx by the quotient rule, less its `1/w²` — positive, so it moves no direction. NDC is
94
96
  // aspect-distorted, so each axis is weighted back into pixels; canvas y grows downward where NDC
95
97
  // y grows up.
96
- const dx = (column[0] * clip.w - clip.x * column[3]) * bufferWidth;
97
- const dy = -(column[1] * clip.w - clip.y * column[3]) * bufferHeight;
98
+ const dx = (column[0] * clip.w - clip.x * column[3]) * cssWidth;
99
+ const dy = -(column[1] * clip.w - clip.y * column[3]) * cssHeight;
98
100
  if (dx === 0 && dy === 0)
99
101
  return undefined;
100
102
  // The plan turned clockwise by θ shows its east axis at screen angle -θ.
@@ -137,7 +139,7 @@ function aimedAt(camera, space) {
137
139
  * installs, which leaves `fov` untouched.
138
140
  *
139
141
  * Width where the imperative renderer took height, so both of {@link measurePixelSize}'s paths
140
- * divide by the one buffer dimension they are given; for a projection whose aspect is the buffer's
142
+ * divide by the one canvas dimension they are given; for a projection whose aspect is the buffer's
141
143
  * the two agree exactly.
142
144
  * @param camera - The camera in use.
143
145
  * @param distance - Distance from the camera to the point it is aimed at, in world units.