@preference-sl/pref-viewer 2.17.0-beta.5 → 2.17.0-beta.6
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
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Native custom elements: `<pref-viewer>`, `<pref-viewer-2d>`, `<pref-viewer-3d>`, `<pref-viewer-dialog>`, and `<pref-viewer-menu-3d>`.
|
|
8
8
|
- Babylon.js-based 3D visualization for glTF and GLB assets.
|
|
9
|
-
- Default-enabled, draggable 3D ViewCube with 26 selectable orientations: click a face, edge or corner for a smooth, model-fitted view, or drag the cube to orbit the model. It follows the camera in real time and
|
|
9
|
+
- Default-enabled, draggable 3D ViewCube with 26 selectable orientations: click a face, edge or corner for a smooth, model-fitted view, or drag the cube to orbit the model. It follows the camera in real time, including its projection: the cube is orthographic for an orthographic viewer camera and perspective, with the same vertical FOV, for a perspective one.
|
|
10
10
|
- Model-only, geometry-dependent orbital zoom protection with near-plane sampling, independent of ViewCube visibility.
|
|
11
11
|
- Viewer dialogs above the render menu and ViewCube, with background pointer-input protection.
|
|
12
12
|
- Proportional mouse-wheel zoom for unlocked orbital cameras: increments decrease as the camera approaches its target, using Babylon's native input without an additional viewer zoom increment. Wheel sensitivity is currently an internal setting, not a public attribute or option.
|
|
@@ -256,7 +256,7 @@ The inner view is the principal model view. English ViewCube face labels read `I
|
|
|
256
256
|
| Top / Superior | +Y | Looks down toward the model. |
|
|
257
257
|
| Bottom / Inferior | -Y | Looks up toward the model. |
|
|
258
258
|
|
|
259
|
-
The cube is drawn on its own small canvas, with a separate Babylon.js engine and virtual scene, in a PrefSketch-style look: six square labelled faces, thin bars on the twelve edges and small spheres on the eight corners; there are no chamfers. Every part of an edge or corner selects the same orientation. The cube is
|
|
259
|
+
The cube is drawn on its own small canvas, with a separate Babylon.js engine and virtual scene, in a PrefSketch-style look: six square labelled faces, thin bars on the twelve edges and small spheres on the eight corners; there are no chamfers. Every part of an edge or corner selects the same orientation. The cube follows the projection and FOV of the viewer camera: it is drawn with an orthographic camera when the viewer camera is orthographic, and with a perspective camera using the same vertical FOV when it is perspective (the cube keeps the same on-screen size either way, and an FOV change redraws it). Apart from that it follows only the direction of the camera, independent of model zoom. It lives in its own scene, so it adds nothing to the model scene, to model picking or to exports.
|
|
260
260
|
|
|
261
261
|
Hover paints the region under the pointer with `styles.colorPrimary` (`--pref-viewer-color-primary`). Any CSS colour syntax is accepted: hex, `rgb()`, `hsl()` or a named colour. The angularly nearest of the 26 views is shown as the current region, painted with the primary colour mixed 25% into the face fill; hover takes precedence until the pointer leaves. A primary-colour change is picked up within 250 ms, or immediately after a resize, a culture change or re-enabling the cube. The rest of the palette (face fill, borders, label text, edge bars and corner spheres) is a fixed light palette and is not configurable. There are no region tooltips.
|
|
262
262
|
|
package/package.json
CHANGED
|
@@ -851,14 +851,15 @@ export default class BabylonJSViewCubeController {
|
|
|
851
851
|
* @param {ArrayLike<number>} matrix - Camera view matrix elements.
|
|
852
852
|
* @param {number} mode - Camera projection mode.
|
|
853
853
|
* @param {boolean} rightHanded - Whether the scene is right-handed.
|
|
854
|
+
* @param {number} fov - Camera vertical field of view in radians.
|
|
854
855
|
* @returns {boolean} Whether anything differs from the previous call.
|
|
855
856
|
*/
|
|
856
|
-
#updateGeometryKey(matrix, mode, rightHanded) {
|
|
857
|
+
#updateGeometryKey(matrix, mode, rightHanded, fov) {
|
|
857
858
|
const key = this.#geometryKey;
|
|
858
|
-
if (key && key.mode === mode && key.rightHanded === rightHanded && key.matrix.every((value, i) => value === matrix[i])) {
|
|
859
|
+
if (key && key.mode === mode && key.rightHanded === rightHanded && key.fov === fov && key.matrix.every((value, i) => value === matrix[i])) {
|
|
859
860
|
return false;
|
|
860
861
|
}
|
|
861
|
-
this.#geometryKey = { matrix: Array.from(matrix), mode, rightHanded };
|
|
862
|
+
this.#geometryKey = { matrix: Array.from(matrix), mode, rightHanded, fov };
|
|
862
863
|
return true;
|
|
863
864
|
}
|
|
864
865
|
|
|
@@ -1114,7 +1115,7 @@ export default class BabylonJSViewCubeController {
|
|
|
1114
1115
|
this.#dirty = true;
|
|
1115
1116
|
}
|
|
1116
1117
|
const rightHanded = camera.getScene().useRightHandedSystem;
|
|
1117
|
-
if (this.#updateGeometryKey(camera.getViewMatrix().m, camera.mode, rightHanded)) {
|
|
1118
|
+
if (this.#updateGeometryKey(camera.getViewMatrix().m, camera.mode, rightHanded, camera.fov)) {
|
|
1118
1119
|
const pose = this.#readPose(camera);
|
|
1119
1120
|
const rotation = this.#rotationMatrix(pose.rotation);
|
|
1120
1121
|
const towardObserver = Vector3.TransformNormal(new Vector3(0, 0, rightHanded ? 1 : -1), rotation).normalize();
|
|
@@ -1124,7 +1125,7 @@ export default class BabylonJSViewCubeController {
|
|
|
1124
1125
|
const angles = anglesFromDirection(horizontal, this.#alpha);
|
|
1125
1126
|
if (angles) {
|
|
1126
1127
|
this.#alpha = angles.alpha;
|
|
1127
|
-
renderer.setPose(angles);
|
|
1128
|
+
renderer.setPose({ ...angles, mode: camera.mode, fov: camera.fov });
|
|
1128
1129
|
}
|
|
1129
1130
|
const nearest = this.#regions.reduce((best, r) => (Vector3.Dot(r.direction, towardObserver) > Vector3.Dot(best.direction, towardObserver) ? r : best));
|
|
1130
1131
|
renderer.setCurrent(nearest.id);
|
|
@@ -20,6 +20,11 @@ const BORDER_INSET = 4;
|
|
|
20
20
|
const BORDER_WIDTH = 4;
|
|
21
21
|
const ORTHO_EXTENT = 1.15;
|
|
22
22
|
const CAMERA_RADIUS = 5;
|
|
23
|
+
const DEFAULT_FOV = 0.8;
|
|
24
|
+
// Perspective depth range around the cube, which is at most ~0.87 from its centre (corner), so +-2 leaves margin.
|
|
25
|
+
const PERSPECTIVE_DEPTH_MARGIN = 2;
|
|
26
|
+
// Distance from the cube centre to a corner sphere centre (half the cube diagonal).
|
|
27
|
+
const CORNER_DISTANCE = Math.sqrt(3) / 2;
|
|
23
28
|
const TEXTURE_SIZE = 128;
|
|
24
29
|
// Hits farther than this behind the nearest one are other sides of the cube, not overlapping parts.
|
|
25
30
|
const PICK_DEPTH_TOLERANCE = 0.15;
|
|
@@ -82,8 +87,8 @@ const FACE_LABEL_KEYS = Object.freeze({ "1,0,0": "left", "-1,0,0": "right", "0,1
|
|
|
82
87
|
/**
|
|
83
88
|
* Renders the 3D view cube on its own Babylon.js engine and scene, bound to a small canvas.
|
|
84
89
|
*
|
|
85
|
-
* The scene is left-handed and Y-up like the viewer scene and its orthographic
|
|
86
|
-
* camera's alpha/beta unchanged, so no axis mirroring is needed. Every pickable mesh carries
|
|
90
|
+
* The scene is left-handed and Y-up like the viewer scene and its ArcRotateCamera (orthographic or perspective, following
|
|
91
|
+
* the host camera) uses the host camera's alpha/beta unchanged, so no axis mirroring is needed. Every pickable mesh carries
|
|
87
92
|
* `metadata.regionId` equal to a VIEW_CUBE_DIRECTIONS id. The class owns no render loop and no
|
|
88
93
|
* interaction logic: the host calls render() and decides what pick() results mean. It touches neither
|
|
89
94
|
* `window` nor `document` itself, so it can be imported in any environment.
|
|
@@ -93,6 +98,8 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
93
98
|
#engine;
|
|
94
99
|
#scene;
|
|
95
100
|
#camera;
|
|
101
|
+
#mode = Camera.ORTHOGRAPHIC_CAMERA;
|
|
102
|
+
#fov = DEFAULT_FOV;
|
|
96
103
|
#colors;
|
|
97
104
|
#labels;
|
|
98
105
|
#devicePixelRatio;
|
|
@@ -159,23 +166,33 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
/**
|
|
162
|
-
* Orients the cube like the host camera.
|
|
163
|
-
*
|
|
169
|
+
* Orients the cube like the host camera and follows its projection. An orthographic host gives an orthographic
|
|
170
|
+
* cube (radius 5, extents +-1.15). A perspective host gives a perspective cube with the host's vertical fov at
|
|
171
|
+
* radius 1.15 / tan(fov / 2), so the cube keeps the same on-screen size.
|
|
172
|
+
* @param {{alpha: number, beta: number, mode?: number, fov?: number}} pose - Host camera angles in radians (beta is clamped away from the poles), projection mode (Camera.ORTHOGRAPHIC_CAMERA or Camera.PERSPECTIVE_CAMERA; absent keeps the last one) and vertical field of view in radians (absent or not a finite positive number keeps the last one, 0.8 initially).
|
|
164
173
|
* @returns {void}
|
|
165
174
|
*/
|
|
166
|
-
setPose({ alpha, beta }) {
|
|
175
|
+
setPose({ alpha, beta, mode, fov }) {
|
|
167
176
|
if (this.#disposed) {
|
|
168
177
|
return;
|
|
169
178
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
const camera = this.#camera;
|
|
180
|
+
if (mode === Camera.ORTHOGRAPHIC_CAMERA || mode === Camera.PERSPECTIVE_CAMERA) {
|
|
181
|
+
this.#mode = mode;
|
|
182
|
+
}
|
|
183
|
+
if (typeof fov === "number" && Number.isFinite(fov) && fov > 0) {
|
|
184
|
+
this.#fov = fov;
|
|
185
|
+
}
|
|
186
|
+
this.#applyProjection(camera);
|
|
187
|
+
camera.alpha = alpha;
|
|
188
|
+
camera.beta = clampBeta(beta);
|
|
189
|
+
camera.getViewMatrix(true);
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
/**
|
|
176
193
|
* Finds the region under a point. Coordinates are CSS pixels from the canvas top-left; they are scaled by
|
|
177
194
|
* the canvas client size (falling back to the render size when the client size is unknown) and cast as an
|
|
178
|
-
* orthographic
|
|
195
|
+
* ray (parallel for an orthographic cube, from the camera for a perspective one), so the result does not depend on hardware scaling. Corners win over edges and edges over
|
|
179
196
|
* faces when they overlap on screen. A front-facing edge or corner whose projected centre is within the pick tolerance
|
|
180
197
|
* of the pointer wins over the face under it (corners before edges, then the nearest centre). The tolerance is
|
|
181
198
|
* min(8 CSS px, 1/12 of the cube side), so small cubes keep their face centres pickable. It also rescues a ray that
|
|
@@ -200,13 +217,16 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
200
217
|
const forward = camera.target.subtract(camera.position).normalize();
|
|
201
218
|
const right = Vector3.Cross(camera.upVector, forward).normalize();
|
|
202
219
|
const up = Vector3.Cross(forward, right);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
const perspective = camera.mode === Camera.PERSPECTIVE_CAMERA;
|
|
221
|
+
// Half extents of the view plane through the cube centre: fixed for orthographic, radius * tan(fov / 2) for perspective.
|
|
222
|
+
const halfHeight = perspective ? camera.radius * Math.tan(camera.fov / 2) : (camera.orthoTop - camera.orthoBottom) / 2;
|
|
223
|
+
const halfWidth = perspective ? halfHeight * (width / height) : (camera.orthoRight - camera.orthoLeft) / 2;
|
|
224
|
+
const offset = right.scale((x / width * 2 - 1) * halfWidth).add(up.scale((1 - y / height * 2) * halfHeight));
|
|
225
|
+
const ray = perspective
|
|
226
|
+
? new Ray(camera.position, forward.scale(camera.radius).add(offset).normalize(), 100)
|
|
227
|
+
: new Ray(camera.position.add(offset), forward, 100);
|
|
208
228
|
const hits = this.#scene
|
|
209
|
-
.multiPickWithRay(
|
|
229
|
+
.multiPickWithRay(ray, (mesh) => Boolean(mesh.metadata?.regionId))
|
|
210
230
|
.filter((hit) => hit.hit)
|
|
211
231
|
.sort((a, b) => a.distance - b.distance);
|
|
212
232
|
const near = this.#pickByTolerance(x, y, width, height, forward, computePickTolerance(Math.min(width, height)));
|
|
@@ -373,6 +393,24 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
373
393
|
return best?.id ?? null;
|
|
374
394
|
}
|
|
375
395
|
|
|
396
|
+
// Orthographic: fixed radius and extents. Perspective: the host fov and the radius that frames the same half extent.
|
|
397
|
+
#applyProjection(camera) {
|
|
398
|
+
if (this.#mode === Camera.PERSPECTIVE_CAMERA) {
|
|
399
|
+
camera.mode = Camera.PERSPECTIVE_CAMERA;
|
|
400
|
+
camera.fovMode = Camera.FOVMODE_VERTICAL_FIXED;
|
|
401
|
+
camera.fov = this.#fov;
|
|
402
|
+
camera.radius = ORTHO_EXTENT / Math.tan(this.#fov / 2);
|
|
403
|
+
// A wide fov brings the camera closer than the margin: keep the near plane positive and before the nearest corner.
|
|
404
|
+
camera.minZ = Math.max(camera.radius - PERSPECTIVE_DEPTH_MARGIN, Math.min(0.1, (camera.radius - CORNER_DISTANCE) / 2));
|
|
405
|
+
camera.maxZ = camera.radius + PERSPECTIVE_DEPTH_MARGIN;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
|
409
|
+
camera.radius = CAMERA_RADIUS;
|
|
410
|
+
camera.minZ = 0.1;
|
|
411
|
+
camera.maxZ = CAMERA_RADIUS * 4;
|
|
412
|
+
}
|
|
413
|
+
|
|
376
414
|
#createCamera() {
|
|
377
415
|
const camera = new ArcRotateCamera("view-cube-camera", Math.PI / 2, Math.PI / 2, CAMERA_RADIUS, Vector3.Zero(), this.#scene);
|
|
378
416
|
camera.inputs.clear();
|
|
@@ -413,9 +451,10 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
413
451
|
const [x, y, z] = region.axes;
|
|
414
452
|
const mesh = MeshBuilder.CreatePlane(`view-cube-face-${region.id}`, { size: CUBE_HALF * 2 }, this.#scene);
|
|
415
453
|
mesh.position.set(x * CUBE_HALF, y * CUBE_HALF, z * CUBE_HALF);
|
|
416
|
-
//
|
|
454
|
+
// Babylon's plane is visible, with its texture upright and unmirrored, from the side its normal points to; aim local
|
|
455
|
+
// +Z outward so back-face culling keeps the near faces (checked on screen: aimed inward, the far faces showed through).
|
|
417
456
|
const up = y ? new Vector3(0, 0, -y) : Vector3.Up();
|
|
418
|
-
mesh.rotationQuaternion = Quaternion.FromLookDirectionLH(new Vector3(
|
|
457
|
+
mesh.rotationQuaternion = Quaternion.FromLookDirectionLH(new Vector3(x, y, z), up);
|
|
419
458
|
region.texture = new DynamicTexture(`view-cube-label-${region.id}`, this.#createLabelCanvas(), this.#scene, true);
|
|
420
459
|
region.material.diffuseTexture = region.texture;
|
|
421
460
|
region.material.emissiveColor = new Color3(EMISSIVE_SCALE, EMISSIVE_SCALE, EMISSIVE_SCALE);
|