Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.
First time visiting? Here are some places to start:- Looking for a certain topic? Check out the categories filter or use Search (upper right).
- Need support? Ask a question to our Community Support category.
- Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
- Be respectful, on topic and if you see a problem, Flag it.
If you would like to contact our Community Manager personally, feel free to send a private message or an email.
Help Debugging Custom Sheet‑Metal Nesting Feature (Parser Errors in Mid‑File Functions)
william_donaldson210
Member Posts: 4 ✭
Hi everyone,
I’m working on a custom FeatureScript for automated sheet‑metal nesting. The goal is to:
- flatten all selected sheet‑metal parts
- auto‑rotate them based on grain direction
- place them onto a virtual sheet
- draw grain‑direction arrows
- draw bend‑direction arrows
- generate a quantity list
- and eventually support multi‑sheet nesting
I’ve broken the script into clean sections (precondition, helper functions, main loop, labeling), and most of the logic is working conceptually.
However, I’m running into a set of persistent parser errors that don’t point to the real problem:
`
Error in initializer function arguments
Missing TOP_SEMI at 'function'
No viable alternative at input 'function isGrainCritical'
Extraneous input 'or' expecting {<EOF>, annotation, export, enum, type, function, ...}
`
These errors always appear together, and they point at the next function in the file, not the actual source of the problem.
✔ What I’ve already checked
- Imports are correct (feature.fs, evaluate.fs, etc.)
- qIntersection() is using the correct two‑argument form
- catch blocks are not empty
- All braces match
- No stray commas or missing semicolons
- No vector/scalar math errors
- No missing units
- No unterminated strings
✔ What seems to be happening
The parser is choking earlier in the file, but the error messages only appear once it reaches the next function keyword. I’ve fixed one missing return already, but the same four errors persist.
❓ What I need help with
I’m looking for guidance on:
1. How to systematically locate the real syntax error that causes these cascade failures.
2. Whether there are known parser edge‑cases in FeatureScript that cause misleading errors like this.
3. Whether there’s a recommended workflow or tool inside Onshape for isolating syntax issues in large FeatureScripts.
4. If anyone is willing to glance at the structure of my helper functions and confirm that nothing subtle is breaking the parser.
I can provide the script in chunks (Onshape has a paste limit), and I’ve already broken it into clean sections.
Any help would be hugely appreciated — this feature is meant to support an aircraft sheet‑metal class, and I’m very close to getting it working.
Thanks in advance!
My Document is linked here.
My code is as follows:
______________________________________________________________________________________
FeatureScript 2892;
import(path : "onshape/std/common.fs", version : "2892.0");
import(path : "onshape/std/geometry.fs", version : "2892.0");
import(path : "onshape/std/feature.fs", version : "2892.0");/
import(path : "onshape/std/sketch.fs", version : "2892.0");
import(path : "onshape/std/evaluate.fs", version : "2892.0");
import(path : "onshape/std/properties.fs", version : "2892.0");
export enum GrainDirection
{
annotation { "Name" : "X direction" }
X,
annotation { "Name" : "Y direction" }
Y
}
annotation { "Feature Type Name" : "Sheet metal nest 360" }
export const sheetMetalNest360 = defineFeature(function(context is Context, id is Id, definition is map)
precondition
{
annotation { "Name" : "Sheet width", "Default" : 48 * inch }
definition.sheetWidth is Length;
annotation { "Name" : "Sheet height", "Default" : 96 * inch }
definition.sheetHeight is Length;
annotation { "Name" : "Global grain direction" }
definition.grainDirection is GrainDirection;
annotation { "Name" : "Allow rotation for non‑grain‑critical parts", "Default" : true }
definition.allowRotation is boolean;
annotation { "Name" : "Grain‑critical property name",
"Comment" : "If this custom property (boolean) is true on a part, it is treated as grain‑critical and only 0°/90° are allowed." }
definition.grainCriticalPropertyName is string;
annotation { "Name" : "Sheet metal parts (usually: all)", "Filter" : EntityType.BODY, "MaxNumberOfPicks" : 0 }
definition.parts is Query;
}
{
var smPartsQuery = qIntersection(
(isQueryEmpty(definition.parts) ? qEverything() : definition.parts),
qSheetMetal()
);
var smParts = evaluateQuery(context, smPartsQuery);
if (size(smParts) == 0)
{
println("No sheet metal parts found.");
return;
}
var layoutPlane = plane(vector(0 * inch, 0 * inch, 0 * inch), vector(0, 0, 1));
var grainVec =
definition.grainDirection == GrainDirection.X ?
vector(1, 0, 0) :
vector(0, 1, 0);
var labelSketchId = id + "labels";
skSketch(context, labelSketchId, {
"sketchPlane" : layoutPlane
});
var xPos = 0 * inch;
var yPos = 0 * inch;
var rowHeight = 0 * inch;
var keyToCount = {};
var keyToName = {};
function isGrainCritical(part is Query) returns boolean
{
if (definition.grainCriticalPropertyName == "")
return false;
try
{
var props = getProperty(context, {
"entity" : part,
"propertyType" : PropertyType.CUSTOM,
"propertyId" : definition.grainCriticalPropertyName
});
if (props != undefined && props.value == true)
return true;
}
catch (e)
{
return false;
}
return false;
}
function geometryKey(flat is map) returns string
{
var bbox = evBox3d(context, { "topology" : flat.faces });
var mp = evMassProperties(context, { "entities" : flat.faces });
var thickness = mp.thickness;
return bbox.minCorner ~ bbox.maxCorner ~ thickness;
}
function chooseBestOrientation(width is ValueWithUnits,
height is ValueWithUnits,
grainCritical is boolean) returns map
{
var angles = [];
if (grainCritical || !definition.allowRotation)
{
angles = [0 * degree, 90 * degree];
}
else
{
angles = [
0 * degree, 45 * degree, 90 * degree, 135 * degree,
180 * degree, 225 * degree, 270 * degree, 315 * degree
];
}
var best = {
"angle" : 0 * degree,
"w" : width,
"h" : height,
"valid" : false
};
for (var a in angles)
{
var c = cos(a);
var s = sin(a);
var wRot = abs(width * c) + abs(height * s);
var hRot = abs(width * s) + abs(height * c);
if (wRot > definition.sheetWidth || hRot > definition.sheetHeight)
continue;
if (!best["valid"] || hRot < best["h"])
{
best["angle"] = a;
best["w"] = wRot;
best["h"] = hRot;
best["valid"] = true;
}
}
if (!best["valid"])
{
best["angle"] = 0 * degree;
best["w"] = width;
best["h"] = height;
best["valid"] = true;
}
return best;
}
// MAIN LOOP — PLACE EVERY SHEET METAL PART
for (var part in smParts)
{
var flat = evSheetMetalFlatPattern(context, {
"sheetMetalPart" : part
});
var bbox = evBox3d(context, { "topology" : flat.faces });
var width = bbox.maxCorner[0] - bbox.minCorner[0];
var height = bbox.maxCorner[1] - bbox.minCorner[1];
var gKey = geometryKey(flat);
if (!keyToCount[gKey])
keyToCount[gKey] = 1;
else
keyToCount[gKey] += 1;
if (!keyToName[gKey])
{
var name = try(evOwnerFeatureName(context, { "entity" : part })) ?: ("Part " ~ gKey);
keyToName[gKey] = name;
}
var grainCritical = isGrainCritical(part);
var best = chooseBestOrientation(width, height, grainCritical);
var angle = best["angle"];
var partWidth = best["w"];
var partHeight = best["h"];
// Wrap to next row if needed
if (xPos + partWidth > definition.sheetWidth)
{
xPos = 0 * inch;
yPos += rowHeight;
rowHeight = 0 * inch;
}
if (partHeight > rowHeight)
rowHeight = partHeight;
// Base transform to position part
var baseTrans = vector(
xPos - bbox.minCorner[0],
yPos - bbox.minCorner[1],
-bbox.minCorner[2]
);
var t = transform(baseTrans);
// Apply rotation if needed
if (angle != 0 * degree)
{
var pivot = vector(xPos, yPos, 0 * inch);
var rot = rotationMatrix(angle, vector(0, 0, 1));
t = transform(pivot) * rot * transform(-pivot) * t;
}
// Create flat pattern instance
opCreateSheetMetalFlatPattern(context, id + "flat_" ~ part, {
"sheetMetalPart" : part,
"transform" : t
});
// Move bend lines if present
if (flat.bendLines != undefined)
{
opTransform(context, id + "bendLines_" ~ part, {
"bodies" : flat.bendLines,
"transform" : t
});
}
// Grain direction arrow
var centerX = xPos + partWidth / 2;
var centerY = yPos + partHeight / 2;
var arrowLen = min(partWidth, partHeight) / 4;
var arrowStart = vector(centerX - grainVec[0] * arrowLen / 2,
centerY - grainVec[1] * arrowLen / 2,
0 * inch);
var arrowEnd = vector(centerX + grainVec[0] * arrowLen / 2,
centerY + grainVec[1] * arrowLen / 2,
0 * inch);
skLineSegment(context, labelSketchId + ("grain_" ~ part), {
"start" : arrowStart,
"end" : arrowEnd
});
// Arrowhead
var perp = vector(-grainVec[1], grainVec[0], 0);
var headSize = arrowLen / 6;
var headBase = arrowEnd;
var head1 = headBase - grainVec * headSize + perp * headSize;
var head2 = headBase - grainVec * headSize - perp * headSize;
skLineSegment(context, labelSketchId + ("grainHead1_" ~ part), {
"start" : headBase,
"end" : head1
});
skLineSegment(context, labelSketchId + ("grainHead2_" ~ part), {
"start" : headBase,
"end" : head2
});
// Bend direction arrow
var bendArrowStart = vector(xPos + 0.3 * inch, yPos + 0.3 * inch, 0 * inch);
var bendArrowEnd = bendArrowStart + vector(0, 0.4 * inch, 0);
skLineSegment(context, labelSketchId + ("bendDir_" ~ part), {
"start" : bendArrowStart,
"end" : bendArrowEnd
});
var bendHeadLeft = bendArrowEnd + vector(-0.1 * inch, -0.1 * inch, 0);
var bendHeadRight = bendArrowEnd + vector(0.1 * inch, -0.1 * inch, 0);
skLineSegment(context, labelSketchId + ("bendDirHead1_" ~ part), {
"start" : bendArrowEnd,
"end" : bendHeadLeft
});
skLineSegment(context, labelSketchId + ("bendDirHead2_" ~ part), {
"start" : bendArrowEnd,
"end" : bendHeadRight
});
xPos += partWidth;
}
var labelOffsetX = definition.sheetWidth + 1 * inch;
var labelY = 0.5 * inch;
for (var gKey in keys(keyToCount))
{
var text = keyToName[gKey] ~ " x" ~ toString(keyToCount[gKey]);
skText(context, labelSketchId + ("qty_" ~ gKey), {
"text" : text,
"position" : vector(labelOffsetX, labelY, 0 * inch),
"height" : 0.2 * inch,
"construction" : false
});
labelY += 0.3 * inch;
}
skSolve(context, labelSketchId);
});
Answers
The first thing that jumps out at me is that you have helper functions defined inside of other functions, which featurescript doesn't do. That's a feature of other programming languages that isn't present in Onshape and is one of many pitfalls of featurescript to navigate when you're first getting into featurescript. (Or the first thing you need to beat out of your LLM when vibe coding featurescript)
Derek Van Allen | Engineering Consultant | MeddlerI realizerealized afterwards that I did not include my code… here is a link to the file I'm working on. And my code is below if that doesn't work:
FeatureScript 2892;
import(path : "onshape/std/common.fs", version : "2892.0");
import(path : "onshape/std/geometry.fs", version : "2892.0");
import(path : "onshape/std/feature.fs", version : "2892.0");
import(path : "onshape/std/sketch.fs", version : "2892.0");
import(path : "onshape/std/evaluate.fs", version : "2892.0");
import(path : "onshape/std/properties.fs", version : "2892.0");
export enum GrainDirection
{
annotation { "Name" : "X direction" }
X,
annotation { "Name" : "Y direction" }
Y
}
annotation { "Feature Type Name" : "Sheet metal nest 360" }
export const sheetMetalNest360 = defineFeature(function(context is Context, id is Id, definition is map)
precondition
{
annotation { "Name" : "Sheet width", "Default" : 48 * inch }
definition.sheetWidth is Length;
annotation { "Name" : "Sheet height", "Default" : 96 * inch }
definition.sheetHeight is Length;
annotation { "Name" : "Global grain direction" }
definition.grainDirection is GrainDirection;
annotation { "Name" : "Allow rotation for non‑grain‑critical parts", "Default" : true }
definition.allowRotation is boolean;
annotation { "Name" : "Grain‑critical property name",
"Comment" : "If this custom property (boolean) is true on a part, it is treated as grain‑critical and only 0°/90° are allowed." }
definition.grainCriticalPropertyName is string;
annotation { "Name" : "Sheet metal parts (usually: all)", "Filter" : EntityType.BODY, "MaxNumberOfPicks" : 0 }
definition.parts is Query;
}
{
var smPartsQuery = qIntersection(
(isQueryEmpty(definition.parts) ? qEverything() : definition.parts),
qSheetMetal()
);
var smParts = evaluateQuery(context, smPartsQuery);
if (size(smParts) == 0)
{
println("No sheet metal parts found.");
return;
}
var layoutPlane = plane(vector(0 * inch, 0 * inch, 0 * inch), vector(0, 0, 1));
var grainVec =
definition.grainDirection == GrainDirection.X ?
vector(1, 0, 0) :
vector(0, 1, 0);
var labelSketchId = id + "labels";
skSketch(context, labelSketchId, {
"sketchPlane" : layoutPlane
});
var xPos = 0 * inch;
var yPos = 0 * inch;
var rowHeight = 0 * inch;
var keyToCount = {};
var keyToName = {};
function isGrainCritical(part is Query) returns boolean
{
if (definition.grainCriticalPropertyName == "")
return false;
try
{
var props = getProperty(context, {
"entity" : part,
"propertyType" : PropertyType.CUSTOM,
"propertyId" : definition.grainCriticalPropertyName
});
if (props != undefined && props.value == true)
return true;
}
catch (e)
{
return false;
}
return false;
}
function geometryKey(flat is map) returns string
{
var bbox = evBox3d(context, { "topology" : flat.faces });
var mp = evMassProperties(context, { "entities" : flat.faces });
var thickness = mp.thickness;
return bbox.minCorner ~ bbox.maxCorner ~ thickness;
}
function chooseBestOrientation(width is ValueWithUnits,
height is ValueWithUnits,
grainCritical is boolean) returns map
{
var angles = [];
if (grainCritical || !definition.allowRotation)
{
angles = [0 * degree, 90 * degree];
}
else
{
angles = [
0 * degree, 45 * degree, 90 * degree, 135 * degree,
180 * degree, 225 * degree, 270 * degree, 315 * degree
];
}
var best = {
"angle" : 0 * degree,
"w" : width,
"h" : height,
"valid" : false
};
for (var a in angles)
{
var c = cos(a);
var s = sin(a);
var wRot = abs(width * c) + abs(height * s);
var hRot = abs(width * s) + abs(height * c);
if (wRot > definition.sheetWidth || hRot > definition.sheetHeight)
continue;
if (!best["valid"] || hRot < best["h"])
{
best["angle"] = a;
best["w"] = wRot;
best["h"] = hRot;
best["valid"] = true;
}
}
if (!best["valid"])
{
best["angle"] = 0 * degree;
best["w"] = width;
best["h"] = height;
best["valid"] = true;
}
return best;
}
// MAIN LOOP — PLACE EVERY SHEET METAL PART
for (var part in smParts)
{
var flat = evSheetMetalFlatPattern(context, {
"sheetMetalPart" : part
});
var bbox = evBox3d(context, { "topology" : flat.faces });
var width = bbox.maxCorner[0] - bbox.minCorner[0];
var height = bbox.maxCorner[1] - bbox.minCorner[1];
var gKey = geometryKey(flat);
if (!keyToCount[gKey])
keyToCount[gKey] = 1;
else
keyToCount[gKey] += 1;
if (!keyToName[gKey])
{
var name = try(evOwnerFeatureName(context, { "entity" : part })) ?: ("Part " ~ gKey);
keyToName[gKey] = name;
}
var grainCritical = isGrainCritical(part);
var best = chooseBestOrientation(width, height, grainCritical);
var angle = best["angle"];
var partWidth = best["w"];
var partHeight = best["h"];
// Wrap to next row if needed
if (xPos + partWidth > definition.sheetWidth)
{
xPos = 0 * inch;
yPos += rowHeight;
rowHeight = 0 * inch;
}
if (partHeight > rowHeight)
rowHeight = partHeight;
// Base transform to position part
var baseTrans = vector(
xPos - bbox.minCorner[0],
yPos - bbox.minCorner[1],
-bbox.minCorner[2]
);
var t = transform(baseTrans);
// Apply rotation if needed
if (angle != 0 * degree)
{
var pivot = vector(xPos, yPos, 0 * inch);
var rot = rotationMatrix(angle, vector(0, 0, 1));
t = transform(pivot) * rot * transform(-pivot) * t;
}
// Create flat pattern instance
opCreateSheetMetalFlatPattern(context, id + "flat_" ~ part, {
"sheetMetalPart" : part,
"transform" : t
});
// Move bend lines if present
if (flat.bendLines != undefined)
{
opTransform(context, id + "bendLines_" ~ part, {
"bodies" : flat.bendLines,
"transform" : t
});
}
// Grain direction arrow
var centerX = xPos + partWidth / 2;
var centerY = yPos + partHeight / 2;
var arrowLen = min(partWidth, partHeight) / 4;
var arrowStart = vector(centerX - grainVec[0] * arrowLen / 2,
centerY - grainVec[1] * arrowLen / 2,
0 * inch);
var arrowEnd = vector(centerX + grainVec[0] * arrowLen / 2,
centerY + grainVec[1] * arrowLen / 2,
0 * inch);
skLineSegment(context, labelSketchId + ("grain_" ~ part), {
"start" : arrowStart,
"end" : arrowEnd
});
// Arrowhead
var perp = vector(-grainVec[1], grainVec[0], 0);
var headSize = arrowLen / 6;
var headBase = arrowEnd;
var head1 = headBase - grainVec * headSize + perp * headSize;
var head2 = headBase - grainVec * headSize - perp * headSize;
skLineSegment(context, labelSketchId + ("grainHead1_" ~ part), {
"start" : headBase,
"end" : head1
});
skLineSegment(context, labelSketchId + ("grainHead2_" ~ part), {
"start" : headBase,
"end" : head2
});
// Bend direction arrow
var bendArrowStart = vector(xPos + 0.3 * inch, yPos + 0.3 * inch, 0 * inch);
var bendArrowEnd = bendArrowStart + vector(0, 0.4 * inch, 0);
skLineSegment(context, labelSketchId + ("bendDir_" ~ part), {
"start" : bendArrowStart,
"end" : bendArrowEnd
});
var bendHeadLeft = bendArrowEnd + vector(-0.1 * inch, -0.1 * inch, 0);
var bendHeadRight = bendArrowEnd + vector(0.1 * inch, -0.1 * inch, 0);
skLineSegment(context, labelSketchId + ("bendDirHead1_" ~ part), {
"start" : bendArrowEnd,
"end" : bendHeadLeft
});
skLineSegment(context, labelSketchId + ("bendDirHead2_" ~ part), {
"start" : bendArrowEnd,
"end" : bendHeadRight
});
xPos += partWidth;
}
var labelOffsetX = definition.sheetWidth + 1 * inch;
var labelY = 0.5 * inch;
for (var gKey in keys(keyToCount))
{
var text = keyToName[gKey] ~ " x" ~ toString(keyToCount[gKey]);
skText(context, labelSketchId + ("qty_" ~ gKey), {
"text" : text,
"position" : vector(labelOffsetX, labelY, 0 * inch),
"height" : 0.2 * inch,
"construction" : false
});
labelY += 0.3 * inch;
}
skSolve(context, labelSketchId);
});
https://cad.onshape.com/documents/69d8fdcfa3b7a1eeabf7337f/w/f34ce052f3dfc2c1efd0309b/e/96d738e69f4d10e314786d4a
the above FS is based on Arul Suresh's Auto Layout Feautrescript (Edited to work with sheet metal)
The solver is not too accurate though so you might want to edit the actual algorithm portion of the script.