Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape, CAD, maker project and design.

First time visiting? Here are some places to start:

  1. Looking for a certain topic? Check out the categories filter or use Search (upper right).
  2. Need support? Ask a question to our Community Support category.
  3. Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
  4. 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.

Auto Layout Feature

marena_richardsonmarena_richardson Member Posts: 20 EDU
Hi!

I am another member of the team of five college students, mentioned by Jacob Kingery, who got the chance to work with FeatureScript this school year, and this is one of the features we made. Let us know what you think!

Auto Layout is a Feature we built to lay out parts for laser cutting or other manufacturing processes. Auto Layout finds all parts that match a material thickness inputted by the user and lays them out on cut sheets of a specified width and height with a specified spacing between them using a binary tree bin-packing algorithm right in their original Part Studio. All the input parameters have default values that are set in the config file and that can be modified for maximum ease of use. The algorithm approximates parts as rectangles, and places them in order from largest to smallest onto a cut sheet. Each time a part is placed, the remaining space on the cut sheet is split into a portion above and a portion to the right. The algorithm then tries to place the next part on the top sub-sheet and the right sub-sheet, and the algorithm continues splitting and placing in this manner recursively. With this Feature, anyone can design something fully assembled and easily lay its parts out without needing to create additional Assemblies, Drawings, or Part Studios. Its uses extend beyond laser cutting and one of the clients we worked with used it to lay out desk parts that he was cutting out of a sheet of plywood. The Feature can be viewed here: 

https://cad.onshape.com/documents/3b3bb87c95d03259328fdb1f/w/9828ddc941ddc2896ebeebdb/e/fcecc760e1bc713ee3aae876


The Feature dialog:

new_al_dialogPNG

An example of Auto Layout being used to lay out desk parts:

ALgif


Auto Layout Code:

