Skip to content

Add model statistics panel with dimensions and filament estimates#69

Open
RosalieWessels wants to merge 3 commits intoAdam-CAD:masterfrom
RosalieWessels:model-statistics
Open

Add model statistics panel with dimensions and filament estimates#69
RosalieWessels wants to merge 3 commits intoAdam-CAD:masterfrom
RosalieWessels:model-statistics

Conversation

@RosalieWessels
Copy link

@RosalieWessels RosalieWessels commented Jan 26, 2026

Summary

Adds a statistics panel to the right sidebar that displays real-time model dimensions and print cost estimates when viewing generated 3D models.
The panel updates automatically based on the currently rendered mesh and provides quick, slicer-style feedback without leaving the viewer.

Screenshot 2026-01-26 at 1 04 30 PM

Features

Dimensions

Displays X, Y, Z dimensions (mm)
Color-coded to match 3D axes:
X: red
Y: green
Z: blue

Print Estimates

  • Model volume (cm³)
  • Filament weight (grams), assuming PLA density (1.24 g/cm³)
  • Estimated print cost (USD), based on $20/kg filament cost

Implementation Details

  • Introduced DimensionsContext to share dimension and estimate data between the 3D viewer and the sidebar UI
  • Added utility functions in meshUtils.ts:
    • calculateMeshVolume() — computes mesh volume using the signed tetrahedron method
    • calculateFilamentEstimates() — derives filament weight and cost from volume
  • Integrated the statistics panel into the existing ParameterSection component
  • All calculations are performed client-side and update whenever the rendered mesh changes

More screenshots

Screenshot 2026-01-26 at 1 11 15 PM

Note

Medium Risk
Adds new client-side mesh analysis (bounding box + volume) wired through shared context; risk is moderate due to potential performance/accuracy issues on large or non-manifold meshes and new state flow between viewer and sidebar.

Overview
Adds a new model statistics readout (dimensions and basic PLA print estimates) to the right sidebar, rendered in ParameterSection when a compiled model is available.

Introduces DimensionsContext (provided by ParametricEditor) and updates OpenSCADViewer to compute bounding box dimensions, mesh volume, and derived filament/cost estimates from the compiled STL output, clearing these values when output is absent. meshUtils gains helpers for volume calculation, bounding box extraction, and filament estimate math, and OpenSCADViewer tightens error handling for the Fix with AI action and guards async parsing with a cancellation flag.

Written by Cursor Bugbot for commit 4c3d778. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Jan 26, 2026

@RosalieWessels is attempting to deploy a commit to the Adam Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 26, 2026

Greptile Overview

Greptile Summary

Adds a statistics panel to the right sidebar displaying real-time model dimensions (X/Y/Z) and filament print estimates (volume, weight, cost) when viewing generated 3D models.

Key Changes

  • Introduced DimensionsContext to share dimension and estimate data between the 3D viewer and sidebar UI
  • Added calculateMeshVolume() using signed tetrahedron method with proper indexed geometry handling
  • Added calculateFilamentEstimates() to derive filament weight and cost from volume (assumes PLA density 1.24 g/cm³, $20/kg)
  • Integrated calculations into OpenSCADViewer with cancellation handling for async operations
  • Rendered color-coded statistics panel in ParameterSection matching 3D axes (X:red, Y:green, Z:blue)

Implementation Quality

The implementation properly handles indexed vs non-indexed geometry by converting to non-indexed before volume calculation, preventing the bug mentioned in previous review threads. The cancellation flag in the async arrayBuffer() handler prevents race conditions from rapid recompiles. The context architecture is clean and follows React best practices.

Notes

  • Cost estimates assume 100% solid infill, which overestimates real print costs (most prints use 10-20% infill)
  • All calculations are client-side and update automatically when mesh changes
  • Previous review concerns about duplicate functions and indexed geometry have been addressed

Confidence Score: 4/5

  • This PR is safe to merge with low-to-moderate risk
  • The implementation is well-structured with proper geometry handling, race condition prevention, and clean context architecture. Previous review issues have been addressed. Minor concerns include cost estimate assumptions (100% infill vs real-world 10-20%) and the order of volume calculation (after centering), but neither causes functional problems.
  • No files require special attention - all implementations follow best practices

Important Files Changed

