@expofp/renderer 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +21 -6
- package/dist/index.js +261 -151
- package/package.json +1 -3
package/dist/index.d.ts
CHANGED
|
@@ -274,8 +274,14 @@ export declare interface LayerDef extends SharedDef {
|
|
|
274
274
|
interactive?: boolean;
|
|
275
275
|
/** Collection of layer's children. Mixing different types of children is not supported. */
|
|
276
276
|
children: RenderableDefCollection;
|
|
277
|
-
/**
|
|
278
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Layer's rendering mode. Propagates to children: child layers inherit the parent's mode
|
|
279
|
+
* unless they specify their own. Defaults to `"2D"` at the root.
|
|
280
|
+
* Resolved once during scene build and should not be changed afterwards.
|
|
281
|
+
* - `"2D"`: painter's algorithm (AlwaysDepth). Layer order determines overlap.
|
|
282
|
+
* - `"3D"`: depth testing (LessDepth). Closer geometry occludes farther geometry.
|
|
283
|
+
*/
|
|
284
|
+
readonly mode?: "2D" | "3D";
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
/** Line definition */
|
|
@@ -338,7 +344,11 @@ export declare class Polygon {
|
|
|
338
344
|
get vertices(): readonly Vector3Like[];
|
|
339
345
|
/** Array of polygon indices. Each index is a triplet of vertex indices forming a triangle. */
|
|
340
346
|
get indices(): readonly Index[];
|
|
341
|
-
/**
|
|
347
|
+
/**
|
|
348
|
+
* Bounding rectangle of the polygon.
|
|
349
|
+
* This is a 2D axis-aligned bounding box computed from the X/Y projection of vertices.
|
|
350
|
+
* The Z extent of the polygon is not tracked.
|
|
351
|
+
*/
|
|
342
352
|
get bounds(): Rect;
|
|
343
353
|
/**
|
|
344
354
|
* Converts a {@link Rect} to a {@link Polygon}.
|
|
@@ -367,7 +377,8 @@ export declare class Polygon {
|
|
|
367
377
|
rotate(rotation: number, center?: Vector2Like): Polygon;
|
|
368
378
|
/**
|
|
369
379
|
* Scales the polygon around the given origin.
|
|
370
|
-
* @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
|
|
380
|
+
* @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
|
|
381
|
+
* vertical axes will be scaled by the same factor. Z is unchanged in both cases.
|
|
371
382
|
* @param origin Origin of the scaling. If omitted, defaults to the bounding rectangle center.
|
|
372
383
|
* @returns this {@link Polygon} instance
|
|
373
384
|
*/
|
|
@@ -387,6 +398,8 @@ export declare interface PtScaleEventData {
|
|
|
387
398
|
export declare class Rect {
|
|
388
399
|
/** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
|
|
389
400
|
rotation: number;
|
|
401
|
+
/** Optional elevation of the rectangle. Positive values raise geometry toward the viewer. Defaults to 0. */
|
|
402
|
+
elevation: number;
|
|
390
403
|
private _min;
|
|
391
404
|
private _max;
|
|
392
405
|
private _center;
|
|
@@ -395,8 +408,9 @@ export declare class Rect {
|
|
|
395
408
|
* @param min Top left corner of the rectangle.
|
|
396
409
|
* @param max Bottom right corner of the rectangle.
|
|
397
410
|
* @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
|
|
411
|
+
* @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
|
|
398
412
|
*/
|
|
399
|
-
constructor(min: IVector2, max: IVector2, rotation?: number);
|
|
413
|
+
constructor(min: IVector2, max: IVector2, rotation?: number, elevation?: number);
|
|
400
414
|
/** Top left corner of the rectangle. */
|
|
401
415
|
get min(): Vector2Like;
|
|
402
416
|
/** Set top left corner of the rectangle. */
|
|
@@ -417,9 +431,10 @@ export declare class Rect {
|
|
|
417
431
|
* Creates a rectangle from an SVG rectangle element.
|
|
418
432
|
* @param rect {@link SVGRectElement} or {@link SVGImageElement}
|
|
419
433
|
* @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
|
|
434
|
+
* @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
|
|
420
435
|
* @returns new {@link Rect} instance
|
|
421
436
|
*/
|
|
422
|
-
static fromSvg(rect: SVGRectElement | SVGImageElement, rotation?: number): Rect;
|
|
437
|
+
static fromSvg(rect: SVGRectElement | SVGImageElement, rotation?: number, elevation?: number): Rect;
|
|
423
438
|
/**
|
|
424
439
|
* Moves the rectangle by the given offset.
|
|
425
440
|
* @param offset Offset to move the rectangle by.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
var _a;
|
|
5
|
-
import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Frustum, Sphere, Box3, DynamicDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion,
|
|
5
|
+
import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Frustum, Sphere, Box3, DynamicDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, LessEqualDepth, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, BufferGeometry, LinearSRGBColorSpace, Mesh, Camera, Spherical, MathUtils, Plane, Raycaster, PerspectiveCamera, Scene, Clock, WebGLRenderer } from "three";
|
|
6
6
|
import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
|
|
7
7
|
import createLog from "debug";
|
|
8
8
|
import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
|
|
@@ -867,8 +867,6 @@ class Text extends Text$1 {
|
|
|
867
867
|
if (!this._batchParent) this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
|
|
868
868
|
}
|
|
869
869
|
uniforms.uTroikaSDFDebug.value = !!this.debugSDF;
|
|
870
|
-
material.polygonOffset = !!this.depthOffset;
|
|
871
|
-
material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0;
|
|
872
870
|
const color = isOutline ? this.outlineColor || 0 : this.color;
|
|
873
871
|
if (color == null) {
|
|
874
872
|
delete material.color;
|
|
@@ -1050,6 +1048,7 @@ const dimColorFrag = (
|
|
|
1050
1048
|
return vec4(m * col.a, col.a);
|
|
1051
1049
|
}`
|
|
1052
1050
|
);
|
|
1051
|
+
const POLYGON_OFFSET_MULTIPLIER = 12;
|
|
1053
1052
|
const sharedParameters = {
|
|
1054
1053
|
side: DoubleSide,
|
|
1055
1054
|
transparent: true,
|
|
@@ -1094,47 +1093,47 @@ class MaterialSystem {
|
|
|
1094
1093
|
}
|
|
1095
1094
|
};
|
|
1096
1095
|
addDimToMaterial(material);
|
|
1096
|
+
this.addPolygonOffset(material);
|
|
1097
1097
|
return material;
|
|
1098
1098
|
}
|
|
1099
1099
|
/**
|
|
1100
1100
|
* Creates a color material.
|
|
1101
|
-
* @param
|
|
1101
|
+
* @param params {@link MaterialColorParams}
|
|
1102
1102
|
* @returns MeshBasicMaterial instance
|
|
1103
1103
|
*/
|
|
1104
|
-
createColorMaterial(
|
|
1105
|
-
const params = {
|
|
1106
|
-
color: partialParams.color ?? 16777215,
|
|
1107
|
-
opacity: partialParams.opacity ?? 1
|
|
1108
|
-
};
|
|
1104
|
+
createColorMaterial(params = {}) {
|
|
1109
1105
|
const material = new MeshBasicMaterial({
|
|
1110
1106
|
...sharedParameters,
|
|
1111
|
-
color: params.color,
|
|
1112
|
-
opacity: params.opacity
|
|
1107
|
+
color: params.color ?? 16777215,
|
|
1108
|
+
opacity: params.opacity ?? 1
|
|
1113
1109
|
});
|
|
1110
|
+
if (params.is3D) material.depthFunc = LessEqualDepth;
|
|
1114
1111
|
addDimToMaterial(material);
|
|
1112
|
+
this.addPolygonOffset(material);
|
|
1115
1113
|
return material;
|
|
1116
1114
|
}
|
|
1117
1115
|
/**
|
|
1118
1116
|
* Creates a texture material.
|
|
1119
|
-
* @param
|
|
1120
|
-
* @param uvOffset whether to enable uv offset with per instance uniforms (for texture atlases)
|
|
1117
|
+
* @param params {@link MaterialTextureParams}
|
|
1121
1118
|
* @returns MeshBasicMaterial instance
|
|
1122
1119
|
*/
|
|
1123
|
-
createTextureMaterial(
|
|
1124
|
-
const material = new MeshBasicMaterial({ ...sharedParameters, map });
|
|
1125
|
-
if (
|
|
1120
|
+
createTextureMaterial(params) {
|
|
1121
|
+
const material = new MeshBasicMaterial({ ...sharedParameters, map: params.map });
|
|
1122
|
+
if (params.is3D) material.depthFunc = LessEqualDepth;
|
|
1123
|
+
if (params.uvOffset) {
|
|
1126
1124
|
material.onBeforeCompile = (shader) => {
|
|
1127
1125
|
shader.vertexShader = shader.vertexShader.replace(
|
|
1128
1126
|
"#include <uv_vertex>",
|
|
1129
1127
|
/*glsl*/
|
|
1130
1128
|
`
|
|
1131
1129
|
#include <uv_vertex>
|
|
1132
|
-
vMapUv = uv * uvOffset.zw + uvOffset.xy;
|
|
1130
|
+
vMapUv = uv * uvOffset.zw + uvOffset.xy;
|
|
1133
1131
|
`
|
|
1134
1132
|
);
|
|
1135
1133
|
};
|
|
1136
1134
|
}
|
|
1137
1135
|
addDimToMaterial(material);
|
|
1136
|
+
this.addPolygonOffset(material);
|
|
1138
1137
|
return material;
|
|
1139
1138
|
}
|
|
1140
1139
|
/**
|
|
@@ -1158,6 +1157,40 @@ class MaterialSystem {
|
|
|
1158
1157
|
}
|
|
1159
1158
|
return this.backgroundMaterial;
|
|
1160
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Patches a material's onBeforeRender to dynamically set polygonOffset based on the
|
|
1162
|
+
* object's renderOrder. Later layers (higher renderOrder) get a larger negative offset,
|
|
1163
|
+
* giving them a slight depth buffer advantage over earlier layers at the same elevation.
|
|
1164
|
+
* This prevents z-fighting between coplanar 3D content across different layers.
|
|
1165
|
+
*
|
|
1166
|
+
* The units multiplier is derived from {@link CameraControls.maxPolarAngle}:
|
|
1167
|
+
*
|
|
1168
|
+
* multiplier = ⌈tan(maxPolarAngle)⌉
|
|
1169
|
+
*
|
|
1170
|
+
* At steep camera pitch, coplanar surfaces from different draw calls (different
|
|
1171
|
+
* BatchedMesh instances) compute slightly different depth values at the same pixel
|
|
1172
|
+
* due to floating-point interpolation differences in the vertex shader. These errors
|
|
1173
|
+
* are proportional to the depth gradient across the surface, which for a horizontal
|
|
1174
|
+
* plane at pitch angle θ is proportional to tan(θ). The multiplier ensures the
|
|
1175
|
+
* per-layer polygon offset exceeds these errors at the configured maximum pitch.
|
|
1176
|
+
*
|
|
1177
|
+
* For elevated content the effective viewing angle can exceed maxPolarAngle, but this
|
|
1178
|
+
* is self-correcting: the screen-space height of the surface shrinks as cos(angle),
|
|
1179
|
+
* so the total visible z-fighting (pixels × severity ∝ cos × tan = sin) is bounded
|
|
1180
|
+
* and becomes subpixel as the effective angle approaches 90°.
|
|
1181
|
+
*
|
|
1182
|
+
* With maxPolarAngle = 85°: ⌈tan(85°)⌉ = ⌈11.43⌉ = 12.
|
|
1183
|
+
* @param material Material to patch
|
|
1184
|
+
*/
|
|
1185
|
+
addPolygonOffset(material) {
|
|
1186
|
+
const onBeforeRender = material.onBeforeRender.bind(material);
|
|
1187
|
+
material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
|
|
1188
|
+
onBeforeRender(renderer, scene, camera, geometry, object, group);
|
|
1189
|
+
material.polygonOffset = true;
|
|
1190
|
+
material.polygonOffsetFactor = 0;
|
|
1191
|
+
material.polygonOffsetUnits = -object.renderOrder * POLYGON_OFFSET_MULTIPLIER;
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1161
1194
|
}
|
|
1162
1195
|
function isShapeDef(def) {
|
|
1163
1196
|
return def.shape !== void 0;
|
|
@@ -1189,36 +1222,14 @@ function isLineLayer(layer) {
|
|
|
1189
1222
|
function isLayerLayer(layer) {
|
|
1190
1223
|
return layer.children[0] && isLayerDef(layer.children[0]);
|
|
1191
1224
|
}
|
|
1192
|
-
function groupBy(list, keyGetter) {
|
|
1193
|
-
const map = /* @__PURE__ */ new Map();
|
|
1194
|
-
list.forEach((item) => {
|
|
1195
|
-
const key = keyGetter(item);
|
|
1196
|
-
const collection = map.get(key);
|
|
1197
|
-
if (!collection) {
|
|
1198
|
-
map.set(key, [item]);
|
|
1199
|
-
} else {
|
|
1200
|
-
collection.push(item);
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
return map;
|
|
1204
|
-
}
|
|
1205
|
-
function partition(list, pred) {
|
|
1206
|
-
const truthy = [];
|
|
1207
|
-
const falsy = [];
|
|
1208
|
-
for (const item of list) {
|
|
1209
|
-
if (pred(item)) {
|
|
1210
|
-
truthy.push(item);
|
|
1211
|
-
} else {
|
|
1212
|
-
falsy.push(item);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
return [truthy, falsy];
|
|
1216
|
-
}
|
|
1217
1225
|
const INTERACTIVE_LAYER = 1;
|
|
1218
|
-
function
|
|
1219
|
-
|
|
1226
|
+
function isInteractive(object) {
|
|
1227
|
+
return object.layers.isEnabled(INTERACTIVE_LAYER);
|
|
1228
|
+
}
|
|
1229
|
+
function setInteractive(object, isInteractive2) {
|
|
1230
|
+
if (isInteractive2) object.layers.enable(INTERACTIVE_LAYER);
|
|
1220
1231
|
else object.layers.disable(INTERACTIVE_LAYER);
|
|
1221
|
-
object.children.forEach((child) => setInteractive(child,
|
|
1232
|
+
object.children.forEach((child) => setInteractive(child, isInteractive2));
|
|
1222
1233
|
}
|
|
1223
1234
|
function isVisible(object) {
|
|
1224
1235
|
if (!object.visible) return false;
|
|
@@ -1462,6 +1473,7 @@ class ImageSystem extends RenderableSystem {
|
|
|
1462
1473
|
/** Textures memory limit in megabytes */
|
|
1463
1474
|
__publicField(this, "memoryLimitMb");
|
|
1464
1475
|
__publicField(this, "packer");
|
|
1476
|
+
__publicField(this, "originalAtlases", /* @__PURE__ */ new Map());
|
|
1465
1477
|
__publicField(this, "globalTranslationMatrix", new Matrix4());
|
|
1466
1478
|
__publicField(this, "originTranslationMatrix", new Matrix4());
|
|
1467
1479
|
__publicField(this, "rotationMatrix", new Matrix4());
|
|
@@ -1472,12 +1484,17 @@ class ImageSystem extends RenderableSystem {
|
|
|
1472
1484
|
const padding = 1;
|
|
1473
1485
|
this.packer = new MaxRectsPacker(atlasTextureSize, atlasTextureSize, padding, { pot: false });
|
|
1474
1486
|
}
|
|
1487
|
+
dispose() {
|
|
1488
|
+
super.dispose();
|
|
1489
|
+
this.originalAtlases.clear();
|
|
1490
|
+
}
|
|
1475
1491
|
updateLayerImpl(group, layerDef) {
|
|
1476
1492
|
super.updateLayerImpl(group, layerDef);
|
|
1477
1493
|
if (this.memoryLimitMb) this.resizeTextures();
|
|
1478
1494
|
}
|
|
1479
1495
|
buildLayer(layer) {
|
|
1480
1496
|
var _a2;
|
|
1497
|
+
const is3D = layer.mode === "3D";
|
|
1481
1498
|
const group = new Group();
|
|
1482
1499
|
const images = layer.children;
|
|
1483
1500
|
const bins = this.packImages(images);
|
|
@@ -1492,7 +1509,7 @@ class ImageSystem extends RenderableSystem {
|
|
|
1492
1509
|
).sort((a, b) => a.originalIndex - b.originalIndex);
|
|
1493
1510
|
const instanceCount = rectsWithDef.length;
|
|
1494
1511
|
const texture = createAtlas(bin);
|
|
1495
|
-
const instanceMaterial = this.materialSystem.createTextureMaterial(texture, true);
|
|
1512
|
+
const instanceMaterial = this.materialSystem.createTextureMaterial({ map: texture, uvOffset: true, is3D });
|
|
1496
1513
|
const instanceGeometry = new PlaneGeometry();
|
|
1497
1514
|
const vertexCount = instanceGeometry.attributes["position"].count;
|
|
1498
1515
|
const indexCount = ((_a2 = instanceGeometry.index) == null ? void 0 : _a2.count) ?? 0;
|
|
@@ -1507,13 +1524,15 @@ class ImageSystem extends RenderableSystem {
|
|
|
1507
1524
|
this.registerDefObject(def, batchedMesh, instanceId);
|
|
1508
1525
|
}
|
|
1509
1526
|
const nonResizable = rectsWithDef.some(({ def }) => def.source instanceof HTMLCanvasElement);
|
|
1510
|
-
|
|
1527
|
+
if (!nonResizable) this.originalAtlases.set(batchedMesh, texture.image);
|
|
1511
1528
|
group.add(batchedMesh);
|
|
1512
1529
|
}
|
|
1513
1530
|
return group;
|
|
1514
1531
|
}
|
|
1515
1532
|
/**
|
|
1516
1533
|
* Resize textures to fit the memory limit.
|
|
1534
|
+
* Uses stored original atlases so that every call computes a globally proportional
|
|
1535
|
+
* resize factor, regardless of how many layers have been loaded so far.
|
|
1517
1536
|
*/
|
|
1518
1537
|
resizeTextures() {
|
|
1519
1538
|
var _a2;
|
|
@@ -1522,53 +1541,60 @@ class ImageSystem extends RenderableSystem {
|
|
|
1522
1541
|
return;
|
|
1523
1542
|
}
|
|
1524
1543
|
logger$c.debug(`Resizing textures to fit memory limit: ${this.memoryLimitMb} MB`);
|
|
1525
|
-
const
|
|
1526
|
-
let
|
|
1544
|
+
const resizableMeshes = [];
|
|
1545
|
+
let totalOriginal = 0;
|
|
1527
1546
|
let totalNonResizable = 0;
|
|
1528
1547
|
for (const mesh of this.getAllObjects()) {
|
|
1548
|
+
const originalCanvas = this.originalAtlases.get(mesh);
|
|
1529
1549
|
const texture = mesh.material.map;
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
totalNonResizable += imageBytes;
|
|
1550
|
+
if (originalCanvas) {
|
|
1551
|
+
totalOriginal += getCanvasSizeBytes(originalCanvas, texture.generateMipmaps);
|
|
1552
|
+
resizableMeshes.push(mesh);
|
|
1534
1553
|
} else {
|
|
1535
|
-
|
|
1536
|
-
texturesToResize.push(mesh);
|
|
1554
|
+
totalNonResizable += getCanvasSizeBytes(texture.image, texture.generateMipmaps);
|
|
1537
1555
|
}
|
|
1538
1556
|
}
|
|
1557
|
+
if (resizableMeshes.length === 0) {
|
|
1558
|
+
logger$c.debug("No resizable meshes found, no need to resize textures.");
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1539
1561
|
const budget = this.memoryLimitMb * 1024 * 1024 - totalNonResizable;
|
|
1540
1562
|
if (budget < 0) {
|
|
1541
1563
|
logger$c.debug("Memory limit is too low, unable to resize textures.");
|
|
1542
1564
|
return;
|
|
1543
1565
|
}
|
|
1544
|
-
const resizeFactor = Math.sqrt(budget /
|
|
1566
|
+
const resizeFactor = Math.sqrt(budget / totalOriginal);
|
|
1545
1567
|
if (resizeFactor >= 1) {
|
|
1546
1568
|
logger$c.debug("Textures are already within the memory limit, no need to resize");
|
|
1547
1569
|
return;
|
|
1548
1570
|
}
|
|
1549
1571
|
logger$c.debug(`Resize factor: ${resizeFactor}`);
|
|
1550
1572
|
let newTotal = totalNonResizable;
|
|
1551
|
-
for (const mesh of
|
|
1573
|
+
for (const mesh of resizableMeshes) {
|
|
1552
1574
|
const material = mesh.material;
|
|
1553
|
-
const
|
|
1554
|
-
const
|
|
1555
|
-
const
|
|
1575
|
+
const currentTexture = material.map;
|
|
1576
|
+
const originalCanvas = this.originalAtlases.get(mesh);
|
|
1577
|
+
const resizedTexture = resizeCanvas(originalCanvas, resizeFactor);
|
|
1578
|
+
const originalDim = `${originalCanvas.width}x${originalCanvas.height}`;
|
|
1556
1579
|
const resizedDim = `${resizedTexture.image.width}x${resizedTexture.image.height}`;
|
|
1557
|
-
logger$c.debug(`Resized atlas for ${mesh.name || ((_a2 = mesh.parent) == null ? void 0 : _a2.name)}, from ${
|
|
1558
|
-
newTotal +=
|
|
1580
|
+
logger$c.debug(`Resized atlas for ${mesh.name || ((_a2 = mesh.parent) == null ? void 0 : _a2.name)}, from ${originalDim} to ${resizedDim}`);
|
|
1581
|
+
newTotal += getCanvasSizeBytes(resizedTexture.image, resizedTexture.generateMipmaps);
|
|
1559
1582
|
material.map = resizedTexture;
|
|
1560
1583
|
material.needsUpdate = true;
|
|
1561
|
-
|
|
1562
|
-
mesh.userData["nonResizable"] = true;
|
|
1584
|
+
currentTexture.dispose();
|
|
1563
1585
|
}
|
|
1564
1586
|
logger$c.debug(`New memory usage after resizing: ${newTotal} bytes`);
|
|
1565
1587
|
}
|
|
1588
|
+
disposeObject(object) {
|
|
1589
|
+
this.originalAtlases.delete(object);
|
|
1590
|
+
super.disposeObject(object);
|
|
1591
|
+
}
|
|
1566
1592
|
updateDefImpl(imageDef, mesh, instanceIds) {
|
|
1567
1593
|
const instanceId = instanceIds[0];
|
|
1568
1594
|
const bounds = imageDef.bounds;
|
|
1569
1595
|
const origin2 = imageDef.origin ?? [0.5, 0.5];
|
|
1570
1596
|
this.originTranslationMatrix.makeTranslation(0.5 - origin2[0], 0.5 - origin2[1], 0);
|
|
1571
|
-
this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y,
|
|
1597
|
+
this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, bounds.elevation);
|
|
1572
1598
|
this.scaleMatrix.makeScale(bounds.size.x, bounds.size.y, 1);
|
|
1573
1599
|
this.rotationMatrix.makeRotationZ(bounds.rotation);
|
|
1574
1600
|
const matrix = this.originTranslationMatrix.premultiply(this.scaleMatrix).premultiply(this.rotationMatrix).premultiply(this.globalTranslationMatrix);
|
|
@@ -1620,12 +1646,12 @@ function createAtlas(bin) {
|
|
|
1620
1646
|
logger$c.debug(`Create atlas took ${(t1 - t0).toFixed(2)} milliseconds.`);
|
|
1621
1647
|
return createTexture(canvas);
|
|
1622
1648
|
}
|
|
1623
|
-
function
|
|
1649
|
+
function resizeCanvas(source, resizeFactor) {
|
|
1624
1650
|
const canvas = document.createElement("canvas");
|
|
1625
|
-
canvas.width = Math.floor(
|
|
1626
|
-
canvas.height = Math.floor(
|
|
1651
|
+
canvas.width = Math.floor(source.width * resizeFactor);
|
|
1652
|
+
canvas.height = Math.floor(source.height * resizeFactor);
|
|
1627
1653
|
const ctx = canvas.getContext("2d");
|
|
1628
|
-
ctx.drawImage(
|
|
1654
|
+
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
1629
1655
|
return createTexture(canvas);
|
|
1630
1656
|
}
|
|
1631
1657
|
function createTexture(source) {
|
|
@@ -1636,9 +1662,8 @@ function createTexture(source) {
|
|
|
1636
1662
|
texture.needsUpdate = true;
|
|
1637
1663
|
return texture;
|
|
1638
1664
|
}
|
|
1639
|
-
function
|
|
1640
|
-
|
|
1641
|
-
return Math.ceil(imageBytes);
|
|
1665
|
+
function getCanvasSizeBytes(canvas, useMipmaps) {
|
|
1666
|
+
return Math.ceil(canvas.width * canvas.height * 4 * (useMipmaps ? 1.33 : 1));
|
|
1642
1667
|
}
|
|
1643
1668
|
const logger$b = createLogger("line");
|
|
1644
1669
|
class LineSystem extends RenderableSystem {
|
|
@@ -1719,10 +1744,13 @@ class Rect {
|
|
|
1719
1744
|
* @param min Top left corner of the rectangle.
|
|
1720
1745
|
* @param max Bottom right corner of the rectangle.
|
|
1721
1746
|
* @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
|
|
1747
|
+
* @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
|
|
1722
1748
|
*/
|
|
1723
|
-
constructor(min, max, rotation) {
|
|
1749
|
+
constructor(min, max, rotation, elevation) {
|
|
1724
1750
|
/** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
|
|
1725
1751
|
__publicField(this, "rotation");
|
|
1752
|
+
/** Optional elevation of the rectangle. Positive values raise geometry toward the viewer. Defaults to 0. */
|
|
1753
|
+
__publicField(this, "elevation");
|
|
1726
1754
|
__publicField(this, "_min");
|
|
1727
1755
|
__publicField(this, "_max");
|
|
1728
1756
|
__publicField(this, "_center", new Vector2());
|
|
@@ -1730,6 +1758,7 @@ class Rect {
|
|
|
1730
1758
|
this._min = createVector2(min);
|
|
1731
1759
|
this._max = createVector2(max);
|
|
1732
1760
|
this.rotation = rotation ?? 0;
|
|
1761
|
+
this.elevation = elevation ?? 0;
|
|
1733
1762
|
this.updateCenterAndSize();
|
|
1734
1763
|
}
|
|
1735
1764
|
/** Top left corner of the rectangle. */
|
|
@@ -1772,14 +1801,15 @@ class Rect {
|
|
|
1772
1801
|
* Creates a rectangle from an SVG rectangle element.
|
|
1773
1802
|
* @param rect {@link SVGRectElement} or {@link SVGImageElement}
|
|
1774
1803
|
* @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
|
|
1804
|
+
* @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
|
|
1775
1805
|
* @returns new {@link Rect} instance
|
|
1776
1806
|
*/
|
|
1777
|
-
static fromSvg(rect, rotation) {
|
|
1807
|
+
static fromSvg(rect, rotation, elevation) {
|
|
1778
1808
|
const x = rect.x.baseVal.value;
|
|
1779
1809
|
const y = rect.y.baseVal.value;
|
|
1780
1810
|
const width = rect.width.baseVal.value;
|
|
1781
1811
|
const height = rect.height.baseVal.value;
|
|
1782
|
-
return new Rect([x, y], [x + width, y + height], rotation);
|
|
1812
|
+
return new Rect([x, y], [x + width, y + height], rotation, elevation);
|
|
1783
1813
|
}
|
|
1784
1814
|
/**
|
|
1785
1815
|
* Moves the rectangle by the given offset.
|
|
@@ -1835,7 +1865,11 @@ class Polygon {
|
|
|
1835
1865
|
get indices() {
|
|
1836
1866
|
return this._indices;
|
|
1837
1867
|
}
|
|
1838
|
-
/**
|
|
1868
|
+
/**
|
|
1869
|
+
* Bounding rectangle of the polygon.
|
|
1870
|
+
* This is a 2D axis-aligned bounding box computed from the X/Y projection of vertices.
|
|
1871
|
+
* The Z extent of the polygon is not tracked.
|
|
1872
|
+
*/
|
|
1839
1873
|
get bounds() {
|
|
1840
1874
|
return this._bbox;
|
|
1841
1875
|
}
|
|
@@ -1845,11 +1879,12 @@ class Polygon {
|
|
|
1845
1879
|
* @returns new {@link Polygon} instance
|
|
1846
1880
|
*/
|
|
1847
1881
|
static fromRect(rect) {
|
|
1882
|
+
const z = rect.elevation;
|
|
1848
1883
|
const vertices = [
|
|
1849
|
-
{ x: rect.min.x, y: rect.min.y },
|
|
1850
|
-
{ x: rect.max.x, y: rect.min.y },
|
|
1851
|
-
{ x: rect.max.x, y: rect.max.y },
|
|
1852
|
-
{ x: rect.min.x, y: rect.max.y }
|
|
1884
|
+
{ x: rect.min.x, y: rect.min.y, z },
|
|
1885
|
+
{ x: rect.max.x, y: rect.min.y, z },
|
|
1886
|
+
{ x: rect.max.x, y: rect.max.y, z },
|
|
1887
|
+
{ x: rect.min.x, y: rect.max.y, z }
|
|
1853
1888
|
];
|
|
1854
1889
|
const indices = [
|
|
1855
1890
|
[0, 1, 2],
|
|
@@ -1901,7 +1936,8 @@ class Polygon {
|
|
|
1901
1936
|
}
|
|
1902
1937
|
/**
|
|
1903
1938
|
* Scales the polygon around the given origin.
|
|
1904
|
-
* @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
|
|
1939
|
+
* @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
|
|
1940
|
+
* vertical axes will be scaled by the same factor. Z is unchanged in both cases.
|
|
1905
1941
|
* @param origin Origin of the scaling. If omitted, defaults to the bounding rectangle center.
|
|
1906
1942
|
* @returns this {@link Polygon} instance
|
|
1907
1943
|
*/
|
|
@@ -1929,6 +1965,31 @@ class Polygon {
|
|
|
1929
1965
|
}
|
|
1930
1966
|
}
|
|
1931
1967
|
const tempVector2 = new Vector2();
|
|
1968
|
+
function groupBy(list, keyGetter) {
|
|
1969
|
+
const map = /* @__PURE__ */ new Map();
|
|
1970
|
+
list.forEach((item) => {
|
|
1971
|
+
const key = keyGetter(item);
|
|
1972
|
+
const collection = map.get(key);
|
|
1973
|
+
if (!collection) {
|
|
1974
|
+
map.set(key, [item]);
|
|
1975
|
+
} else {
|
|
1976
|
+
collection.push(item);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
return map;
|
|
1980
|
+
}
|
|
1981
|
+
function partition(list, pred) {
|
|
1982
|
+
const truthy = [];
|
|
1983
|
+
const falsy = [];
|
|
1984
|
+
for (const item of list) {
|
|
1985
|
+
if (pred(item)) {
|
|
1986
|
+
truthy.push(item);
|
|
1987
|
+
} else {
|
|
1988
|
+
falsy.push(item);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return [truthy, falsy];
|
|
1992
|
+
}
|
|
1932
1993
|
function countGeometry(geometry) {
|
|
1933
1994
|
var _a2;
|
|
1934
1995
|
if (geometry.index == null) return { vertices: geometry.getAttribute("position").count, indices: 0 };
|
|
@@ -1937,6 +1998,18 @@ function countGeometry(geometry) {
|
|
|
1937
1998
|
indices: ((_a2 = geometry.index) == null ? void 0 : _a2.count) ?? 0
|
|
1938
1999
|
};
|
|
1939
2000
|
}
|
|
2001
|
+
const sphereCenter = new Vector3();
|
|
2002
|
+
function computeBoundingSphere(bounds, origin2, out = new Sphere()) {
|
|
2003
|
+
const ox = (origin2 == null ? void 0 : origin2[0]) ?? 0.5;
|
|
2004
|
+
const oy = (origin2 == null ? void 0 : origin2[1]) ?? 0.5;
|
|
2005
|
+
const w = bounds.size.x;
|
|
2006
|
+
const h = bounds.size.y;
|
|
2007
|
+
const dx = Math.max(ox, 1 - ox) * w;
|
|
2008
|
+
const dy = Math.max(oy, 1 - oy) * h;
|
|
2009
|
+
sphereCenter.set(bounds.center.x, bounds.center.y, bounds.elevation);
|
|
2010
|
+
out.set(sphereCenter, Math.hypot(dx, dy));
|
|
2011
|
+
return out;
|
|
2012
|
+
}
|
|
1940
2013
|
const logger$a = createLogger("mesh");
|
|
1941
2014
|
extend([namesPlugin]);
|
|
1942
2015
|
class MeshSystem extends RenderableSystem {
|
|
@@ -1958,6 +2031,7 @@ class MeshSystem extends RenderableSystem {
|
|
|
1958
2031
|
this.rectGeometry.deleteAttribute("uv");
|
|
1959
2032
|
}
|
|
1960
2033
|
buildLayer(layer) {
|
|
2034
|
+
const is3D = layer.mode === "3D";
|
|
1961
2035
|
const shapes = layer.children;
|
|
1962
2036
|
const mapShapeToNormColor = /* @__PURE__ */ new Map();
|
|
1963
2037
|
for (const shapeDef of shapes) {
|
|
@@ -1974,21 +2048,15 @@ class MeshSystem extends RenderableSystem {
|
|
|
1974
2048
|
const transparentShapesGrouped = groupBy(transparentShapes, (shapeDef) => mapShapeToNormColor.get(shapeDef).a);
|
|
1975
2049
|
const group = new Group();
|
|
1976
2050
|
for (const [opacity, shapes2] of transparentShapesGrouped) {
|
|
1977
|
-
const transparentMesh = this.buildBatchedMesh(shapes2, opacity);
|
|
2051
|
+
const transparentMesh = this.buildBatchedMesh(shapes2, is3D, opacity);
|
|
1978
2052
|
transparentMesh.name = "transparent";
|
|
1979
2053
|
group.add(transparentMesh);
|
|
1980
2054
|
}
|
|
1981
2055
|
if (opaqueShapes.length) {
|
|
1982
|
-
const opaqueMesh = this.buildBatchedMesh(opaqueShapes);
|
|
2056
|
+
const opaqueMesh = this.buildBatchedMesh(opaqueShapes, is3D);
|
|
1983
2057
|
opaqueMesh.name = "opaque";
|
|
1984
2058
|
group.add(opaqueMesh);
|
|
1985
2059
|
}
|
|
1986
|
-
if (layer.mode === "3d") {
|
|
1987
|
-
group.children.filter((child) => child instanceof Mesh).forEach((mesh) => {
|
|
1988
|
-
const material = mesh.material;
|
|
1989
|
-
material.depthFunc = LessEqualDepth;
|
|
1990
|
-
});
|
|
1991
|
-
}
|
|
1992
2060
|
return group;
|
|
1993
2061
|
}
|
|
1994
2062
|
updateDefImpl(shapeDef, mesh, instanceIds, firstUpdate) {
|
|
@@ -2027,13 +2095,13 @@ class MeshSystem extends RenderableSystem {
|
|
|
2027
2095
|
mesh.setColorAt(instanceId, this.color.setRGB(color.r / 255, color.g / 255, color.b / 255, SRGBColorSpace));
|
|
2028
2096
|
}
|
|
2029
2097
|
updateRect(shape, mesh, instanceId) {
|
|
2030
|
-
this.position.set(shape.center.x, shape.center.y,
|
|
2031
|
-
this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), shape.rotation
|
|
2098
|
+
this.position.set(shape.center.x, shape.center.y, shape.elevation);
|
|
2099
|
+
this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), shape.rotation);
|
|
2032
2100
|
this.scale.set(shape.size.x, shape.size.y, 1);
|
|
2033
2101
|
this.matrix.compose(this.position, this.rotation, this.scale);
|
|
2034
2102
|
mesh.setMatrixAt(instanceId, this.matrix);
|
|
2035
2103
|
}
|
|
2036
|
-
buildBatchedMesh(shapes, opacity = 1) {
|
|
2104
|
+
buildBatchedMesh(shapes, is3D, opacity = 1) {
|
|
2037
2105
|
let vertexCount = 0;
|
|
2038
2106
|
let indexCount = 0;
|
|
2039
2107
|
let rectAdded = false;
|
|
@@ -2052,7 +2120,7 @@ class MeshSystem extends RenderableSystem {
|
|
|
2052
2120
|
vertexCount += vertices;
|
|
2053
2121
|
indexCount += indices;
|
|
2054
2122
|
}
|
|
2055
|
-
const material = this.materialSystem.createColorMaterial({ opacity });
|
|
2123
|
+
const material = this.materialSystem.createColorMaterial({ opacity, is3D });
|
|
2056
2124
|
const batchedMesh = new BatchedMesh(shapes.length, vertexCount, indexCount, material);
|
|
2057
2125
|
const rectGeometryId = rectAdded ? batchedMesh.addGeometry(this.rectGeometry) : void 0;
|
|
2058
2126
|
batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
|
|
@@ -2188,9 +2256,10 @@ class TextSystem extends RenderableSystem {
|
|
|
2188
2256
|
}
|
|
2189
2257
|
}
|
|
2190
2258
|
buildBatchedText(layer) {
|
|
2259
|
+
const is3D = layer.mode === "3D";
|
|
2191
2260
|
const textDefs = layer.children;
|
|
2192
2261
|
const batchedText = new BatchedText();
|
|
2193
|
-
batchedText.material = this.materialSystem.createColorMaterial();
|
|
2262
|
+
batchedText.material = this.materialSystem.createColorMaterial({ is3D });
|
|
2194
2263
|
const mappingData = [];
|
|
2195
2264
|
let instanceId = 0;
|
|
2196
2265
|
for (const textDef of textDefs) {
|
|
@@ -2234,7 +2303,7 @@ class TextSystem extends RenderableSystem {
|
|
|
2234
2303
|
this.worldPosition.copy(this.localPosition).rotateAround({ x: 0, y: 0 }, textDef.bounds.rotation).add(textDef.bounds.center);
|
|
2235
2304
|
this.textScale.copy(this.initialTextScale).multiplyScalar(fontSize * dpr);
|
|
2236
2305
|
text.scale.set(this.textScale.x, this.textScale.y, 1);
|
|
2237
|
-
text.position.set(this.worldPosition.x, this.worldPosition.y,
|
|
2306
|
+
text.position.set(this.worldPosition.x, this.worldPosition.y, textDef.bounds.elevation);
|
|
2238
2307
|
text.rotation.set(0, 0, textDef.bounds.rotation);
|
|
2239
2308
|
this.calculateClipRect(text, textDef, this.localPosition, this.textScale, this.localToMin, this.localToMax);
|
|
2240
2309
|
this.localPosition.y += height * dpr;
|
|
@@ -2469,22 +2538,24 @@ class LayerSystem {
|
|
|
2469
2538
|
* @returns sorted leaf layers for debug logging
|
|
2470
2539
|
*/
|
|
2471
2540
|
initRenderOrder(rootLayer) {
|
|
2472
|
-
const
|
|
2473
|
-
const
|
|
2541
|
+
const twoDLayers = [];
|
|
2542
|
+
const threeDLayers = [];
|
|
2543
|
+
const stack = [
|
|
2544
|
+
{ layer: rootLayer, inheritedMode: rootLayer.mode ?? "2D" }
|
|
2545
|
+
];
|
|
2474
2546
|
while (stack.length) {
|
|
2475
|
-
const layer = stack.pop();
|
|
2547
|
+
const { layer, inheritedMode } = stack.pop();
|
|
2476
2548
|
if (isLayerLayer(layer)) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
for (const child of children) {
|
|
2549
|
+
for (let i = layer.children.length - 1; i >= 0; i--) {
|
|
2550
|
+
const child = layer.children[i];
|
|
2480
2551
|
this.mapLayerDefToParent.set(child, layer);
|
|
2481
|
-
stack.push(child);
|
|
2552
|
+
stack.push({ layer: child, inheritedMode: child.mode ?? inheritedMode });
|
|
2482
2553
|
}
|
|
2483
2554
|
} else {
|
|
2484
|
-
|
|
2555
|
+
layer.mode ?? (layer.mode = inheritedMode);
|
|
2556
|
+
(layer.mode === "3D" ? threeDLayers : twoDLayers).push(layer);
|
|
2485
2557
|
}
|
|
2486
2558
|
}
|
|
2487
|
-
const [threeDLayers, twoDLayers] = partition(leafLayers, (layer) => layer.mode === "3d");
|
|
2488
2559
|
const sorted = [...twoDLayers, ...threeDLayers];
|
|
2489
2560
|
for (let i = 0; i < sorted.length; i++) {
|
|
2490
2561
|
this.renderOrderMap.set(sorted[i], i + 1);
|
|
@@ -2599,10 +2670,12 @@ class ExternalCameraSystem {
|
|
|
2599
2670
|
}
|
|
2600
2671
|
}
|
|
2601
2672
|
const originalProjectionMatrix = new Matrix4();
|
|
2673
|
+
const patchedMeshes = /* @__PURE__ */ new WeakSet();
|
|
2602
2674
|
function patchMatricesInExternalMode(scene) {
|
|
2603
2675
|
scene.traverse((child) => {
|
|
2604
2676
|
const mesh = child;
|
|
2605
|
-
if (!mesh.isMesh) return;
|
|
2677
|
+
if (!mesh.isMesh || patchedMeshes.has(mesh)) return;
|
|
2678
|
+
patchedMeshes.add(mesh);
|
|
2606
2679
|
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
2607
2680
|
for (const material of materials) {
|
|
2608
2681
|
const onBeforeRender = material.onBeforeRender.bind(material);
|
|
@@ -2998,12 +3071,15 @@ class CoordinatesSystem {
|
|
|
2998
3071
|
return out;
|
|
2999
3072
|
}
|
|
3000
3073
|
/**
|
|
3001
|
-
* Converts a point from
|
|
3074
|
+
* Converts a point from NDC to world space by intersecting the ground plane (z=0).
|
|
3075
|
+
* Returns 2D world coordinates (z is always 0). This is appropriate for coordinate
|
|
3076
|
+
* conversions that don't need to account for 3D geometry (e.g. canvasToSvg, camera controls).
|
|
3077
|
+
* For pointer events that should land on 3D geometry, use scene raycasting instead.
|
|
3002
3078
|
* @param ndcCoords Point in NDC (normalized device coordinates)
|
|
3003
3079
|
* @param out Optional output vector
|
|
3004
|
-
* @returns Point in world space
|
|
3080
|
+
* @returns Point on the ground plane in world space, or undefined if the ray is parallel to the plane
|
|
3005
3081
|
*/
|
|
3006
|
-
|
|
3082
|
+
ndcToWorldPlane(ndcCoords, out = new Vector3()) {
|
|
3007
3083
|
return this.pickingSystem.intersectPlane(ndcCoords, this.cameraSystem.camera, out);
|
|
3008
3084
|
}
|
|
3009
3085
|
/**
|
|
@@ -3033,7 +3109,7 @@ class CoordinatesSystem {
|
|
|
3033
3109
|
const vec2 = new Vector2();
|
|
3034
3110
|
const vec3 = new Vector3();
|
|
3035
3111
|
const ndcPoint = this.canvasToNDC(point, vec2);
|
|
3036
|
-
const worldPoint = this.
|
|
3112
|
+
const worldPoint = this.ndcToWorldPlane(ndcPoint, vec3);
|
|
3037
3113
|
if (!worldPoint) return [];
|
|
3038
3114
|
const result = [];
|
|
3039
3115
|
for (const sceneState of this.sceneSystem.sceneStates) {
|
|
@@ -5652,6 +5728,9 @@ const subsetOfTHREE = {
|
|
|
5652
5728
|
};
|
|
5653
5729
|
CameraControls.install({ THREE: subsetOfTHREE });
|
|
5654
5730
|
const logger$4 = createLogger("cameraController");
|
|
5731
|
+
const ELEVATION_PRECISION = 0.1;
|
|
5732
|
+
const ELEVATION_HEADROOM_RATIO = 0.98;
|
|
5733
|
+
const DEPTH_BUFFER_LEVELS = 2 ** 24;
|
|
5655
5734
|
class CameraController extends CameraControls {
|
|
5656
5735
|
/**
|
|
5657
5736
|
* @param camera {@link PerspectiveCamera} instance
|
|
@@ -5673,6 +5752,7 @@ class CameraController extends CameraControls {
|
|
|
5673
5752
|
three: CameraController.ACTION.NONE
|
|
5674
5753
|
};
|
|
5675
5754
|
this.touchCancelListener = () => this.cancel();
|
|
5755
|
+
void this.rotatePolarTo(0, false);
|
|
5676
5756
|
}
|
|
5677
5757
|
/**
|
|
5678
5758
|
* Get bearing angle between current camera orientation and true north (in radians).
|
|
@@ -5683,15 +5763,50 @@ class CameraController extends CameraControls {
|
|
|
5683
5763
|
}
|
|
5684
5764
|
update(delta) {
|
|
5685
5765
|
const needsUpdate = super.update(delta);
|
|
5686
|
-
if (needsUpdate
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5766
|
+
if (needsUpdate) {
|
|
5767
|
+
this.updateNearPlane();
|
|
5768
|
+
if (delta > 0) {
|
|
5769
|
+
const position = this.camera.position.toArray().map((value) => +value.toFixed(2));
|
|
5770
|
+
const target = this._target.toArray().map((value) => +value.toFixed(2));
|
|
5771
|
+
const { theta, phi, radius } = this._spherical;
|
|
5772
|
+
const spherical = [theta * RAD2DEG, phi * RAD2DEG, radius].map((value) => +value.toFixed(2));
|
|
5773
|
+
const clippingPlanes = [this.camera.near, this.camera.far];
|
|
5774
|
+
logger$4.debug("camera update %O", { position, target, spherical, clippingPlanes });
|
|
5775
|
+
}
|
|
5692
5776
|
}
|
|
5693
5777
|
return needsUpdate;
|
|
5694
5778
|
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Updates the near clipping plane to maintain consistent depth buffer precision
|
|
5781
|
+
* at the scene plane regardless of zoom level.
|
|
5782
|
+
*
|
|
5783
|
+
* Two constraints determine the near plane:
|
|
5784
|
+
* - **Headroom**: near = D × (1 − headroom ratio). Dominates at close/default zoom,
|
|
5785
|
+
* keeping headroom at exactly the target ratio. Precision exceeds the target.
|
|
5786
|
+
* - **Precision**: near = D² / (2²⁴ × P). Dominates at far zoom, maintaining target
|
|
5787
|
+
* precision at the cost of reduced headroom.
|
|
5788
|
+
*
|
|
5789
|
+
* The larger of the two is used (precision is never sacrificed for headroom).
|
|
5790
|
+
* The depth precision P is derived from {@link ELEVATION_PRECISION} and the current
|
|
5791
|
+
* {@link CameraControls.maxPolarAngle}, so it adapts when pitch limits change.
|
|
5792
|
+
*
|
|
5793
|
+
* Depth precision at the floor plane (z=0) is the guaranteed minimum. Content above
|
|
5794
|
+
* (closer to camera) has quadratically better precision: P × (d/D)². Content below the
|
|
5795
|
+
* floor degrades quadratically but remains within ~1% for practical depths (< 1% of D).
|
|
5796
|
+
*/
|
|
5797
|
+
updateNearPlane() {
|
|
5798
|
+
const camera = this.camera;
|
|
5799
|
+
const D = this.distance;
|
|
5800
|
+
if (D <= 0) return;
|
|
5801
|
+
const cosPitch = Math.max(Math.cos(this.maxPolarAngle), 0.01);
|
|
5802
|
+
const depthPrecision = ELEVATION_PRECISION * cosPitch;
|
|
5803
|
+
const headroomNear = D * (1 - ELEVATION_HEADROOM_RATIO);
|
|
5804
|
+
const precisionNear = D * D / (DEPTH_BUFFER_LEVELS * depthPrecision);
|
|
5805
|
+
const near = Math.max(headroomNear, precisionNear);
|
|
5806
|
+
if (camera.near === near) return;
|
|
5807
|
+
camera.near = near;
|
|
5808
|
+
camera.updateProjectionMatrix();
|
|
5809
|
+
}
|
|
5695
5810
|
connect(domElement) {
|
|
5696
5811
|
super.connect(domElement);
|
|
5697
5812
|
domElement.addEventListener("touchcancel", this.touchCancelListener);
|
|
@@ -6061,7 +6176,7 @@ class RollHandler extends Handler {
|
|
|
6061
6176
|
}
|
|
6062
6177
|
setPivot(e) {
|
|
6063
6178
|
this.coordinatesSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
|
|
6064
|
-
if (!this.coordinatesSystem.
|
|
6179
|
+
if (!this.coordinatesSystem.ndcToWorldPlane(this.pivotNDC, this.pivotWorld)) return false;
|
|
6065
6180
|
this.controller.getPosition(this.cameraPosition);
|
|
6066
6181
|
this.controller.getTarget(this.targetWorld);
|
|
6067
6182
|
this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
|
|
@@ -6147,7 +6262,7 @@ class InteractionsSystem {
|
|
|
6147
6262
|
if (internalCameraSystem) {
|
|
6148
6263
|
this.eventManager = new EventManager(this.canvas, {
|
|
6149
6264
|
recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
|
|
6150
|
-
//
|
|
6265
|
+
// TODO: Double click to zoom
|
|
6151
6266
|
});
|
|
6152
6267
|
this.handlers = {
|
|
6153
6268
|
pan: new PanHandler(this.canvas, this.eventManager, internalCameraSystem, coordinatesSystem),
|
|
@@ -6175,8 +6290,6 @@ class InteractionsSystem {
|
|
|
6175
6290
|
});
|
|
6176
6291
|
this.handlers.pan.enable();
|
|
6177
6292
|
this.handlers.zoom.enable();
|
|
6178
|
-
this.handlers.roll.enable();
|
|
6179
|
-
this.handlers.pitch.enable();
|
|
6180
6293
|
}
|
|
6181
6294
|
}
|
|
6182
6295
|
/**
|
|
@@ -6238,15 +6351,18 @@ class InteractionsSystem {
|
|
|
6238
6351
|
if (isDragging || !hasListeners) return;
|
|
6239
6352
|
const mousePointer = eventToCanvas(event);
|
|
6240
6353
|
coordinatesSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
|
|
6241
|
-
|
|
6242
|
-
const intersectionsByScene = this.viewportAccess.getIntersectedObjectsByScene(this.mousePointerNDC);
|
|
6354
|
+
const raycastResults = this.viewportAccess.raycastByScene(this.mousePointerNDC);
|
|
6243
6355
|
const data = [];
|
|
6244
|
-
for (const { sceneId, intersections } of
|
|
6245
|
-
const
|
|
6246
|
-
const
|
|
6356
|
+
for (const { sceneId, intersections } of raycastResults) {
|
|
6357
|
+
const interactive = intersections.filter((i) => isInteractive(i.object));
|
|
6358
|
+
const closestHit = interactive[0] ?? intersections[0];
|
|
6359
|
+
const worldPoint = closestHit ? this.mousePointerWorld.copy(closestHit.point) : coordinatesSystem.ndcToWorldPlane(this.mousePointerNDC, this.mousePointerWorld);
|
|
6360
|
+
if (!worldPoint) continue;
|
|
6361
|
+
const point = coordinatesSystem.worldToModel(worldPoint, this.mousePointerModel, sceneId);
|
|
6362
|
+
const defs = this.layerSystem.getIntersectedDefs(interactive);
|
|
6247
6363
|
data.push({ point: { x: point.x, y: point.y }, defs, sceneId });
|
|
6248
6364
|
}
|
|
6249
|
-
this.events.emit(eventType, { event, data });
|
|
6365
|
+
if (data.length > 0) this.events.emit(eventType, { event, data });
|
|
6250
6366
|
};
|
|
6251
6367
|
const mousemove = (event) => sharedMouseHandler("mousemove", event);
|
|
6252
6368
|
const mouseout = (event) => sharedMouseHandler("mouseout", event);
|
|
@@ -6282,7 +6398,7 @@ class UpdatesSystem {
|
|
|
6282
6398
|
constructor(ctx, layerSystem) {
|
|
6283
6399
|
__publicField(this, "pendingDefs", /* @__PURE__ */ new Set());
|
|
6284
6400
|
__publicField(this, "culledDefs", /* @__PURE__ */ new Set());
|
|
6285
|
-
__publicField(this, "
|
|
6401
|
+
__publicField(this, "defSphere", new Sphere());
|
|
6286
6402
|
__publicField(this, "useUpdateBuffering", true);
|
|
6287
6403
|
this.ctx = ctx;
|
|
6288
6404
|
this.layerSystem = layerSystem;
|
|
@@ -6323,10 +6439,9 @@ class UpdatesSystem {
|
|
|
6323
6439
|
let culled = false;
|
|
6324
6440
|
this.pendingDefs.delete(def);
|
|
6325
6441
|
if (isTextDef(def) || isImageDef(def)) {
|
|
6326
|
-
const
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
culled = !frustum2.intersectsBox(this.defBounds);
|
|
6442
|
+
const origin2 = isImageDef(def) ? def.origin : void 0;
|
|
6443
|
+
computeBoundingSphere(def.bounds, origin2, this.defSphere);
|
|
6444
|
+
culled = !frustum2.intersectsSphere(this.defSphere);
|
|
6330
6445
|
}
|
|
6331
6446
|
if (culled) this.culledDefs.add(def);
|
|
6332
6447
|
else {
|
|
@@ -6371,8 +6486,8 @@ class InternalCameraSystem {
|
|
|
6371
6486
|
const h = ctx.getDrawingBufferSizePx()[1];
|
|
6372
6487
|
this.prevViewportHeightPx = h;
|
|
6373
6488
|
this.camera = new PerspectiveCamera(this.defaultFov);
|
|
6489
|
+
this.camera.up.set(0, 0, -1);
|
|
6374
6490
|
this.controller = new CameraController(this.camera);
|
|
6375
|
-
void this.controller.rotatePolarTo(0, false);
|
|
6376
6491
|
}
|
|
6377
6492
|
/** Current camera zoom factor. */
|
|
6378
6493
|
get zoomFactor() {
|
|
@@ -6405,12 +6520,8 @@ class InternalCameraSystem {
|
|
|
6405
6520
|
initCamera(viewbox) {
|
|
6406
6521
|
this.zoomBounds = [0.1, viewbox.size.x > 1e4 ? 100 : 35];
|
|
6407
6522
|
this.updateCamera();
|
|
6408
|
-
this.
|
|
6409
|
-
this.camera.position.set(0, 0, -this.zoomIdentityDistance);
|
|
6410
|
-
this.camera.lookAt(0, 0, 0);
|
|
6523
|
+
this.controller.update(0);
|
|
6411
6524
|
this.camera.updateMatrixWorld();
|
|
6412
|
-
this.camera.up.set(0, 0, -1);
|
|
6413
|
-
this.controller.updateCameraUp();
|
|
6414
6525
|
}
|
|
6415
6526
|
/** Updates the camera when the renderer size changes. */
|
|
6416
6527
|
updateCamera() {
|
|
@@ -6422,8 +6533,7 @@ class InternalCameraSystem {
|
|
|
6422
6533
|
const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
|
|
6423
6534
|
const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
|
|
6424
6535
|
this.camera.aspect = w / (h || 1);
|
|
6425
|
-
this.camera.
|
|
6426
|
-
this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
|
|
6536
|
+
this.camera.far = Math.max(maxDistance, 0.1) * 2;
|
|
6427
6537
|
this.camera.updateProjectionMatrix();
|
|
6428
6538
|
this.controller.minDistance = minDistance;
|
|
6429
6539
|
this.controller.maxDistance = maxDistance;
|
|
@@ -6458,21 +6568,21 @@ class InternalCameraSystem {
|
|
|
6458
6568
|
}
|
|
6459
6569
|
}
|
|
6460
6570
|
class PickingSystem {
|
|
6461
|
-
/** */
|
|
6462
6571
|
constructor() {
|
|
6463
6572
|
__publicField(this, "raycaster", new Raycaster());
|
|
6464
6573
|
__publicField(this, "ndcPoint", new Vector2());
|
|
6465
6574
|
__publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
|
|
6466
|
-
this.raycaster.layers.set(INTERACTIVE_LAYER);
|
|
6467
6575
|
}
|
|
6468
6576
|
/**
|
|
6469
|
-
*
|
|
6577
|
+
* Raycasts the scene and returns all visible intersections sorted by distance.
|
|
6578
|
+
* Does not filter by interaction layer — consumers are responsible for partitioning
|
|
6579
|
+
* interactive vs non-interactive results using {@link isInteractive}.
|
|
6470
6580
|
* @param ndcCoords raycast point in NDC (normalized device coordinates)
|
|
6471
6581
|
* @param scene {@link Scene} instance
|
|
6472
6582
|
* @param camera {@link Camera} instance
|
|
6473
|
-
* @returns Array of {@link Intersection} instances
|
|
6583
|
+
* @returns Array of {@link Intersection} instances, nearest first
|
|
6474
6584
|
*/
|
|
6475
|
-
|
|
6585
|
+
raycast(ndcCoords, scene, camera) {
|
|
6476
6586
|
this.setRaycasterFromCamera(ndcCoords, camera);
|
|
6477
6587
|
const intersections = this.raycaster.intersectObject(scene, true);
|
|
6478
6588
|
return intersections.filter((i) => isVisible(i.object));
|
|
@@ -6595,7 +6705,7 @@ class SceneSystem {
|
|
|
6595
6705
|
const scaleFactor = Math.min(visibleRectSize.width, visibleRectSize.height);
|
|
6596
6706
|
const { x: centerX, y: centerY } = viewbox.center;
|
|
6597
6707
|
this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
|
|
6598
|
-
this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
|
|
6708
|
+
this.scaleMatrix.makeScale(scaleFactor, scaleFactor, -1);
|
|
6599
6709
|
targetMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
|
|
6600
6710
|
const viewportRectCenter = this.tempVector2.copy(viewportRectPx.center);
|
|
6601
6711
|
const offset = viewportRectCenter.sub({ x: bufferW / 2, y: bufferH / 2 });
|
|
@@ -6667,15 +6777,16 @@ class ViewportSystem {
|
|
|
6667
6777
|
this.visibleRectCss = rect;
|
|
6668
6778
|
}
|
|
6669
6779
|
/**
|
|
6670
|
-
*
|
|
6780
|
+
* Raycasts all loaded scenes and returns all visible intersections per scene.
|
|
6781
|
+
* Results are not filtered by interaction layer — consumers partition as needed.
|
|
6671
6782
|
* @param ndcCoords raycast point in NDC (normalized device coordinates)
|
|
6672
|
-
* @returns Array of {@link
|
|
6783
|
+
* @returns Array of {@link SceneRaycastResult} instances
|
|
6673
6784
|
*/
|
|
6674
|
-
|
|
6785
|
+
raycastByScene(ndcCoords) {
|
|
6675
6786
|
const result = [];
|
|
6676
6787
|
for (const sceneState of this.sceneSystem.sceneStates) {
|
|
6677
6788
|
if (!sceneState.loaded) continue;
|
|
6678
|
-
const intersections = this.pickingSystem.
|
|
6789
|
+
const intersections = this.pickingSystem.raycast(ndcCoords, sceneState.scene, this.camera);
|
|
6679
6790
|
result.push({ intersections, sceneId: sceneState.id });
|
|
6680
6791
|
}
|
|
6681
6792
|
return result;
|
|
@@ -7033,7 +7144,6 @@ class Renderer {
|
|
|
7033
7144
|
} else if (sceneState.scene.children.length === 0) {
|
|
7034
7145
|
const root = this.layerSystem.buildScene(sceneState.sceneDef);
|
|
7035
7146
|
scene.add(root);
|
|
7036
|
-
if (this.isExternalMode) patchMatricesInExternalMode(scene);
|
|
7037
7147
|
}
|
|
7038
7148
|
}
|
|
7039
7149
|
const justLoaded = loadedChanged && sceneState.loaded;
|
|
@@ -7041,6 +7151,7 @@ class Renderer {
|
|
|
7041
7151
|
this.viewportSystem.updatePtScale(id);
|
|
7042
7152
|
const hasDefsUpdated = this.updatesSystem.processPendingUpdates(frustum2, 3);
|
|
7043
7153
|
const forceRedraw = hasControlsUpdated || hasDefsUpdated || justLoaded || this.isExternalMode || this.ui;
|
|
7154
|
+
if (this.isExternalMode) patchMatricesInExternalMode(scene);
|
|
7044
7155
|
if (this.needsRedraw || forceRedraw) this.renderer.render(scene, camera);
|
|
7045
7156
|
if (hasDefsUpdated || this.needsRedraw) this.viewportSystem.invalidateSceneBounds(sceneState);
|
|
7046
7157
|
}
|
|
@@ -7077,8 +7188,7 @@ class Renderer {
|
|
|
7077
7188
|
}
|
|
7078
7189
|
initScene(sceneDef, sceneId) {
|
|
7079
7190
|
const root = this.layerSystem.buildScene(sceneDef);
|
|
7080
|
-
|
|
7081
|
-
if (this.isExternalMode) patchMatricesInExternalMode(scene);
|
|
7191
|
+
this.viewportSystem.initScene(sceneDef, sceneId).add(root);
|
|
7082
7192
|
}
|
|
7083
7193
|
// https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
|
|
7084
7194
|
resizeCanvasToDisplaySize() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/renderer",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"@types/object-hash": "^3.0.6",
|
|
15
15
|
"@types/three": "^0.174.0",
|
|
16
16
|
"stats-gl": "^3.6.0",
|
|
17
|
-
"typescript": "^5.2.2",
|
|
18
|
-
"vite": "^5.2.0",
|
|
19
17
|
"vite-bundle-analyzer": "^1.3.2",
|
|
20
18
|
"vite-plugin-dts": "^4.5.4",
|
|
21
19
|
"vite-plugin-externalize-deps": "^0.9.0"
|