FeatureScript 275;
import(path : "onshape/std/geometry.fs", version : "275.0");
import(path : "b2e35aca6e9ed7a858fa702f", version : "b85c432998567d427f0c5fdb");
annotation { "Feature Type Name": "Auto Layout" }
export const autolayout = defineFeature(function(context is Context, id is Id, definition is map)
   precondition
   {
       annotation { "Name" : "Thickness of material" }
       isLength(definition.thickness, DEFAULT_THICKNESS);
       
       annotation { "Name" : "Cut sheet width" }
       isLength(definition.width, DEFAULT_SHEET_WIDTH);
       
       annotation { "Name" : "Cut sheet height" }
       isLength(definition.height, DEFAULT_SHEET_HEIGHT);
       
       annotation { "Name" : "Spacing" }
       isLength(definition.spacing, DEFAULT_SPACING);
       
       annotation { "Name" : "Show Cut Sheet Sketches" }
       definition.showSheets is boolean;
   }
   {
       //step 1 - query for all bodies
       //step 2 - find all the planar faces owned by each body
       //step 3 - sort planar faces by size
       //step 4 - use largest planar face to create a bounding box and check
       //         that the depth of the part = the thickness of the material
       //step 6 - If the part has correct thickness, lie flat on the top plane
       //step 7 - space parts out using a binary tree bin packing algorithm
       // Query for all bodies
       var bodies = evaluateQuery(context, qBodyType(qEverything(EntityType.BODY), BodyType.SOLID));
       // Initialize list of parameters necessary for bin packing
       var blocks = [];
       
       // Initialize list of bodies that are laser cuttable
       var yesParts = [];
       
       // Iterate through bodies
       for (var i = 0; i < size(bodies); i += 1)
       {
           // Query for planar faces and sort them by their surface area
           var faces = evaluateQuery(context, qGeometry(qOwnedByBody(bodies[i], EntityType.FACE), GeometryType.PLANE));
           var sorted = sort(faces, function(face1, face2) {
               return evArea(context, {"entities": face2}) - evArea(context, {"entities": face1});
           });
           // Check if part has planar faces (is planar)
           if (size(sorted) > 0) {
               var largestFacePlane = evPlane(context, {
                   "face": sorted[0]
               });
               const orientedCSys = planeToCSys(largestFacePlane);
               const boundingBox is Box3d = evBox3d(context, {
                   "topology": bodies[i],
                   "cSys": orientedCSys
               });
               const deltaX = abs(boundingBox.maxCorner[0] - boundingBox.minCorner[0]);
               const deltaY = abs(boundingBox.maxCorner[1] - boundingBox.minCorner[1]);
               const deltaZ = abs(boundingBox.maxCorner[2] - boundingBox.minCorner[2]);
               // Check that the part has a z depth equal to the thickness of the material
               if (tolerantEquals(definition.thickness, deltaZ)) {
                   blocks = append(blocks, new box({'w': deltaX, 'h': deltaY, 'owner': bodies[i], 'rotated': false}));
                   yesParts = append(yesParts, bodies[i]);
                   // Move parts to the top plane and to the origin
                   var transformFromWorld = fromWorld(orientedCSys);
                   var transformToOrigin = transform(-boundingBox.minCorner);
                   opTransform(context, id + ("transform_to_top_and_origin" ~ i), {
                       "bodies": bodies[i],
                       "transform": transformToOrigin * transformFromWorld
                   });   
               }
           }
       }
       
       // Move non laser cuttable parts out of the way
       var noParts = evaluateQuery(context, qSubtraction(qBodyType(qEverything(EntityType.BODY), BodyType.SOLID), qUnion(yesParts)));
       for (var i = 0; i < size(noParts); i += 1) {
           const boundingBox is Box3d = evBox3d(context, {
               "topology": noParts[i]
           });
           var transformToOrigin = transform(-boundingBox.maxCorner);
           var transformAway = transform(vector(-(definition.width * 0.3), 0*meter, 0*meter));
           opTransform(context, id + ("transform_to_origin2" ~ i), {
               "bodies": noParts[i],
               "transform": transformAway * transformToOrigin
           });
       }
       
       // Everything below this point is for spacing
       var sortedBlocks = sortBlocks(blocks);
       var prevBlocks = [];
       var cutSheetNumber = 0;
       blocks = [];
       while (size(sortedBlocks) > 0) {
           // Run binary tree bin-packing algorithm
           Packer(definition.width, definition.height, definition.spacing, sortedBlocks, cutSheetNumber);
           for (var i = 0; i < size(sortedBlocks); i += 1) {
               var block = sortedBlocks[i];
               // Move parts to the binary tree bin packing location
               if (block[].fit != undefined) {
                   if (block[].rotated == true) {
                       rotateBlock(context, id, i ~ cutSheetNumber, block);
                   }
                   var fitVector = vector(block[].fit[].x, block[].fit[].y, 0*inch);
                   var transformToBin = transform(fitVector);
                   opTransform(context, id + ("transform_to_bin" ~ i ~ cutSheetNumber), {
                       "bodies": block[].owner,
                       "transform": transformToBin
                   });
               } else {
                   blocks = append(blocks, block);
               }
           }
           // Sketch cut sheets if specified
           if (definition.showSheets) {
               var sketch1 = newSketch(context, id + ("sketch" ~ cutSheetNumber), {
                       "sketchPlane" : qCreatedBy(makeId("Top"), EntityType.FACE)
               });
               var newX = cutSheetNumber * definition.width * 1.1;
               skRectangle(sketch1, "rectangle" ~ cutSheetNumber, {
                       "firstCorner" : vector(newX, 0*inch),
                       "secondCorner" : vector(definition.width + newX, definition.height)
               });
               skSolve(sketch1);
           }
           // This condition checks that it is possible to space the parts, prevents infinite loop
           if (size(prevBlocks) != 0 && size(blocks) != 0 && prevBlocks == blocks) {
               println("Cut sheet is smaller than largest part plus twice the spacing");
               throw regenError(ErrorStringEnum.TRANSFORM_FAILED, ["cutSheet"]);
           } else {
               // Here you are left with whatever parts don't fit on the first cut sheet.
               // This loop runs once for each cut sheet.
               prevBlocks = blocks;
               sortedBlocks = sortBlocks(blocks);
               cutSheetNumber += 1;
               blocks = [];
           }
       }
   }, { });
   
   // Sort parts based on largest dimension
   export function sortBlocks(blocks is array) {
       var sortedBlocks = sort(blocks, function(block1, block2) {
           if (max(block2[].w, block2[].h) != max(block1[].w, block1[].h)) {
               return max(block2[].w, block2[].h) - max(block1[].w, block1[].h);
           } else {
               return min(block2[].w, block2[].h) - min(block1[].w, block1[].h);
           }
       });
       return sortedBlocks;
   }
   
   // This is a helper function that rotates blocks in place so that they can be
   // placed either vertically or horizontally on the cut sheet.
   export function rotateBlock(context is Context, id is Id, unique is string, block is box) {
       var zaxis is Line = line(vector(0, 0, 0) * inch, vector(0, 0, 1));
       opTransform(context, id + ("rotate" ~ unique), {
              "bodies" : block[].owner,
              "transform" : rotationAround(zaxis, 90 * degree)
       });
       
       const boundingBox is Box3d = evBox3d(context, {
              "topology" : block[].owner
       });
       var transformToOrigin = transform(-boundingBox.minCorner);
       
       opTransform(context, id + ("translate" ~ unique), {
              "bodies" : block[].owner,
              "transform" : transformToOrigin
       });
   }
   
   // Modified binary tree bin packing from: https://github.com/jakesgordon/bin-packing/blob/master/js/packer.js
   
   // Initializer for the bin packing algorithm
   export function Packer(width is ValueWithUnits, height is ValueWithUnits, spacing is ValueWithUnits, blocks is array, cutSheetNumber) returns array {
       var root = new box({'x': cutSheetNumber * width * 1.1 + spacing, 'y': 0*inch + spacing, 'w': width - 2*spacing, 'h': height - 2*spacing, 'used': false});
       return fit(root, blocks, spacing);
   }
   
   // Fit function calls findNode to determine recursively where the part fits on the sheet,
   // then calls splitNode to create a bin above and a bin to the right
   export function fit(root is box, blocks is array, spacing is ValueWithUnits) returns array{
       var node;
       var nodeR;
       var block;
       for (var n = 0; n < size(blocks); n += 1) {
           block = blocks[n];
           node = findNode(root, block[].w, block[].h);
           if (node != undefined) {
               block[].fit = splitNode(node, block[].w, block[].h, spacing);
           } else {
               nodeR = findNode(root, block[].h, block[].w);
               // See if a position can be found for the part if it is rotated
               // 90 degrees, if none is found in its original orientation
               if (nodeR != undefined) {
                   block[].fit = splitNode(nodeR, block[].h, block[].w, spacing);
                   block[].rotated = true;
               }
               else {
                   block[].fit = undefined;
               }
           }
       }
       return blocks;
   }
   
   // Recursively finds a bin where the part will fit
   export function findNode(root is box, w is ValueWithUnits, h is ValueWithUnits) {
       if (root[].used) {
           var right = findNode(root[].right, w, h);
           var above = findNode(root[].above, w, h);
           if (right != undefined) {
               return right;
           } else if (above != undefined) {
               return above;
           }
       } else if ((w < root[].w || tolerantEquals(w, root[].w)) && (h < root[].h || tolerantEquals(h, root[].h))) {
           return root;
       } else {
           return undefined;
       }
   }
   
   // Once a part is placed, creates a new bin above it and to the right of it
   export function splitNode(node is box, w is ValueWithUnits, h is ValueWithUnits, spacing is ValueWithUnits) returns box {
       node[].used = true;
       node[].above = new box({'x': node[].x, 'y': node[].y + h + spacing, 'w': node[].w, 'h': node[].h - (h + spacing), 'used': false});
       node[].right = new box({'x': node[].x + w + spacing, 'y': node[].y, 'w': node[].w - (w + spacing), 'h': h + spacing, 'used': false});
       return node;
   }