Filename Overview
src/utils/meshUtils.ts Adds volume calculation via signed tetrahedron method and filament cost estimation functions, properly handles indexed geometry
src/contexts/DimensionsContext.tsx Creates clean context for sharing dimensions and filament estimates between viewer and sidebar components
src/components/viewer/OpenSCADViewer.tsx Integrates dimension/volume calculations into STL processing pipeline with cancellation handling
src/components/parameter/ParameterSection.tsx Renders statistics panel displaying dimensions and print estimates in color-coded, slicer-style format

Sequence Diagram

sequenceDiagram
    participant User
    participant ParametricEditor
    participant OpenSCADViewer
    participant STLLoader
    participant meshUtils
    participant DimensionsContext
    participant ParameterSection

    User->>ParametricEditor: Views 3D model page
    ParametricEditor->>DimensionsContext: Wraps components with DimensionsProvider
    
    Note over OpenSCADViewer: When scadCode changes
    OpenSCADViewer->>OpenSCADViewer: compileScad(scadCode)
    OpenSCADViewer->>OpenSCADViewer: Receives compiled Blob output
    
    OpenSCADViewer->>STLLoader: output.arrayBuffer().then(parse)
    STLLoader-->>OpenSCADViewer: Returns BufferGeometry
    
    OpenSCADViewer->>OpenSCADViewer: geom.computeBoundingBox()
    OpenSCADViewer->>meshUtils: calculateBoundingBox(geom.boundingBox)
    meshUtils-->>OpenSCADViewer: {x, y, z} dimensions
    OpenSCADViewer->>DimensionsContext: setDimensions({x, y, z})
    
    OpenSCADViewer->>OpenSCADViewer: geom.center() - for display
    
    OpenSCADViewer->>meshUtils: calculateMeshVolume(geom)
    Note over meshUtils: Handles indexed geometry<br/>Uses signed tetrahedron method
    meshUtils-->>OpenSCADViewer: volumeMm3
    
    OpenSCADViewer->>meshUtils: calculateFilamentEstimates(volumeMm3)
    Note over meshUtils: Converts to cm³<br/>Applies PLA density (1.24 g/cm³)<br/>Calculates cost ($20/kg)
    meshUtils-->>OpenSCADViewer: {volumeCm3, weightGrams, costUSD}
    OpenSCADViewer->>DimensionsContext: setFilamentEstimates(estimates)
    
    DimensionsContext->>ParameterSection: Context updates trigger re-render
    ParameterSection->>ParameterSection: Displays dimensions (X/Y/Z color-coded)
    ParameterSection->>ParameterSection: Displays print estimates (volume, weight, cost)
    ParameterSection-->>User: Shows statistics panel in sidebar
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +30 to +64
/**
* Calculate the volume of a mesh from its BufferGeometry
* Uses the signed tetrahedron volume method
*/
function calculateMeshVolume(geometry: BufferGeometry): number {
const position = geometry.getAttribute('position');
if (!position) return 0;

let volume = 0;
const vertices = position.array;

// For each triangle, calculate signed volume of tetrahedron with origin
for (let i = 0; i < position.count; i += 3) {
const v0x = vertices[i * 3];
const v0y = vertices[i * 3 + 1];
const v0z = vertices[i * 3 + 2];

const v1x = vertices[(i + 1) * 3];
const v1y = vertices[(i + 1) * 3 + 1];
const v1z = vertices[(i + 1) * 3 + 2];

const v2x = vertices[(i + 2) * 3];
const v2y = vertices[(i + 2) * 3 + 1];
const v2z = vertices[(i + 2) * 3 + 2];

// Signed volume of tetrahedron formed by triangle and origin
volume +=
(v0x * (v1y * v2z - v2y * v1z) -
v1x * (v0y * v2z - v2y * v0z) +
v2x * (v0y * v1z - v1y * v0z)) /
6;
}

return Math.abs(volume);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate function - move to meshUtils.ts

This calculateMeshVolume function duplicates functionality that should be centralized in src/utils/meshUtils.ts alongside calculateFilamentEstimates. This improves maintainability and allows reuse in other parts of the codebase.

Suggested change
/**
* Calculate the volume of a mesh from its BufferGeometry
* Uses the signed tetrahedron volume method
*/
function calculateMeshVolume(geometry: BufferGeometry): number {
const position = geometry.getAttribute('position');
if (!position) return 0;
let volume = 0;
const vertices = position.array;
// For each triangle, calculate signed volume of tetrahedron with origin
for (let i = 0; i < position.count; i += 3) {
const v0x = vertices[i * 3];
const v0y = vertices[i * 3 + 1];
const v0z = vertices[i * 3 + 2];
const v1x = vertices[(i + 1) * 3];
const v1y = vertices[(i + 1) * 3 + 1];
const v1z = vertices[(i + 1) * 3 + 2];
const v2x = vertices[(i + 2) * 3];
const v2y = vertices[(i + 2) * 3 + 1];
const v2z = vertices[(i + 2) * 3 + 2];
// Signed volume of tetrahedron formed by triangle and origin
volume +=
(v0x * (v1y * v2z - v2y * v1z) -
v1x * (v0y * v2z - v2y * v0z) +
v2x * (v0y * v1z - v1y * v0z)) /
6;
}
return Math.abs(volume);
}
// Remove this function and import it from meshUtils.ts instead
// Add to meshUtils.ts:
// export function calculateMeshVolume(geometry: BufferGeometry): number { ... }

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/viewer/OpenSCADViewer.tsx
Line: 30:64

Comment:
Duplicate function - move to `meshUtils.ts`

This `calculateMeshVolume` function duplicates functionality that should be centralized in `src/utils/meshUtils.ts` alongside `calculateFilamentEstimates`. This improves maintainability and allows reuse in other parts of the codebase.

```suggestion
// Remove this function and import it from meshUtils.ts instead
// Add to meshUtils.ts:
// export function calculateMeshVolume(geometry: BufferGeometry): number { ... }
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +8 to +12
export function calculateMeshVolume(geometry: THREE.BufferGeometry): number {
const position = geometry.getAttribute('position');
if (!position) return 0;

let volume = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] calculateMeshVolume likely incorrect/0 for indexed or non-triangle geometries

calculateMeshVolume iterates position in groups of 3 vertices and ignores geometry.index. For indexed BufferGeometry (common after loader/optimizations), this will compute the wrong volume. It can also produce nonsense if position.count isn’t a multiple of 3 (non-triangle draw mode).

If STLLoader returns indexed geometry in some cases, estimates will silently be wrong. Consider handling geometry.index (iterate indices) or forcing geometry = geometry.toNonIndexed() before volume calc.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/meshUtils.ts
Line: 8:12

Comment:
[P1] `calculateMeshVolume` likely incorrect/0 for indexed or non-triangle geometries

`calculateMeshVolume` iterates `position` in groups of 3 vertices and ignores `geometry.index`. For indexed `BufferGeometry` (common after loader/optimizations), this will compute the wrong volume. It can also produce nonsense if `position.count` isn’t a multiple of 3 (non-triangle draw mode).

If STLLoader returns indexed geometry in some cases, estimates will silently be wrong. Consider handling `geometry.index` (iterate indices) or forcing `geometry = geometry.toNonIndexed()` before volume calc.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 89 to 96
@@ -86,12 +94,31 @@ export function OpenSCADViewer() {
const geom = loader.parse(buffer);
geom.center();
geom.computeVertexNormals();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Missing cancellation/ordering guard for async output.arrayBuffer() path

The output.arrayBuffer().then(...) chain can race when output changes rapidly (e.g., fast recompiles): a slower previous promise can resolve after a newer compile and overwrite geometry/dimensions/estimates with stale values. Consider adding a cancellation flag or sequence id inside the effect so only the latest output updates state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/viewer/OpenSCADViewer.tsx
Line: 89:96

Comment:
[P2] Missing cancellation/ordering guard for async `output.arrayBuffer()` path

The `output.arrayBuffer().then(...)` chain can race when `output` changes rapidly (e.g., fast recompiles): a slower previous promise can resolve after a newer compile and overwrite `geometry`/dimensions/estimates with stale values. Consider adding a cancellation flag or sequence id inside the effect so only the latest `output` updates state.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +95 to +107
geom.center();
geom.computeVertexNormals();
geom.computeBoundingBox();

// Calculate and set dimensions
const box = geom.boundingBox;
if (box) {
setDimensions({
x: Math.round((box.max.x - box.min.x) * 100) / 100,
y: Math.round((box.max.y - box.min.y) * 100) / 100,
z: Math.round((box.max.z - box.min.z) * 100) / 100,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Dimensions computed after geom.center() will be centered extents, not original size

You call geom.center() before geom.computeBoundingBox() and then compute box.max - box.min. Centering translates geometry but shouldn’t change size, so this is probably fine; however it does mean the bounding box is for the centered geometry (useful) and can hide issues if later code expects pre-center bounds. If the intent is “model size regardless of centering”, computing the bounding box before centering makes the dependency clearer.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/viewer/OpenSCADViewer.tsx
Line: 95:107

Comment:
[P2] Dimensions computed after `geom.center()` will be centered extents, not original size

You call `geom.center()` before `geom.computeBoundingBox()` and then compute `box.max - box.min`. Centering translates geometry but shouldn’t change size, so this is probably fine; however it does mean the bounding box is for the centered geometry (useful) and can hide issues if later code expects pre-center bounds. If the intent is “model size regardless of centering”, computing the bounding box before centering makes the dependency clearer.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (1)

src/components/viewer/OpenSCADViewer.tsx
[P1] as cast bypasses type safety and can misfire

In FixWithAIButton, fixError?.(error as OpenSCADError) relies on error.name === 'OpenSCADError' (a mutable string) rather than an actual type guard, which defeats the repo rule against casting and can call fixError with a non-OpenSCADError object.

Also appears to violate the “avoid casting” rule.

Consider switching to a real runtime check (e.g., error instanceof OpenSCADError) or refactoring the prop types so FixWithAIButton only receives an OpenSCADError when fixError is provided.

Context Used: Rule from dashboard - Avoid using 'as' type casting in TypeScript code. Find alternative approaches for type safety instea... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/viewer/OpenSCADViewer.tsx
Line: 205:211

Comment:
[P1] `as` cast bypasses type safety and can misfire

In `FixWithAIButton`, `fixError?.(error as OpenSCADError)` relies on `error.name === 'OpenSCADError'` (a mutable string) rather than an actual type guard, which defeats the repo rule against casting and can call `fixError` with a non-`OpenSCADError` object.

Also appears to violate the “avoid casting” rule.

Consider switching to a real runtime check (e.g., `error instanceof OpenSCADError`) or refactoring the prop types so `FixWithAIButton` only receives an `OpenSCADError` when `fixError` is provided.

**Context Used:** Rule from `dashboard` - Avoid using 'as' type casting in TypeScript code. Find alternative approaches for type safety instea... ([source](https://app.greptile.com/review/custom-context?memory=096c10cb-12df-4744-903f-f7cad6ba6369))

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

geom.center();

// Calculate volume and filament estimates
const volumeMm3 = calculateMeshVolume(geom);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider calculating volume before centering for cleaner data flow

Volume is calculated after centering, which shouldn't mathematically change the result but does modify vertex positions. Computing volume before centering would avoid potential precision issues with centered coordinates.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/viewer/OpenSCADViewer.tsx
Line: 110:110

Comment:
Consider calculating volume before centering for cleaner data flow

Volume is calculated after centering, which shouldn't mathematically change the result but does modify vertex positions. Computing volume before centering would avoid potential precision issues with centered coordinates.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +66 to +68
const PLA_DENSITY_G_CM3 = 1.24;
// Average PLA filament cost: ~$20/kg = $0.02/g
const FILAMENT_COST_PER_GRAM = 0.02;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider documenting that cost assumes 100% solid infill

Real 3D printing typically uses 10-20% infill, not 100%, plus support material and ~10% waste. The solid volume calculation will overestimate costs for most prints. Adding a comment about this assumption would help future maintainers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/utils/meshUtils.ts
Line: 66:68

Comment:
Consider documenting that cost assumes 100% solid infill

Real 3D printing typically uses 10-20% infill, not 100%, plus support material and ~10% waste. The solid volume calculation will overestimate costs for most prints. Adding a comment about this assumption would help future maintainers.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +225 to +227
<p className="mt-1 text-[10px] text-adam-text-secondary/60">
PLA @ $20/kg
</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider clarifying that cost is based on solid (100% infill) model

Most prints use 10-20% infill, making actual cost much lower than shown. Something like "Based on solid model @ PLA $20/kg" would set better expectations.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/parameter/ParameterSection.tsx
Line: 225:227

Comment:
Consider clarifying that cost is based on solid (100% infill) model

Most prints use 10-20% infill, making actual cost much lower than shown. Something like "Based on solid model @ PLA $20/kg" would set better expectations.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

import type { FilamentEstimates, BoundingBox } from '@/utils/meshUtils';

export type { FilamentEstimates };
export type Dimensions = BoundingBox;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused exported types in DimensionsContext

Low Severity

FilamentEstimates and Dimensions are exported from this context file (lines 4-5) but are never imported by any other file in the codebase. The types are only used internally within DimensionsContext.tsx. These exports can be removed.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant