partforge 0.138.2 → 0.139.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.
@@ -3013,8 +3013,9 @@ instead of (or in addition to) the built-in `#part` bar:
3013
3013
  Resolves `null` on failure rather than throwing (a build error, a part with no sub-parts
3014
3014
  in that view, a disposed runtime). The render happens in a throwaway scene, so it takes
3015
3015
  no colour from the viewer's light/dark theme. The default `style` is `"thumbnail"`, the
3016
- product shot: a fixed light background, soft lights, lighter edges, a contact shadow and
3017
- fit framing — a thumbnail is captured once and then displayed under host chrome
3016
+ product shot: a fixed light background, soft lights, lighter edges, a contact shadow,
3017
+ 4:3 framing (`size` is the width: 640 → 640×480, the shape of the cards it is shown in;
3018
+ `cad` stays square) and fit framing — a thumbnail is captured once and then displayed under host chrome
3018
3019
  partforge cannot see. `style: "cad"` gives the agent-render look instead. `style.view` is
3019
3020
  only a default angle; an explicit `angle` wins. Pass `background` (any
3020
3021
  `THREE.Color`-compatible value) to override the style's, or `background: null` for no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.138.2",
3
+ "version": "0.139.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",
@@ -45,7 +45,11 @@ const STYLES = {
45
45
  view: null,
46
46
  },
47
47
  thumbnail: {
48
- camera: { projection: "perspective", fov: 45, framing: "fit", fill: 0.88 },
48
+ // aspect (width / height) is the shape of the card the product shot is
49
+ // shown in: 4:3 across the cloud's dashboard and library, so the shape-fitted
50
+ // framing fills the card exactly instead of being cropped by object-cover.
51
+ // A style without one (cad) is square.
52
+ camera: { projection: "perspective", fov: 45, framing: "fit", fill: 0.88, aspect: 4 / 3 },
49
53
  lights: {
50
54
  hemisphere: { ...CAD_LIGHTS.hemisphere, intensity: 1.65 },
51
55
  key: { ...CAD_LIGHTS.key, intensity: 1.25 },
@@ -71,6 +75,7 @@ export function validateRenderStyle(s) {
71
75
  check(inRange(s.camera.fov, 1, 120), "camera.fov");
72
76
  check(s.camera.framing === "canonical" || s.camera.framing === "fit", "camera.framing");
73
77
  if (s.camera.framing === "fit") check(inRange(s.camera.fill, 0.05, 1), "camera.fill");
78
+ if (s.camera.aspect !== undefined) check(inRange(s.camera.aspect, 0.25, 4), "camera.aspect");
74
79
  const h = s.lights?.hemisphere;
75
80
  check(isHex(h?.sky) && isHex(h?.ground) && inRange(h?.intensity, 0, 20), "lights.hemisphere");
76
81
  for (const k of ["key", "fill"]) {
@@ -2228,7 +2228,11 @@ export function createViewer(container, part) {
2228
2228
  const worldPositions = payloads.map((p) => modelToWorld(p.positions));
2229
2229
  const points = new Float32Array(worldPositions.reduce((n, a) => n + a.length, 0));
2230
2230
  worldPositions.reduce((o, a) => { points.set(a, o); return o + a.length; }, 0);
2231
- const { pose, fov, sceneBounds } = stylePose(st, angle, box, { aspect: 1, points });
2231
+ // `size` is the long (width) edge; the style's aspect (the thumbnail's 4:3
2232
+ // card shape) sets the height, and the fit frames for that same aspect.
2233
+ const width = size;
2234
+ const height = Math.max(1, Math.round(size / (st.camera.aspect ?? 1)));
2235
+ const { pose, fov, sceneBounds } = stylePose(st, angle, box, { aspect: width / height, points });
2232
2236
 
2233
2237
  // Light the throwaway scene ourselves: renderOffscreen's own key/fill (and the
2234
2238
  // persistent hemisphere) live in the LIVE scene, which is never rendered here — so
@@ -2272,7 +2276,7 @@ export function createViewer(container, part) {
2272
2276
  pose,
2273
2277
  // The throwaway scene holds these meshes and nothing else — no grid, no
2274
2278
  // gizmo — so its own bounds are the whole of what the planes must hold.
2275
- { width: size, height: size, fov, quality, sceneBounds },
2279
+ { width, height, fov, quality, sceneBounds },
2276
2280
  tmpScene,
2277
2281
  );
2278
2282
  } finally {
@@ -50,11 +50,15 @@ export function renderAngle(name) {
50
50
  // part still occludes what is behind it (real transparency needs a depth sort
51
51
  // this renderer does not do; stills only need to read as faded).
52
52
  export async function renderViewImages(kernel, part, view = Object.keys(part.views)[0], {
53
- views = ["iso", "front", "top"], size = [800, 600], edges = true, params = {}, opacity = {},
53
+ views = ["iso", "front", "top"], size, edges = true, params = {}, opacity = {},
54
54
  style = "cad", supersample = 2,
55
55
  } = {}) {
56
56
  const { PNG } = await import("pngjs");
57
- getRenderStyle(style); // a bad style fails here, before the kernel build
57
+ const resolved = getRenderStyle(style); // a bad style fails here, before the kernel build
58
+ // No size given: a style with its own aspect (the thumbnail's 4:3) sets the
59
+ // shape — 640 wide, the viewer's thumbnail size — else the 800×600 default.
60
+ // An explicit size is the caller's choice and is honoured as given.
61
+ if (!size) size = resolved.camera.aspect ? [640, Math.round(640 / resolved.camera.aspect)] : [800, 600];
58
62
  for (const angle of views) {
59
63
  if (!renderAngle(angle)) throw new Error(`unknown angle "${angle}" (use: ${RENDER_VIEWS.join(", ")}, or a view-cube orientation such as top-front-left)`);
60
64
  }
package/types/index.d.ts CHANGED
@@ -420,7 +420,7 @@ export interface CaptureCurrentOptions {
420
420
  }
421
421
 
422
422
  export interface CaptureViewOptions {
423
- /** Square render resolution in px. Default 640. */
423
+ /** Render width in px (the long edge). The style's aspect sets the height: `"thumbnail"` is 4:3 (640 → 640×480), `"cad"` is square. Default 640. */
424
424
  size?: number;
425
425
  /** JPEG quality, 0..1. Default 0.8. */
426
426
  quality?: number;
@@ -678,6 +678,7 @@ export function renderViews(
678
678
  opts?: {
679
679
  views?: string[];
680
680
  out?: string;
681
+ /** [width, height] px. Default: 800×600, or 640 wide at the style's aspect when it has one (thumbnail → 640×480). */
681
682
  size?: [number, number];
682
683
  edges?: boolean;
683
684
  params?: ResolvedParams;
@@ -708,6 +709,7 @@ export function renderViewImages(
708
709
  view?: string,
709
710
  opts?: {
710
711
  views?: string[];
712
+ /** [width, height] px. Default: 800×600, or 640 wide at the style's aspect when it has one (thumbnail → 640×480). */
711
713
  size?: [number, number];
712
714
  edges?: boolean;
713
715
  params?: ResolvedParams;