Config file:

FeatureScript 275;
import(path : "onshape/std/geometry.fs", version : "275.0");
// This file contains the defaults for thickness, cut sheet width, cut sheet height, and spacing.
// Edit as necessary.
export const DEFAULT_THICKNESS =
{
   "min"        : -500 * meter,
   "max"        : 500 * meter,
   (meter)      : [1e-5, 0.0127, 500],
   (centimeter) : 1.27,
   (millimeter) : 12.7,
   (inch)       : 0.5,
   (foot)       : 0.04,
   (yard)       : 0.014
} as LengthBoundSpec;
export const DEFAULT_SHEET_WIDTH =
{
   "min"        : -500 * meter,
   "max"        : 500 * meter,
   (meter)      : [1e-5, 1.524, 500],
   (centimeter) : 152.4,
   (millimeter) : 1524,
   (inch)       : 60,
   (foot)       : 5,
   (yard)       : 1.7
} as LengthBoundSpec;
export const DEFAULT_SHEET_HEIGHT =
{
   "min"        : -500 * meter,
   "max"        : 500 * meter,
   (meter)      : [1e-5, 2.54, 500],
   (centimeter) : 254,
   (millimeter) : 2540,
   (inch)       : 100,
   (foot)       : 8.3,
   (yard)       : 2.8
} as LengthBoundSpec;
export const DEFAULT_SPACING =
{
   "min"        : -500 * meter,
   "max"        : 500 * meter,
   (meter)      : [1e-5, 0.00254, 500],
   (centimeter) : 0.254,
   (millimeter) : 2.54,
   (inch)       : 0.1,
   (foot)       : 0.0083,
   (yard)       : 0.0028
} as LengthBoundSpec;


