partforge 0.170.0 → 0.171.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/README.md +1 -1
- package/package.json +1 -1
- package/src/framework/mount-preview.js +10 -6
- package/src/framework/preview-player.js +24 -0
- package/src/framework/viewer.js +51 -2
- package/types/index.d.ts +3 -2
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ never under `prefers-reduced-motion`), `fill: 0.8` (frame the model so its
|
|
|
199
199
|
bounding box, as seen from the opening view, spans that fraction of the preview
|
|
200
200
|
along its tighter axis — larger in a small card; with a turntable the model swept
|
|
201
201
|
a full turn is framed, so it never clips as it turns and a round part still fills, and it
|
|
202
|
-
turns about its hub; a model that is not turning has its on-screen outline centred, since 0.169.0), `camera: "default"` (drop the carried camera
|
|
202
|
+
turns about its hub; a model that is not turning has its on-screen outline centred, since 0.169.0, and a playing animation is framed over every pose it passes through, since 0.171.0, so a part that swings out never leaves the frame), `camera: "default"` (drop the carried camera
|
|
203
203
|
and projection and keep the file's own framing; a carried render mode and
|
|
204
204
|
environment still apply) and `renderMode: "cad" | "realistic"` (the opening
|
|
205
205
|
mode, over any carried one; it can change afterwards), and `autostart: false`
|
package/package.json
CHANGED
|
@@ -59,11 +59,13 @@
|
|
|
59
59
|
// about the vertical (its farthest vertex from the axis), so the turntable
|
|
60
60
|
// never clips and a round part still fills, centred on its horizontal
|
|
61
61
|
// vertex centroid (a lopsided part turns about its hub). A model that is
|
|
62
|
-
// not turning (a still, or a playing animation
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
62
|
+
// not turning (a still, or a playing animation) is fitted by its posed
|
|
63
|
+
// VERTICES and its silhouette centred on screen (0.169.0): the target moves
|
|
64
|
+
// in the screen plane so the projected outline's bounding box is centred,
|
|
65
|
+
// spanning `fill`. A playing animation is fitted over EVERY pose it passes
|
|
66
|
+
// through (0.171.0; each part's outermost points at each keyframe), so a
|
|
67
|
+
// part that swings out of its first pose's outline stays in frame — the
|
|
68
|
+
// outline of all its poses together spans `fill`.
|
|
67
69
|
// Unset, the viewer's fixed framing distance, as before.
|
|
68
70
|
// The swept framing follows the configuration, not reduced motion (0.169.0):
|
|
69
71
|
// a turntable mount is framed swept even when reduced motion keeps it still.
|
|
@@ -122,7 +124,7 @@ import { observeViewbarClear } from "./viewbar-clear.js";
|
|
|
122
124
|
import { createTooltipPresenter } from "./tooltip.js";
|
|
123
125
|
import { isFaceAlignedState } from "./projection.js";
|
|
124
126
|
import { ENVIRONMENTS } from "./materials/environments.js";
|
|
125
|
-
import { createPreviewPlayer } from "./preview-player.js";
|
|
127
|
+
import { createPreviewPlayer, animationPoses } from "./preview-player.js";
|
|
126
128
|
import { attachPreviewPlayButton } from "./preview-play-button.js";
|
|
127
129
|
|
|
128
130
|
// A value the option does not know is a host bug: say so at the call rather
|
|
@@ -292,6 +294,8 @@ export function mountPreview(container, options = {}) {
|
|
|
292
294
|
transparent: opts.transparent, contactShadow: opts.contactShadow, zoom: opts.zoom, pan: opts.pan,
|
|
293
295
|
// A turning model is framed swept a full turn, so no angle of it clips.
|
|
294
296
|
frameFill: opts.fill, frameSwept: sweeps,
|
|
297
|
+
// An animation is framed over every pose it passes through (0.171.0).
|
|
298
|
+
framePoses: plays ? animationPoses(decoded.animation) : null,
|
|
295
299
|
edgeWidth: opts.edgeWidth, edgeOpacity: opts.edgeOpacity,
|
|
296
300
|
});
|
|
297
301
|
cleanups.push(() => viewer.dispose());
|
|
@@ -95,6 +95,30 @@ export function sampleOpacity(keys, t) {
|
|
|
95
95
|
return keys[keys.length - 1][1];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// The poses an animation passes through, for framing it whole: one
|
|
99
|
+
// { name → pose } map per sampled time, each pose the matrix `apply` would hand
|
|
100
|
+
// setSubPose (keyframed transform with the rest taken out). The times are every
|
|
101
|
+
// keyframe of every track and the midpoint of each gap between them (a rotation
|
|
102
|
+
// can bulge past both its keyframes on the way), thinned evenly to at most
|
|
103
|
+
// `max`, with t = 0 and the end always kept. A baked file samples densely, so
|
|
104
|
+
// that is close to every frame it draws. Untracked sub-parts are left out of
|
|
105
|
+
// every map (they sit at rest).
|
|
106
|
+
export function animationPoses(animation, { max = 128 } = {}) {
|
|
107
|
+
const { tracks = {}, rest = {} } = animation ?? {};
|
|
108
|
+
const names = Object.keys(tracks);
|
|
109
|
+
if (!names.length) return [];
|
|
110
|
+
const restInverse = Object.fromEntries(names.map((name) => [name, invertRigid(rest[name])]));
|
|
111
|
+
const all = new Set([0]);
|
|
112
|
+
for (const name of names) for (const t of tracks[name].times) if (Number.isFinite(t)) all.add(t);
|
|
113
|
+
const keys = [...all].sort((x, y) => x - y);
|
|
114
|
+
let times = keys.flatMap((t, i) => (i === 0 ? [t] : [(keys[i - 1] + t) / 2, t]));
|
|
115
|
+
if (times.length > max) {
|
|
116
|
+
const step = (times.length - 1) / (max - 1);
|
|
117
|
+
times = Array.from({ length: max }, (_, i) => times[Math.round(i * step)]);
|
|
118
|
+
}
|
|
119
|
+
return times.map((seconds) => Object.fromEntries(names.map((name) => [name, mulMat4(sampleTrack(tracks[name], seconds), restInverse[name])])));
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
export function createPreviewPlayer(viewer, animation, { reducedMotion = false } = {}) {
|
|
99
123
|
const { duration, tracks, rest, opacity = {} } = animation;
|
|
100
124
|
const steps = animation.steps ?? [];
|
package/src/framework/viewer.js
CHANGED
|
@@ -179,9 +179,14 @@ export function captureCurrentFromScene(
|
|
|
179
179
|
// `edgeWidth` (CSS px, default 1) and `edgeOpacity` (0 < o <= 1, default 1)
|
|
180
180
|
// style the CAD feature edge lines; a small preview reads them as heavy at the
|
|
181
181
|
// default. Fades multiply into the opacity rather than replacing it.
|
|
182
|
+
// `framePoses` (with `frameFill`, not `frameSwept`) is a list of
|
|
183
|
+
// { sub-part name → pose matrix } maps — the poses an animation passes through
|
|
184
|
+
// (preview-player's animationPoses) — and the fill framing then fits the model
|
|
185
|
+
// in every one of them as well as as shown, so a part that swings out of its
|
|
186
|
+
// first pose's outline stays in frame.
|
|
182
187
|
export function createViewer(container, part, {
|
|
183
188
|
transparent = false, contactShadow = false, zoom = true, pan = true, frameFill = null, frameSwept = false,
|
|
184
|
-
edgeWidth = 1, edgeOpacity = 1,
|
|
189
|
+
edgeWidth = 1, edgeOpacity = 1, framePoses = null,
|
|
185
190
|
} = {}) {
|
|
186
191
|
const names = Object.keys(part.parts);
|
|
187
192
|
// Membership for host-supplied names. subMesh, cadMats and part.parts are
|
|
@@ -2056,11 +2061,54 @@ export function createViewer(container, part, {
|
|
|
2056
2061
|
}
|
|
2057
2062
|
if (n > 0) center.set(sx / n, sy / n, center.z);
|
|
2058
2063
|
}
|
|
2064
|
+
// A sub-part's outermost vertices along SUPPORT_DIRECTIONS (deduplicated, in
|
|
2065
|
+
// its delivered frame): the points that decide its outline from any angle, to
|
|
2066
|
+
// within a sliver. Framing the extra `framePoses` reads these, not every
|
|
2067
|
+
// vertex, so a long animation of a dense model frames in a few milliseconds.
|
|
2068
|
+
const SUPPORT_DIRECTIONS = (() => {
|
|
2069
|
+
const out = [], n = 64, golden = Math.PI * (3 - Math.sqrt(5));
|
|
2070
|
+
for (let i = 0; i < n; i++) {
|
|
2071
|
+
const y = 1 - (2 * (i + 0.5)) / n, r = Math.sqrt(1 - y * y), a = golden * i;
|
|
2072
|
+
out.push([r * Math.cos(a), y, r * Math.sin(a)]);
|
|
2073
|
+
}
|
|
2074
|
+
return out;
|
|
2075
|
+
})();
|
|
2076
|
+
const supportCache = new WeakMap();
|
|
2077
|
+
function supportPoints(geometry) {
|
|
2078
|
+
let points = supportCache.get(geometry);
|
|
2079
|
+
if (points) return points;
|
|
2080
|
+
const pos = geometry.getAttribute("position");
|
|
2081
|
+
const best = SUPPORT_DIRECTIONS.map(() => -Infinity), at = SUPPORT_DIRECTIONS.map(() => -1);
|
|
2082
|
+
for (let i = 0; i < (pos?.count ?? 0); i++) {
|
|
2083
|
+
_sweepV.fromBufferAttribute(pos, i);
|
|
2084
|
+
for (let k = 0; k < SUPPORT_DIRECTIONS.length; k++) {
|
|
2085
|
+
const [dx, dy, dz] = SUPPORT_DIRECTIONS[k];
|
|
2086
|
+
const d = _sweepV.x * dx + _sweepV.y * dy + _sweepV.z * dz;
|
|
2087
|
+
if (d > best[k]) { best[k] = d; at[k] = i; }
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
points = [...new Set(at.filter((i) => i >= 0))].map((i) => new THREE.Vector3().fromBufferAttribute(pos, i));
|
|
2091
|
+
supportCache.set(geometry, points);
|
|
2092
|
+
return points;
|
|
2093
|
+
}
|
|
2094
|
+
const _framePose = new THREE.Matrix4();
|
|
2059
2095
|
// Every posed vertex of `names` about `center`, stood up by the pivot, as
|
|
2060
|
-
// flat x/y/z arrays (world units, pivot-relative)
|
|
2096
|
+
// flat x/y/z arrays (world units, pivot-relative) — plus, with `framePoses`,
|
|
2097
|
+
// each sub-part's support points in every one of those poses.
|
|
2061
2098
|
function posedVertices(names, center) {
|
|
2062
2099
|
let n = 0;
|
|
2063
2100
|
for (const name of names) n += subCache[name]?.getAttribute("position")?.count ?? 0;
|
|
2101
|
+
const extra = [];
|
|
2102
|
+
for (const poses of framePoses ?? []) {
|
|
2103
|
+
for (const name of names) {
|
|
2104
|
+
if (!subCache[name]) continue;
|
|
2105
|
+
const pose = poses[name] ? _framePose.fromArray(poses[name]) : subMesh[name].matrix;
|
|
2106
|
+
for (const p of supportPoints(subCache[name])) {
|
|
2107
|
+
extra.push(p.clone().applyMatrix4(pose).sub(center).applyQuaternion(pivot.quaternion));
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
n += extra.length;
|
|
2064
2112
|
const px = new Float64Array(n), py = new Float64Array(n), pz = new Float64Array(n);
|
|
2065
2113
|
let k = 0;
|
|
2066
2114
|
for (const name of names) {
|
|
@@ -2072,6 +2120,7 @@ export function createViewer(container, part, {
|
|
|
2072
2120
|
px[k] = _sweepV.x; py[k] = _sweepV.y; pz[k] = _sweepV.z;
|
|
2073
2121
|
}
|
|
2074
2122
|
}
|
|
2123
|
+
for (const p of extra) { px[k] = p.x; py[k] = p.y; pz[k] = p.z; k++; }
|
|
2075
2124
|
return { px, py, pz, n };
|
|
2076
2125
|
}
|
|
2077
2126
|
|
package/types/index.d.ts
CHANGED
|
@@ -846,8 +846,9 @@ export interface MountPreviewOptions {
|
|
|
846
846
|
* clips and a round part still fills, centred on its horizontal vertex centroid (a
|
|
847
847
|
* lopsided part turns about its hub). Without a turntable (0.169.0+) the posed mesh
|
|
848
848
|
* vertices are fitted and their on-screen outline is centred (the target moves in the
|
|
849
|
-
* screen plane), spanning `fill`; a baked animation
|
|
850
|
-
* pose
|
|
849
|
+
* screen plane), spanning `fill`; a playing baked animation (0.171.0+) is fitted over
|
|
850
|
+
* every pose it passes through, so a part that swings out of its first pose's outline
|
|
851
|
+
* stays in frame and all its poses together span `fill`. Unset, the
|
|
851
852
|
* fixed framing distance. Since 0.169.0 a turntable mount is framed swept under reduced
|
|
852
853
|
* motion too, so every visitor sees the same framing.
|
|
853
854
|
*/
|