Generate, validate, and render JSCAD v2 CAD scripts from natural language prompts
94
94%
Does it follow best practices?
Impact
97%
1.64xAverage score across 3 eval scenarios
Passed
No known issues
Engineering principles for generating correct 3D models in JSCAD, adapted from GD&T (ASME Y14.5), feature-based CAD (SolidWorks/Fusion360 paradigm), and orthographic projection standards (ISO 128).
Principle: Every measurement is relative to established datum features, not arbitrary coordinates. A datum is a theoretically exact geometric reference derived from a real feature of the part.
3-2-1 rule (constraining 6 degrees of freedom):
Note: a cylindrical datum (bore/shaft) constrains 4 DOF (2 translational + 2 rotational), changing the count. ASME and ISO differ here — ASME datums are theoretical planes from real features; ISO uses "situation features".
Rules for JSCAD scripts:
main() before any geometrycenter: and translate() values must trace back to datums// ── Datums ──
// A: Z axis (axis of rotational symmetry)
// B: Z = 0 (base plane — flat bottom sits here)
// C: +Y direction (spout extends this way)Common mistake: Choosing an arbitrary axis instead of the natural datum. If PCA shows the primary axis is tilted relative to X/Y/Z, the model's coordinate system doesn't match its geometry — redesign the script to align the primary feature with a standard axis. Programmatic models often default to geometric center as origin — this is almost never where the manufacturing datum is.
Principle: A 3D model is a tree of features, each defined by a 2D sketch + operation (extrude, revolve, sweep, loft) or a modification (fillet, chamfer, boolean). The feature tree is the generative recipe.
Feature types relevant to JSCAD:
| Feature | JSCAD equivalent | When to use |
|---|---|---|
| Revolve | extrudeRotate(profile) | Rotationally symmetric body |
| Extrude | extrudeLinear({height}, profile) | Prismatic shapes |
| Sweep | hull(sphere_at_A, sphere_at_B, ...) | Organic spouts, handles |
| Fillet | expand({delta, corners: 'round'}) | Rounded edges |
| Boss | union(base, cylinder) | Mounting posts |
subtract(base, cuboid) | Slots, cavities | |
| Pattern | Loop + translate | Arrays of features |
Rules for JSCAD scripts:
const body = ..., const spout = ...union/subtractsubtract() for pockets/holes, union() for bosses/padssubtract(A, B) != subtract(B, A) — always
apply fillets after cutting pockets that intersect themCommon mistakes:
Principle: Most real objects are NOT fully symmetric. They have a symmetric body with asymmetric features (handles, knobs, mounting tabs, keyways). The modeling strategy depends on the symmetry classification.
Use analyzeSymmetry to classify:
| Symmetry result | Modeling strategy |
|---|---|
| All 3 axes symmetric | Full extrudeRotate — no clipping needed |
| 2 axes symmetric, 1 asymmetric | Revolve body, features extend along the asymmetric axis |
| 1 axis symmetric, 2 asymmetric | Revolve body + clip with half-space + union features |
| No axes symmetric | Multi-profile extrusion or primitive composition |
Pattern for partially symmetric objects:
// 1. Revolve body from symmetric axis profile
const bodyProfile = polygon({ points: profileFromDirectionalExtraction });
const bodyRevolved = extrudeRotate({ segments: 64 }, bodyProfile);
// 2. Clip if body extends beyond reference bounds on asymmetric side
const clipPlane = cuboid({ size: [big, big, big], center: [0, -big/2 - clipY, h/2] });
const body = subtract(bodyRevolved, clipPlane);
// 3. Union with detected features
const feature1 = hull(
translate([0, attachY, featureMinH], sphere({ radius: r1 })),
translate([0, tipY, featureMidH], sphere({ radius: r2 })),
);
return union(body, feature1);Key rule: Extract the body profile from the SYMMETRIC measurement axis
(extractDirectionalProfile with measureAxis = symmetric axis). This
avoids contamination from asymmetric features. Use the asymmetric axis
profile only to determine where features attach.
extentRatio from analyzeSymmetry is the clearest indicator: values
near 1.0 mean the centroid is centered (symmetric extents); values near
0.5 mean the centroid is offset (one side extends much further than the
other, usually due to features).
Principle: For rotationally symmetric parts, the entire shape is defined by a single 2D profile curve. The profile is the most information-dense representation — it encodes the body, neck, lid, and base in one array.
Profile convention:
[radius, height] from base (height=0) to topheight=0 (base)radius ≥ 0// Profile for a vase-like body
const profile = primitives.polygon({ points: [
[0, 0], // center of base
[baseR, 0], // base radius
[maxR, H * 0.35], // widest point at 35% height
[maxR * 0.6, H * 0.7],
[neckR, H], // neck
[0, H], // center of top
]});
const body = extrusions.extrudeRotate({ segments: 64 }, profile);Rules:
extractProfile() output matches your intended curvehull() of positioned spheressegments: 64 for smooth appearance, 48 for 3D printing, 32 for
preview/comparison.Common mistakes:
extrudeRotate — gives ellipsoids, not
the nuanced curves of real objectssubtract() behaves unexpectedly, try
reversing profile point order.Principle: Define geometry through ratios and constraints, not absolute dimensions. This makes models scale-invariant and easier to verify against references regardless of orientation.
Key ratios to define first:
| Ratio | Meaning | Example |
|---|---|---|
bodyH / totalH | What fraction is the body | 0.55 |
maxR / totalH | How "fat" is the body | 0.22 |
neckR / maxR | How tight is the neck | 0.23 |
spoutL / totalH | How far the spout extends | 0.35 |
handleR / maxR | Handle proportion | 0.45 |
widestAt | Height % of widest point | 0.35 |
Three tiers of parameters (Suh's Axiomatic Design):
TOTAL_H = 260)Rules:
TOTAL_H), derive everything from ratioscompare method to verify: proportionDeltas should be < 0.05TOTAL_H — all proportions follow automaticallywidth = height * 2 exists, never also write height = width * 0.5Principle: Two models of the same object may have completely different coordinate system orientations. Compare using PCA-aligned sorted axis lengths and internal proportions, never raw X/Y/Z dimensions.
Comparison metrics (from compareModels method):
| Metric | Good match | Meaning |
|---|---|---|
ratios (all 3) | All ≈ same value | Uniform scaling |
proportionDeltas.midToLong | < 0.05 | Same shape proportions |
proportionDeltas.shortToLong | < 0.05 | Same shape proportions |
profileMatch | > 0.85 | Profile curves similar |
symmetryScore | Both similar | Same symmetry character |
Use enhancedCompare which provides both PCA and AABB metrics:
| Metric | Good match | Meaning |
|---|---|---|
alignedAABB.long/mid/short.deltaPct | All < 5% | Bounding box dimensions match per PCA-mapped axis |
alignedAABB.long/mid/short.ratio | All ≈ same value | Uniform scaling |
proportionDeltas.midToLong | < 0.05 | PCA shape proportions match |
profileMatch | > 0.85 | PCA profile curves similar |
symmetryRefAxes = symmetryModelAxes | Same set | Same symmetry character |
PCA vs AABB discrepancy: PCA comparison can be misleading for asymmetric objects. PCA finds the axes of maximum variance, which tilt when features (handles, protrusions) extend asymmetrically. The aligned AABB comparison uses PCA only to MAP axes between meshes (longest↔longest, etc.), then compares the actual bounding box spans along those mapped world axes.
proportionDeltas don't → trust AABB. The PCA
axis tilt is the discrepancy, not the model.PCA gotchas:
Rules:
enhancedCompare before declaring a model "done"Principle: 6 standard views fully describe a 3D object. Each view shows two of three principal dimensions.
Third-angle projection (ISO standard, used by our slicer):
Row 0: FRONT | RIGHT | BACK
Row 1: TOP | LEFT | BOTTOMWhat each view tells you:
Rules:
Standards:
Textbooks:
Papers (mesh comparison):