Comments

  • mlaflechecadmlaflechecad Onshape Employees, Developers Posts: 16
    This works so well, thanks so much for sharing!
    Regards,
    Mike LaFleche   @mlaflecheCAD
  • billy2billy2 Member, OS Professional, Mentor, Developers Posts: 818 PRO
    Very nice! Thanks for sharing.



  • shashank_aaryashashank_aarya Member Posts: 265 ✭✭✭
    Thanks very much for sharing!
  • viruviru Member, Developers Posts: 615 ✭✭✭✭
    Very nice! Thanks for sharing.



  • to_cato_ca Member Posts: 13
    Hi Marena,

    this is brilliant and works so well. Have looked through so many tools and addons to find a CAD program with a simple feature like this to enable simple laser cutting. My previous tool was clicking on each face, exporting them separately and then lining them up in another programme to get a DXF. This is 100x faster to iterate with. Thanks so much!
  • jochen333jochen333 OS Professional Posts: 27 PRO
    This really shows the power of feature script....

    Had to do a big assembly with a lot of sheets. It was really a pita generating all the dxf data for laser cutting.

    Tested your feature script with this assembly and it worked great...

    This will be a great relief in future.
  • Logan_5Logan_5 Member Posts: 1
    How do you add this to your own Onshape Part Studio?
  • lemon1324lemon1324 Member, Developers Posts: 82 EDU
    edited July 14
    When you've got the auto layout document open, click the "+ Custom Features" button at the top left, just below the document name. That adds the feature to your own toolbar, it'll be at the right end, possibly in a dropdown with other custom features you've added.

    Arul Suresh
    PhD Candidate at Stanford University
  • Jake_RosenfeldJake_Rosenfeld Onshape Employees, Developers Posts: 319
    See the "Start using custom features" section of this page:
    https://www.onshape.com/featurescript
    Jake Rosenfeld - Modeling Team
Sign In or Register to comment.