partforge 0.129.0 → 0.130.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.
@@ -353,7 +353,10 @@ meta: { title: "…", units: "mm", environment: "studio" },
353
353
  colour, flattened so dark materials stay readable. **Realistic** mode — the
354
354
  viewer's toggle — shows the full material under environment lighting, with a
355
355
  ground and soft shadow and no feature lines. The part never moves when the
356
- mode changes.
356
+ mode changes. The ground is placed under the part when a view is shown and
357
+ after each edit; while an animation plays or is scrubbed it stays where it is
358
+ (dropping lower only if the part would otherwise sink into it), so a motion
359
+ that changes the part's footprint never slides the floor around under it.
357
360
 
358
361
  **3D-print layer lines** (`pla-print`, `petg-print`) run perpendicular to the
359
362
  **export** pose's +Z — the way the part will be printed, not the way it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.129.0",
3
+ "version": "0.130.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -504,6 +504,12 @@ export function attachAnimationControls(viewer, part, {
504
504
  // write params, or it would dirty the regen loop for a display-only fade.
505
505
  // First write for this run also remembers what the user's params were, so
506
506
  // Reset can put them back.
507
+ // Anything the animation moves holds the realistic floor where it is (see
508
+ // viewer.js placeGround): a track that rebuilds geometry re-shows the
509
+ // assembly every delivery, and the floor must not chase each frame's
510
+ // bounds. Released by a user edit, reset/stop, or a view switch — NOT by
511
+ // reaching the end, which would just move the jump to the last frame.
512
+ if (Object.keys(r.values).length || Object.keys(r.opacity ?? {}).length) viewer.holdGround?.(true);
507
513
  if (Object.keys(r.values).length) {
508
514
  if (snapshot == null) snapshot = getParamValues(current.trackedKeys);
509
515
  applyValues(r.values);
@@ -539,6 +545,9 @@ export function attachAnimationControls(viewer, part, {
539
545
  }
540
546
 
541
547
  function doReset() {
548
+ // Before the snapshot is re-applied: the rebuild that restores the user's
549
+ // params is an ordinary placement, so the floor comes back to the part.
550
+ viewer.holdGround?.(false);
542
551
  playback?.reset();
543
552
  viewer.cancelCameraTween();
544
553
  viewer.clearSubPartOpacities?.();
@@ -814,6 +823,7 @@ export function attachAnimationControls(viewer, part, {
814
823
  // pause playback rather than fight over them.
815
824
  notifyUserEdit() {
816
825
  disarmAutoplay();
826
+ viewer.holdGround?.(false); // a user edit places the floor again, as without animation
817
827
  viewer.cancelCameraTween();
818
828
  playback?.userEdited();
819
829
  syncUi();
@@ -854,6 +864,7 @@ export function attachAnimationControls(viewer, part, {
854
864
  if (status !== "playing" && status !== "intro") guarded(() => playback.play());
855
865
  },
856
866
  detach() {
867
+ viewer.holdGround?.(false);
857
868
  offFrame();
858
869
  offOrbit();
859
870
  viewer.clearSubPartOpacities?.();
@@ -794,15 +794,28 @@ export function createViewer(container, part) {
794
794
  // The ground comes to the part: it sits at the bottom of what is visible and
795
795
  // is sized from it. Only on showAssembly (and on entering the mode) — an
796
796
  // animated pose change moves the shadow, never the floor.
797
+ //
798
+ // An animation that REBUILDS geometry re-shows the assembly on every
799
+ // delivery, though, and re-deriving the floor from each frame's bounds slides
800
+ // it under the part as the bounds shift (feedback #138: a lid swinging open
801
+ // dragged the floor back with it). So while the animation driver holds the
802
+ // ground, a placement reuses the last one and may only LOWER it — never slide
803
+ // it sideways or resize it, and never lift it into the part. The hold is
804
+ // viewer-wide rather than per rig, so an environment switch mid-playback
805
+ // lands its floor in the same place.
806
+ let groundHeld = false;
807
+ let lastGround = null;
808
+ function holdGround(on) { groundHeld = !!on; }
797
809
  function placeGround({ force = false } = {}) {
798
810
  if (!realisticRig) return;
799
811
  const b = getVisibleWorldBounds(); // shared Box3: read it out before anything else runs
800
812
  if (b.isEmpty()) return;
801
813
  const center = b.getCenter(new THREE.Vector3());
802
814
  const size = b.getSize(new THREE.Vector3());
803
- realisticRig.setGround({
804
- y: b.min.y, centerX: center.x, centerZ: center.z, radius: size.length() / 2, footprintMm: Math.max(size.x, size.z),
805
- });
815
+ lastGround = groundHeld && lastGround
816
+ ? { ...lastGround, y: Math.min(lastGround.y, b.min.y) }
817
+ : { y: b.min.y, centerX: center.x, centerZ: center.z, radius: size.length() / 2, footprintMm: Math.max(size.x, size.z) };
818
+ realisticRig.setGround(lastGround);
806
819
  shadowMovedAt = null; // this render is the full-resolution one
807
820
  renderShadow(undefined, { force });
808
821
  }
@@ -2658,6 +2671,7 @@ export function createViewer(container, part) {
2658
2671
  onFrame,
2659
2672
  onAssemblyChange: (cb) => { assemblyListeners.add(cb); return () => assemblyListeners.delete(cb); },
2660
2673
  tweenCameraTo,
2674
+ holdGround,
2661
2675
  cancelCameraTween,
2662
2676
  orbitBy,
2663
2677
  onCameraStart,