@preference-sl/pref-viewer 2.17.1 → 2.17.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
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,34 @@ 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 0.5 + 1.15 / tan(fov / 2), so the near face and its corner knobs keep the on-screen size of the
|
|
172
|
+
* orthographic layout (the rest of the cube foreshortens behind it).
|
|
173
|
+
* @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
174
|
* @returns {void}
|
|
165
175
|
*/
|
|
166
|
-
setPose({ alpha, beta }) {
|
|
176
|
+
setPose({ alpha, beta, mode, fov }) {
|
|
167
177
|
if (this.#disposed) {
|
|
168
178
|
return;
|
|
169
179
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
const camera = this.#camera;
|
|
181
|
+
if (mode === Camera.ORTHOGRAPHIC_CAMERA || mode === Camera.PERSPECTIVE_CAMERA) {
|
|
182
|
+
this.#mode = mode;
|
|
183
|
+
}
|
|
184
|
+
if (typeof fov === "number" && Number.isFinite(fov) && fov > 0) {
|
|
185
|
+
this.#fov = fov;
|
|
186
|
+
}
|
|
187
|
+
this.#applyProjection(camera);
|
|
188
|
+
camera.alpha = alpha;
|
|
189
|
+
camera.beta = clampBeta(beta);
|
|
190
|
+
camera.getViewMatrix(true);
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
/**
|
|
176
194
|
* Finds the region under a point. Coordinates are CSS pixels from the canvas top-left; they are scaled by
|
|
177
195
|
* the canvas client size (falling back to the render size when the client size is unknown) and cast as an
|
|
178
|
-
* orthographic
|
|
196
|
+
* 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
197
|
* faces when they overlap on screen. A front-facing edge or corner whose projected centre is within the pick tolerance
|
|
180
198
|
* of the pointer wins over the face under it (corners before edges, then the nearest centre). The tolerance is
|
|
181
199
|
* 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 +218,16 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
200
218
|
const forward = camera.target.subtract(camera.position).normalize();
|
|
201
219
|
const right = Vector3.Cross(camera.upVector, forward).normalize();
|
|
202
220
|
const up = Vector3.Cross(forward, right);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
221
|
+
const perspective = camera.mode === Camera.PERSPECTIVE_CAMERA;
|
|
222
|
+
// Half extents of the view plane through the cube centre: fixed for orthographic, radius * tan(fov / 2) for perspective.
|
|
223
|
+
const halfHeight = perspective ? camera.radius * Math.tan(camera.fov / 2) : (camera.orthoTop - camera.orthoBottom) / 2;
|
|
224
|
+
const halfWidth = perspective ? halfHeight * (width / height) : (camera.orthoRight - camera.orthoLeft) / 2;
|
|
225
|
+
const offset = right.scale((x / width * 2 - 1) * halfWidth).add(up.scale((1 - y / height * 2) * halfHeight));
|
|
226
|
+
const ray = perspective
|
|
227
|
+
? new Ray(camera.position, forward.scale(camera.radius).add(offset).normalize(), 100)
|
|
228
|
+
: new Ray(camera.position.add(offset), forward, 100);
|
|
208
229
|
const hits = this.#scene
|
|
209
|
-
.multiPickWithRay(
|
|
230
|
+
.multiPickWithRay(ray, (mesh) => Boolean(mesh.metadata?.regionId))
|
|
210
231
|
.filter((hit) => hit.hit)
|
|
211
232
|
.sort((a, b) => a.distance - b.distance);
|
|
212
233
|
const near = this.#pickByTolerance(x, y, width, height, forward, computePickTolerance(Math.min(width, height)));
|
|
@@ -373,6 +394,25 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
373
394
|
return best?.id ?? null;
|
|
374
395
|
}
|
|
375
396
|
|
|
397
|
+
// Orthographic: fixed radius and extents. Perspective: the host fov and the radius that frames the same half extent
|
|
398
|
+
// at the near face plane (CUBE_HALF in front of the centre), where the corner knobs and the face in view are drawn.
|
|
399
|
+
#applyProjection(camera) {
|
|
400
|
+
if (this.#mode === Camera.PERSPECTIVE_CAMERA) {
|
|
401
|
+
camera.mode = Camera.PERSPECTIVE_CAMERA;
|
|
402
|
+
camera.fovMode = Camera.FOVMODE_VERTICAL_FIXED;
|
|
403
|
+
camera.fov = this.#fov;
|
|
404
|
+
camera.radius = CUBE_HALF + ORTHO_EXTENT / Math.tan(this.#fov / 2);
|
|
405
|
+
// A wide fov brings the camera closer than the margin: keep the near plane positive and before the nearest corner.
|
|
406
|
+
camera.minZ = Math.max(camera.radius - PERSPECTIVE_DEPTH_MARGIN, Math.min(0.1, (camera.radius - CORNER_DISTANCE) / 2));
|
|
407
|
+
camera.maxZ = camera.radius + PERSPECTIVE_DEPTH_MARGIN;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
|
411
|
+
camera.radius = CAMERA_RADIUS;
|
|
412
|
+
camera.minZ = 0.1;
|
|
413
|
+
camera.maxZ = CAMERA_RADIUS * 4;
|
|
414
|
+
}
|
|
415
|
+
|
|
376
416
|
#createCamera() {
|
|
377
417
|
const camera = new ArcRotateCamera("view-cube-camera", Math.PI / 2, Math.PI / 2, CAMERA_RADIUS, Vector3.Zero(), this.#scene);
|
|
378
418
|
camera.inputs.clear();
|
|
@@ -413,9 +453,10 @@ export default class BabylonJSViewCubeRenderer {
|
|
|
413
453
|
const [x, y, z] = region.axes;
|
|
414
454
|
const mesh = MeshBuilder.CreatePlane(`view-cube-face-${region.id}`, { size: CUBE_HALF * 2 }, this.#scene);
|
|
415
455
|
mesh.position.set(x * CUBE_HALF, y * CUBE_HALF, z * CUBE_HALF);
|
|
416
|
-
//
|
|
456
|
+
// Babylon's plane is visible, with its texture upright and unmirrored, from the side its normal points to; aim local
|
|
457
|
+
// +Z outward so back-face culling keeps the near faces (checked on screen: aimed inward, the far faces showed through).
|
|
417
458
|
const up = y ? new Vector3(0, 0, -y) : Vector3.Up();
|
|
418
|
-
mesh.rotationQuaternion = Quaternion.FromLookDirectionLH(new Vector3(
|
|
459
|
+
mesh.rotationQuaternion = Quaternion.FromLookDirectionLH(new Vector3(x, y, z), up);
|
|
419
460
|
region.texture = new DynamicTexture(`view-cube-label-${region.id}`, this.#createLabelCanvas(), this.#scene, true);
|
|
420
461
|
region.material.diffuseTexture = region.texture;
|
|
421
462
|
region.material.emissiveColor = new Color3(EMISSIVE_SCALE, EMISSIVE_SCALE, EMISSIVE_SCALE);
|