├── .gitignore ├── AppStoreDescription.txt ├── AppStorePrivacyPolicy.txt ├── FuseNest.manifest ├── FuseNest.py ├── LICENSE.txt ├── README.md ├── SVGnest ├── LICENSE.txt ├── favicon16.gif ├── favicon32.gif ├── font │ ├── fonts │ │ ├── LatoLatin-Bold.eot │ │ ├── LatoLatin-Bold.ttf │ │ ├── LatoLatin-Bold.woff │ │ ├── LatoLatin-Bold.woff2 │ │ ├── LatoLatin-BoldItalic.eot │ │ ├── LatoLatin-BoldItalic.ttf │ │ ├── LatoLatin-BoldItalic.woff │ │ ├── LatoLatin-BoldItalic.woff2 │ │ ├── LatoLatin-Light.eot │ │ ├── LatoLatin-Light.ttf │ │ ├── LatoLatin-Light.woff │ │ ├── LatoLatin-Light.woff2 │ │ ├── LatoLatin-Regular.eot │ │ ├── LatoLatin-Regular.ttf │ │ ├── LatoLatin-Regular.woff │ │ └── LatoLatin-Regular.woff2 │ ├── generator_config.txt │ ├── lato-hai-demo.html │ ├── lato-hai-webfont.eot │ ├── lato-hai-webfont.svg │ ├── lato-hai-webfont.ttf │ ├── lato-hai-webfont.woff │ ├── lato-lig-demo.html │ ├── lato-lig-webfont.eot │ ├── lato-lig-webfont.svg │ ├── lato-lig-webfont.ttf │ ├── lato-lig-webfont.woff │ ├── latolatinfonts.css │ ├── specimen_files │ │ ├── easytabs.js │ │ ├── grid_12-825-55-15.css │ │ └── specimen_stylesheet.css │ └── stylesheet.css ├── img │ ├── background.png │ ├── close.svg │ ├── code.svg │ ├── download.svg │ ├── logo.svg │ ├── settings.svg │ ├── spin.svg │ ├── start.svg │ ├── upload.svg │ ├── zoomin.svg │ └── zoomout.svg ├── index.html ├── readme.md ├── style.css ├── svgnest.js ├── svgparser.js └── util │ ├── clipper.js │ ├── domparser.js │ ├── eval.js │ ├── filesaver.js │ ├── geometryutil.js │ ├── json.js │ ├── matrix.js │ ├── parallel.js │ ├── pathsegpolyfill.js │ └── placementworker.js └── resources ├── 16x16-disabled.png ├── 16x16.png ├── 16x16@2x.png ├── 32x32-disabled.png ├── 32x32.png ├── 32x32@2x.png └── description.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /AppStoreDescription.txt: -------------------------------------------------------------------------------- 1 | This app automatically places parts on a sheet in a material-efficient manner given sheet size & spacing using the SVGNest open-source nesting algorithm. 2 | 3 | Features: 4 | - Automatically lays out parts 5 | - Starts a new sheet once the first one is full. 6 | - Adjustable spacing between parts 7 | - Continues to look for better solutions after the first one is found. 8 | 9 | This is the professional version, a free version is also available for non-commercial, educational, and trial use. Users of the professional version are prioritized regarding support and bug reports. 10 | 11 | This is the free version for non-commercial, educational, and trial use only. A professional version for commercial use, including priority support is also available. 12 | 13 | More details, written use instructions and the source code can be found here: 14 | https://github.com/NicoSchlueter/FuseNest -------------------------------------------------------------------------------- /AppStorePrivacyPolicy.txt: -------------------------------------------------------------------------------- 1 | This Add-In does not capture or store any user data. 2 | 3 | The Autodesk App Store captures, and gives us access to, a download record containing the following personal information: 4 | 5 | Language 6 | Country 7 | First Name 8 | Last Name 9 | Email 10 | Company Name 11 | Date & Time of download 12 | 13 | We do not store or use this data. -------------------------------------------------------------------------------- /FuseNest.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "addin", 4 | "id": "b6b8525c-7a45-4246-9eb5-548c03f63e16", 5 | "author": "Nico Schlueter", 6 | "description": { 7 | "": "SVG nest for Fusion360" 8 | }, 9 | "version": "1.0.0", 10 | "runOnStartup": false, 11 | "supportedOS": "windows|mac", 12 | "editEnabled": true 13 | } -------------------------------------------------------------------------------- /FuseNest.py: -------------------------------------------------------------------------------- 1 | #Author-Nico Schlueter 2 | #Description-SVG nest for Fusion360 3 | 4 | import adsk.core, adsk.fusion, adsk.cam, traceback 5 | import math 6 | import re 7 | from xml.dom import minidom 8 | import time, threading 9 | 10 | # Global set of event handlers to keep them referenced for the duration of the command 11 | _handlers = [] 12 | 13 | 14 | COMMAND_ID = "fuseNest" 15 | COMMAND_NAME = "FuseNest 2D Nesting" 16 | COMMAND_TOOLTIP = "2D nest/pack parts on a sheet" 17 | 18 | TOOLBAR_PANELS = ["SolidModifyPanel"] 19 | 20 | SVG_UNIT_FACTOR = 100 21 | 22 | # Initial persistence Dict 23 | pers = { 24 | "VISheetWidth": 50, 25 | "VISheetHeight": 30, 26 | "VISpacing": 0, 27 | "VISheetOffsetX": 0, 28 | "VISheetOffsetY": 5, 29 | "ISRotations": 4 30 | } 31 | 32 | transform_data = None 33 | 34 | # Fires when the CommandDefinition gets executed. 35 | # Responsible for adding commandInputs to the command & 36 | # registering the other command handlers. 37 | class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler): 38 | def __init__(self): 39 | super().__init__() 40 | def notify(self, args): 41 | try: 42 | global transform_data 43 | global pers 44 | transform_data = None 45 | 46 | # Get the command that was created. 47 | cmd = adsk.core.Command.cast(args.command) 48 | 49 | # Registers the CommandDestryHandler 50 | onExecute = CommandExecuteHandler() 51 | cmd.execute.add(onExecute) 52 | _handlers.append(onExecute) 53 | 54 | # Registers the CommandInputChangedHandler 55 | onInputChanged = CommandInputChangedHandler() 56 | cmd.inputChanged.add(onInputChanged) 57 | _handlers.append(onInputChanged) 58 | 59 | 60 | # Get the CommandInputs collection associated with the command. 61 | inputs = cmd.commandInputs 62 | 63 | siBodies = inputs.addSelectionInput("SIBodies", "Bodies", "Select Bodies") 64 | siBodies.addSelectionFilter("SolidBodies") 65 | siBodies.setSelectionLimits(0, 0) 66 | siBodies.tooltip = "Select parts to be placed" 67 | 68 | viSheetWidth = inputs.addValueInput("VISheetWidth", "Sheet Width", "mm", adsk.core.ValueInput.createByReal(pers["VISheetWidth"])) 69 | viSheetWidth.tooltip = "Width of the sheet the parts will be placed on" 70 | 71 | viSheetHeight = inputs.addValueInput("VISheetHeight", "Sheet Height", "mm", adsk.core.ValueInput.createByReal(pers["VISheetHeight"])) 72 | viSheetHeight.tooltip = "Height of the sheet the parts will be placed on" 73 | 74 | viSheetOffsetX = inputs.addValueInput("VISheetOffsetX", "Sheet Offset X", "mm", adsk.core.ValueInput.createByReal(pers["VISheetOffsetX"])) 75 | viSheetOffsetX.tooltip = "Offset between multiple sheets in x" 76 | 77 | viSheetOffsetY = inputs.addValueInput("VISheetOffsetY", "Sheet Offset Y", "mm", adsk.core.ValueInput.createByReal(pers["VISheetOffsetY"])) 78 | viSheetOffsetY.tooltip = "Offset between multiple sheets in y" 79 | 80 | viSpacing = inputs.addValueInput("VISpacing", "Spacing", "mm", adsk.core.ValueInput.createByReal(pers["VISpacing"])) 81 | viSpacing.tooltip = "Spacing between parts" 82 | viSpacing.tooltipDescription = "May not be exact, can be off by ±0.5mm" 83 | 84 | isRotations = inputs.addIntegerSpinnerCommandInput("ISRotations", "Rotations", 2, 999999, 1, pers["ISRotations"]) 85 | isRotations.tooltip = "Number of possible rotations" 86 | isRotations.tooltipDescription ="How many rotations to try.\nTrying more rotations will take longer to find a good result, but is necessary for some shapes\n\nRecommendations:\n2 - Perfectly circular, square or hexagonal\n4 - Rectangular\n8+ - Odd/Organic shapes or mixed" 87 | 88 | bvNest = inputs.addBoolValueInput("BVNest", " Start Nesting ", False) 89 | bvNest.isFullWidth = True 90 | bvNest.tooltip = "Start nesting process" 91 | 92 | root = adsk.core.Application.get().activeProduct.rootComponent 93 | for i in root.bRepBodies: 94 | siBodies.addSelection(i) 95 | 96 | # Selects all the things 97 | for o in root.occurrences: 98 | for i in o.bRepBodies: 99 | siBodies.addSelection(i) 100 | 101 | except: 102 | print(traceback.format_exc()) 103 | 104 | 105 | #Fires when the User executes the Command 106 | #Responsible for doing the changes to the document 107 | class CommandExecuteHandler(adsk.core.CommandEventHandler): 108 | def __init__(self): 109 | super().__init__() 110 | def notify(self, args): 111 | try: 112 | global transform_data 113 | if(transform_data): 114 | selections = [] 115 | 116 | for i in range(args.command.commandInputs.itemById("SIBodies").selectionCount): 117 | selections.append(args.command.commandInputs.itemById("SIBodies").selection(i).entity) 118 | 119 | first_move = None 120 | last_move = None 121 | 122 | root = adsk.core.Application.get().activeProduct.rootComponent 123 | des = adsk.core.Application.get().activeDocument.design 124 | 125 | offset_x = 0 126 | offset_y = 0 127 | 128 | if(args.command.commandInputs.itemById("VISheetOffsetX").value > 0): 129 | offset_x = args.command.commandInputs.itemById("VISheetWidth").value + args.command.commandInputs.itemById("VISheetOffsetX").value 130 | elif(args.command.commandInputs.itemById("VISheetOffsetX").value < 0): 131 | offset_x = -args.command.commandInputs.itemById("VISheetWidth").value + args.command.commandInputs.itemById("VISheetOffsetX").value 132 | 133 | if(args.command.commandInputs.itemById("VISheetOffsetY").value > 0): 134 | offset_y = args.command.commandInputs.itemById("VISheetHeight").value + args.command.commandInputs.itemById("VISheetOffsetY").value 135 | elif(args.command.commandInputs.itemById("VISheetOffsetY").value < 0): 136 | offset_y = -args.command.commandInputs.itemById("VISheetHeight").value + args.command.commandInputs.itemById("VISheetOffsetY").value 137 | 138 | 139 | 140 | for i, t in transform_data: 141 | mat1 = adsk.core.Matrix3D.create() 142 | mat1.translation = adsk.core.Vector3D.create(t[0] + offset_x * t[3], -t[1] - offset_y * t[3], 0) 143 | 144 | mat2 = adsk.core.Matrix3D.create() 145 | mat2.setToRotation( 146 | math.radians(-t[2]), 147 | adsk.core.Vector3D.create(0,0,1), 148 | adsk.core.Point3D.create(0,0,0) 149 | ) 150 | 151 | mat2.transformBy(mat1) 152 | 153 | if(selections[i].parentComponent == root): 154 | oc = adsk.core.ObjectCollection.create() 155 | oc.add(selections[i]) 156 | 157 | move_input = root.features.moveFeatures.createInput(oc, mat2) 158 | 159 | if(first_move is None): 160 | first_move = root.features.moveFeatures.add(move_input) 161 | last_move = first_move 162 | else: 163 | last_move = root.features.moveFeatures.add(move_input) 164 | else: 165 | trans = selections[i].assemblyContext.transform 166 | trans.transformBy(mat2) 167 | selections[i].assemblyContext.transform = trans 168 | if(des.designType and des.snapshots.hasPendingSnapshot): 169 | des.snapshots.add() 170 | if(first_move): 171 | des.timeline.timelineGroups.add(first_move.timelineObject.index, last_move.timelineObject.index+1) 172 | elif(first_move and not( first_move == last_move) and des.designType): 173 | des.timeline.timelineGroups.add(first_move.timelineObject.index, last_move.timelineObject.index) 174 | except: 175 | print(traceback.format_exc()) 176 | 177 | 178 | # Fires when CommandInputs are changed 179 | # Responsible for dynamically updating other Command Inputs 180 | class CommandInputChangedHandler(adsk.core.InputChangedEventHandler): 181 | def __init__(self): 182 | super().__init__() 183 | def notify(self, args): 184 | try: 185 | if(args.input.id == "BVNest"): 186 | 187 | global pers 188 | 189 | pers["VISheetWidth"] = args.inputs.itemById("VISheetWidth").value 190 | pers["VISheetHeight"] = args.inputs.itemById("VISheetHeight").value 191 | pers["VISpacing"] = args.inputs.itemById("VISpacing").value 192 | pers["ISRotations"] = args.inputs.itemById("ISRotations").value 193 | pers["VISheetOffsetX"] = args.inputs.itemById("VISheetOffsetX").value 194 | pers["VISheetOffsetY"] = args.inputs.itemById("VISheetOffsetY").value 195 | 196 | # If there is already a palette, delete it 197 | # This is mainly for debugging purposes 198 | ui = adsk.core.Application.get().userInterface 199 | palette = ui.palettes.itemById('paletteSVGNest') 200 | if palette: 201 | palette.deleteMe() 202 | 203 | global SVG_UNIT_FACTOR 204 | 205 | root = adsk.core.Application.get().activeProduct.rootComponent 206 | 207 | 208 | # Getting selections now, as creating sketches clears em 209 | selections = [] 210 | for i in range(args.inputs.itemById("SIBodies").selectionCount): 211 | selections.append(args.inputs.itemById("SIBodies").selection(i).entity) 212 | 213 | paths = [] 214 | 215 | for s in selections: 216 | sketch = root.sketches.add(root.xYConstructionPlane) 217 | sketch.project(s) 218 | paths.append(sketchToSVGPaths(sketch)) 219 | sketch.deleteMe() 220 | 221 | svg = buildSVGFromPaths(paths, args.inputs.itemById("VISheetWidth").value, args.inputs.itemById("VISheetHeight").value) 222 | 223 | dataToSend = "{};{};{};{};{}".format( 224 | svg, 225 | args.inputs.itemById("VISpacing").value * SVG_UNIT_FACTOR, 226 | args.inputs.itemById("ISRotations").value, 227 | True, 228 | True 229 | ) 230 | 231 | # Re-selects all components, as they get lost during the previous steps 232 | for s in selections: 233 | args.inputs.itemById("SIBodies").addSelection(s) 234 | 235 | 236 | palette = ui.palettes.add('paletteSVGNest', '2D Nest', 'SVGnest/index.html', True, True, True, 1500, 1000, True) 237 | 238 | # Dock the palette to the right side of Fusion window. 239 | palette.dockingState = adsk.core.PaletteDockingStates.PaletteDockStateTop 240 | 241 | # Add handler to CloseEvent of the palette. 242 | onClosed = PaletteCloseEventHandler() 243 | palette.closed.add(onClosed) 244 | _handlers.append(onClosed) 245 | 246 | # Add handler to HTMLEvent of the palette. 247 | onHTMLEvent = PaletteHTMLEventHandler() 248 | palette.incomingFromHTML.add(onHTMLEvent) 249 | _handlers.append(onHTMLEvent) 250 | 251 | # Sends data to palette after it had time to load 252 | threading.Timer(4, sendDataToPalette, [palette, dataToSend]).start() 253 | 254 | except: 255 | print(traceback.format_exc()) 256 | 257 | 258 | # Fires when Palette is closed 259 | # Responsible for deleting it 260 | class PaletteCloseEventHandler(adsk.core.UserInterfaceGeneralEventHandler): 261 | def __init__(self): 262 | super().__init__() 263 | def notify(self, args): 264 | try: 265 | ui = adsk.core.Application.get().userInterface 266 | palette = ui.palettes.itemById('paletteSVGNest') 267 | 268 | #if palette: 269 | # palette.deleteMe() 270 | except: 271 | print(traceback.format_exc()) 272 | 273 | 274 | # Fires when an HTML event is sent from the Palette 275 | # Responsive for parsing that data 276 | class PaletteHTMLEventHandler(adsk.core.HTMLEventHandler): 277 | def __init__(self): 278 | super().__init__() 279 | def notify(self, args): 280 | try: 281 | if(args.action == "exportSVG"): 282 | global transform_data 283 | transform_data = getTransformsFromSVG(args.data) 284 | 285 | ui = adsk.core.Application.get().userInterface 286 | palette = ui.palettes.itemById('paletteSVGNest') 287 | if palette: 288 | palette.deleteMe() 289 | 290 | except: 291 | print(traceback.format_exc()) 292 | 293 | 294 | def sendDataToPalette(palette, data): 295 | """Sends data string to pallet 296 | 297 | Args: 298 | palette: (Palette) Palette to send data to 299 | data: (String) Data string to send to pallete 300 | """ 301 | if palette: 302 | palette.sendInfoToHTML("importSVG", data) 303 | 304 | 305 | def buildSVGFromPaths(paths, width=50, height=25): 306 | """Constructs a full svg fle from paths 307 | 308 | Args: 309 | paths: (String[][]) SVG path data 310 | width: (float) Width ouf bounding rectangle 311 | height: (float) Height of bounding rectangle 312 | 313 | Returns: 314 | [string]: full svg 315 | 316 | """ 317 | global SVG_UNIT_FACTOR 318 | 319 | rtn = "" 320 | 321 | 322 | rtn += r" ".format(width*SVG_UNIT_FACTOR, height*SVG_UNIT_FACTOR) 323 | 324 | rtn += r" ".format(width*SVG_UNIT_FACTOR, height*SVG_UNIT_FACTOR) 325 | 326 | for i, p in enumerate(paths): 327 | for j in p: 328 | rtn += r" ".format(j, i) 329 | 330 | rtn += r"" 331 | 332 | return rtn 333 | 334 | 335 | def getTransformsFromSVG(svg): 336 | """Imports SVG result from svgnest and extracts transform data 337 | 338 | Args: 339 | svg: (String) SVG data 340 | 341 | Returns: 342 | [zip]: zip of index and transform data (x, y, r) 343 | """ 344 | 345 | global SVG_UNIT_FACTOR 346 | 347 | # Wraps svg data into single root node 348 | svg = "{}".format(svg) 349 | 350 | dom = minidom.parseString(svg) 351 | 352 | ids = [] 353 | transforms = [] 354 | 355 | p = re.compile(r'[\(\s]-?\d*\.{0,1}\d+') 356 | 357 | for sheetNumber, s in enumerate(dom.firstChild.childNodes): 358 | for e in s.getElementsByTagName('path'): 359 | if not(int(e.getAttribute('id')) in ids): 360 | ids.append(int(e.getAttribute('id'))) 361 | 362 | # Gets the transform atributes as string 363 | transformTag = (e.parentNode.getAttribute('transform')) 364 | 365 | # Converts string to list of floats via regex 366 | # Adds sheet number to the end 367 | transforms.append([float(i[1:]) for i in p.findall(transformTag)] + [sheetNumber] ) 368 | 369 | # X, Y, rotation, sheet number 370 | transformsScaled = [ [t[0]/SVG_UNIT_FACTOR, t[1]/SVG_UNIT_FACTOR, t[2], t[3]] for t in transforms] 371 | 372 | return zip(ids, transformsScaled) 373 | 374 | 375 | def sketchToSVGPaths(sketch): 376 | """Converts a Sketch into a SVG Path date 377 | 378 | Args: 379 | sketch: (Sketch) Sketch to convert 380 | 381 | Returns: 382 | [str]: Array of SVG paths 383 | """ 384 | 385 | rtn = "" 386 | 387 | sortedProfiles = sorted(sketch.profiles, key=lambda x: len(x.profileLoops), reverse=True) 388 | 389 | """for p in sketch.profiles: 390 | for pl in p.profileLoops: 391 | 392 | if(pl.isOuter): 393 | rtn.append(loopToSVGPath(pl)) 394 | break 395 | """ 396 | 397 | 398 | for pl in sortedProfiles[0].profileLoops: 399 | # Outer should be clockwise 400 | # Inner should be counterclockwise 401 | if(pl.isOuter != isLoopClockwise(pl)): 402 | rtn += loopToSVGPath(pl, True) 403 | else: 404 | rtn += loopToSVGPath(pl, False) 405 | 406 | return [rtn] 407 | 408 | 409 | def loopToSVGPath(loop, reverse = False): 410 | """Converts a ProfileLoop into a SVG Path date 411 | 412 | Args: 413 | loop: (ProfileLoop) Loop to convert 414 | reverse: (Bool) Invert direction 415 | 416 | Returns: 417 | str: SVG Path data 418 | """ 419 | 420 | global SVG_UNIT_FACTOR 421 | 422 | rtn = "" 423 | 424 | profileCurves = [i for i in loop.profileCurves] 425 | 426 | if(reverse): 427 | profileCurves.reverse() 428 | 429 | flip = getWhatCurvesToFlip(profileCurves) 430 | 431 | if(reverse and len(flip) == 1): 432 | flip = [not(i) for i in flip] 433 | 434 | 435 | for c, f in zip(profileCurves, flip): 436 | rtn += curveToPathSegment( 437 | c, 438 | 1/SVG_UNIT_FACTOR, 439 | f, 440 | not rtn 441 | ) 442 | return rtn 443 | 444 | 445 | def curveToPathSegment(curve, scale=1, invert=False, moveTo=False): 446 | """Converts a ProfileCurve into a SVG Path date segment 447 | 448 | Args: 449 | curve: (ProfileCurve) The curve object to be converted 450 | scale: (float) How many units are per SVG unit 451 | invert: (bool) Swaps curve's startPoint and endPoint 452 | moveTo: (bool) Moves to the startPoint before conversion 453 | 454 | Returns: 455 | str: Segment of SVG Path data. 456 | 457 | """ 458 | 459 | rtn = "" 460 | 461 | 462 | if(curve.geometry.objectType == "adsk::core::Line3D"): 463 | if(not invert): 464 | if(moveTo): 465 | rtn += "M{0:.6f} {1:.6f} ".format( 466 | curve.geometry.startPoint.x / scale, 467 | -curve.geometry.startPoint.y / scale 468 | ) 469 | 470 | rtn += "L{0:.6f} {1:.6f} ".format( 471 | curve.geometry.endPoint.x / scale, 472 | -curve.geometry.endPoint.y / scale) 473 | 474 | else: 475 | if(moveTo): 476 | rtn += "M{0:.6f} {1:.6f} ".format( 477 | curve.geometry.endPoint.x / scale, 478 | -curve.geometry.endPoint.y / scale 479 | ) 480 | 481 | rtn += "L{0:.6f} {1:.6f} ".format( 482 | curve.geometry.startPoint.x / scale, 483 | -curve.geometry.startPoint.y / scale) 484 | 485 | elif(curve.geometry.objectType == "adsk::core::Arc3D"): 486 | if(not invert): 487 | if(moveTo): 488 | rtn += "M{0:.6f} {1:.6f} ".format( 489 | curve.geometry.startPoint.x / scale, 490 | -curve.geometry.startPoint.y / scale 491 | ) 492 | 493 | # rx ry rot large_af sweep_af x y 494 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 495 | curve.geometry.radius / scale, 496 | curve.geometry.endAngle-curve.geometry.startAngle > math.pi, 497 | 0, 498 | curve.geometry.endPoint.x / scale, 499 | -curve.geometry.endPoint.y / scale 500 | ) 501 | else: 502 | if(moveTo): 503 | rtn += "M{0:.6f} {1:.6f} ".format( 504 | curve.geometry.endPoint.x / scale, 505 | -curve.geometry.endPoint.y / scale 506 | ) 507 | 508 | # rx ry rot large_af sweep_af x y 509 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 510 | curve.geometry.radius / scale, 511 | curve.geometry.endAngle-curve.geometry.startAngle > math.pi, 512 | 1, 513 | curve.geometry.startPoint.x / scale, 514 | -curve.geometry.startPoint.y / scale 515 | ) 516 | 517 | elif(curve.geometry.objectType == "adsk::core::Circle3D"): 518 | sp = curve.geometry.center.copy() 519 | sp.translateBy(adsk.core.Vector3D.create(curve.geometry.radius, 0, 0)) 520 | 521 | ep = curve.geometry.center.copy() 522 | ep.translateBy(adsk.core.Vector3D.create(0, curve.geometry.radius, 0)) 523 | 524 | if(not invert): 525 | 526 | if(moveTo): 527 | rtn += "M{0:.6f} {1:.6f} ".format( 528 | sp.x / scale, 529 | -sp.y / scale 530 | ) 531 | 532 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 533 | curve.geometry.radius / scale, 534 | 1, 535 | 1, 536 | ep.x / scale, 537 | -ep.y / scale 538 | ) 539 | 540 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 541 | curve.geometry.radius / scale, 542 | 0, 543 | 1, 544 | sp.x / scale, 545 | -sp.y / scale 546 | ) 547 | 548 | else: 549 | if(moveTo): 550 | rtn += "M{0:.6f} {1:.6f} ".format( 551 | sp.x / scale, 552 | -sp.y / scale 553 | ) 554 | 555 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 556 | curve.geometry.radius / scale, 557 | 0, 558 | 0, 559 | ep.x / scale, 560 | -ep.y / scale 561 | ) 562 | 563 | rtn += "A {0:.6f} {0:.6f} 0 {1:.0f} {2:.0f} {3:.6f} {4:.6f}".format( 564 | curve.geometry.radius / scale, 565 | 1, 566 | 0, 567 | sp.x / scale, 568 | -sp.y / scale 569 | ) 570 | 571 | 572 | elif(curve.geometry.objectType == "adsk::core::Ellipse3D"): 573 | sp = curve.geometry.center.copy() 574 | la = curve.geometry.majorAxis.copy() 575 | la.normalize() 576 | la.scaleBy(curve.geometry.majorRadius) 577 | sp.translateBy(la) 578 | 579 | ep = curve.geometry.center.copy() 580 | sa = adsk.core.Vector3D.crossProduct(curve.geometry.majorAxis, adsk.core.Vector3D.create(0,0,1)) 581 | sa.normalize() 582 | sa.scaleBy(curve.geometry.minorRadius) 583 | ep.translateBy(sa) 584 | 585 | angle = -math.degrees(math.atan2(la.y, la.x)) 586 | 587 | if(not invert): 588 | if(moveTo): 589 | rtn += "M{0:.6f} {1:.6f} ".format( 590 | sp.x / scale, 591 | -sp.y / scale 592 | ) 593 | 594 | # rx ry rot large_af sweep_af x y 595 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 596 | curve.geometry.majorRadius / scale, 597 | curve.geometry.minorRadius / scale, 598 | angle, 599 | 1, 600 | 0, 601 | ep.x / scale, 602 | -ep.y / scale 603 | ) 604 | 605 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 606 | curve.geometry.majorRadius / scale, 607 | curve.geometry.minorRadius / scale, 608 | angle, 609 | 0, 610 | 0, 611 | sp.x / scale, 612 | -sp.y / scale 613 | ) 614 | else: 615 | if(moveTo): 616 | rtn += "M{0:.6f} {1:.6f} ".format( 617 | sp.x / scale, 618 | -sp.y / scale 619 | ) 620 | 621 | # rx ry rot large_af sweep_af x y 622 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 623 | curve.geometry.majorRadius / scale, 624 | curve.geometry.minorRadius / scale, 625 | angle, 626 | 0, 627 | 1, 628 | ep.x / scale, 629 | -ep.y / scale 630 | ) 631 | 632 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 633 | curve.geometry.majorRadius / scale, 634 | curve.geometry.minorRadius / scale, 635 | angle, 636 | 1, 637 | 1, 638 | sp.x / scale, 639 | -sp.y / scale 640 | ) 641 | 642 | 643 | elif(curve.geometry.objectType == "adsk::core::EllipticalArc3D"): 644 | angle = -math.degrees(math.atan2(curve.geometry.majorAxis.y, curve.geometry.majorAxis.x)) 645 | 646 | _, sp, ep = curve.geometry.evaluator.getEndPoints() 647 | 648 | if(not invert): 649 | if(moveTo): 650 | rtn += "M{0:.6f} {1:.6f} ".format( 651 | sp.x / scale, 652 | -sp.y / scale 653 | ) 654 | 655 | # rx ry rot large_af sweep_af x y 656 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 657 | curve.geometry.majorRadius / scale, 658 | curve.geometry.minorRadius / scale, 659 | angle, 660 | curve.geometry.endAngle-curve.geometry.startAngle > math.pi, 661 | 0, 662 | ep.x / scale, 663 | -ep.y / scale 664 | ) 665 | else: 666 | if(moveTo): 667 | rtn += "M{0:.6f} {1:.6f} ".format( 668 | ep.x / scale, 669 | -ep.y / scale 670 | ) 671 | 672 | # rx ry rot large_af sweep_af x y 673 | rtn += "A {0:.6f} {1:.6f} {2:.6f} {3:.0f} {4:.0f} {5:.6f} {6:.6f}".format( 674 | curve.geometry.majorRadius / scale, 675 | curve.geometry.minorRadius / scale, 676 | angle, 677 | curve.geometry.endAngle-curve.geometry.startAngle > math.pi, 678 | 1, 679 | sp.x / scale, 680 | -sp.y / scale 681 | ) 682 | 683 | elif(curve.geometry.objectType == "adsk::core::NurbsCurve3D"): 684 | # Aproximates nurbs with straight line segments 685 | 686 | ev = curve.geometry.evaluator 687 | _, sp, ep = ev.getParameterExtents() 688 | 689 | # List of segments, initially subdivided into two segments 690 | s = [sp, lerp(sp, ep, 0.5), ep] 691 | 692 | # Maxiumum angle two neighboring segments can have 693 | maxAngle = math.radians(10) 694 | 695 | # Minimum length of segment 696 | minLength = 0.05 697 | 698 | i = 0 699 | while(i < len(s)-1): 700 | _, t = ev.getTangents(s) 701 | _, p = ev.getPointsAtParameters(s) 702 | 703 | # If the angle to the next segment is small enough, move one 704 | if( t[i].angleTo( t[i+1]) < maxAngle or p[i].distanceTo(p[i+1]) < minLength): 705 | i += 1 706 | # Otherwise subdivide the next segment into two 707 | else: 708 | s.insert(i+1, lerp( s[i], s[i+1], 0.5)) 709 | 710 | if(not invert): 711 | if(moveTo): 712 | rtn += "M{0:.6f} {1:.6f} ".format( 713 | p[0].x / scale, 714 | -p[0].y / scale 715 | ) 716 | for i in p[1:]: 717 | rtn += "L{0:.6f} {1:.6f} ".format( 718 | i.x / scale, 719 | -i.y / scale 720 | ) 721 | else: 722 | if(moveTo): 723 | rtn += "M{0:.6f} {1:.6f} ".format( 724 | p[-1].x / scale, 725 | -p[-1].y / scale 726 | ) 727 | for i in reversed(p[:-1]): 728 | rtn += "L{0:.6f} {1:.6f} ".format( 729 | i.x / scale, 730 | -i.y / scale 731 | ) 732 | 733 | else: 734 | print("Warning: Unsupported curve type, could not be converted: {}".format(curve.geometryType)) 735 | return rtn 736 | 737 | 738 | def isLoopClockwise(loop): 739 | """Determins if a ProfileLoop is clockwise 740 | 741 | Args: 742 | loop: (ProfileLoop) The loop to check 743 | 744 | Returns: 745 | bool: True if clockwise. 746 | """ 747 | 748 | # If if it has only one segment it is clockwise by definition 749 | if(len(loop.profileCurves) == 1): 750 | return False; 751 | 752 | # https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order 753 | res = 0 754 | 755 | sp = [getStartPoint(i.geometry) for i in loop.profileCurves] 756 | ep = [getEndPoint(i.geometry) for i in loop.profileCurves] 757 | 758 | 759 | # range(len()) one of the deally sins of python 760 | for s, e in zip(sp, ep): 761 | res += (e.x - s.x) * (e.y + s.y) 762 | 763 | return res > 0 764 | 765 | 766 | def getWhatCurvesToFlip(curves): 767 | """Determins which ProfileCurves need their startPoint and endPoint flipped to line up end to end 768 | 769 | Args: 770 | curves: (ProfileCurves) The List of points to check agains 771 | 772 | Returns: 773 | bool[]: List of bools of equal length to curves. 774 | """ 775 | 776 | if(len(curves)==1): 777 | return [False] 778 | else: 779 | rtn = [] 780 | 781 | for i in range(len(curves)): 782 | if(i==0): 783 | rtn.append( 784 | isPointInList( 785 | getStartPoint(curves[i].geometry), 786 | [getStartPoint(curves[1].geometry), getEndPoint(curves[1].geometry)] 787 | ) 788 | ) 789 | else: 790 | rtn.append( 791 | not isPointInList( 792 | getStartPoint(curves[i].geometry), 793 | [getStartPoint(curves[i-1].geometry), getEndPoint(curves[i-1].geometry)] 794 | ) 795 | ) 796 | return rtn 797 | 798 | 799 | def getStartPoint(curve): 800 | """Gets the start point of any Curve3D object 801 | 802 | Args: 803 | curves: (Curve3D) The curve 804 | 805 | Returns: 806 | Point3D: Start point of the curve 807 | """ 808 | 809 | if(curve.objectType in ["adsk::core::Line3D", "adsk::core::Arc3D"]): 810 | return curve.startPoint 811 | 812 | elif(curve.objectType == "adsk::core::Circle3D"): 813 | sp = curve.center.copy() 814 | sp.translateBy(adsk.core.Vector3D.create(curve.radius, 0, 0)) 815 | return sp 816 | 817 | elif(curve.objectType == "adsk::core::Ellipse3D"): 818 | sp = curve.center.copy() 819 | la = curve.majorAxis.copy() 820 | la.normalize() 821 | la.scaleBy(curve.majorRadius) 822 | sp.translateBy(la) 823 | return sp 824 | 825 | elif(curve.objectType in ["adsk::core::EllipticalArc3D", "adsk::core::NurbsCurve3D"]): 826 | _, sp, ep = curve.evaluator.getEndPoints() 827 | return sp 828 | 829 | 830 | def getEndPoint(curve): 831 | """Gets the end point of any Curve3D object 832 | 833 | Args: 834 | curves: (Curve3D) The curve 835 | 836 | Returns: 837 | Point3D: End point of the curve 838 | """ 839 | 840 | if(curve.objectType in ["adsk::core::Line3D", "adsk::core::Arc3D"]): 841 | return curve.endPoint 842 | 843 | elif(curve.objectType == "adsk::core::Circle3D"): 844 | ep = curve.center.copy() 845 | ep.translateBy(adsk.core.Vector3D.create(curve.radius, 0, 0)) 846 | return ep 847 | 848 | elif(curve.objectType == "adsk::core::Ellipse3D"): 849 | ep = curve.center.copy() 850 | la = curve.majorAxis.copy() 851 | la.normalize() 852 | la.scaleBy(curve.majorRadius) 853 | ep.translateBy(la) 854 | return ep 855 | 856 | elif(curve.objectType in ["adsk::core::EllipticalArc3D", "adsk::core::NurbsCurve3D"]): 857 | _, sp, ep = curve.evaluator.getEndPoints() 858 | return ep 859 | 860 | 861 | def isPointInList(point, pointList, tol=1e-4): 862 | """Determins if a Point3D is almost-equal to a Point3D in a list 863 | 864 | Args: 865 | point: (Point3D) The Point to be checked 866 | pointList: (Point3D[]) The List of points to check agains 867 | tol: (float) Tollerance for almost-equality 868 | 869 | Returns: 870 | bool: True if almost equal to any Point in list 871 | """ 872 | 873 | for i in pointList: 874 | if(isPointEqual(point, i, tol)): 875 | return True 876 | return False 877 | 878 | 879 | def isPointEqual(point1, point2, tol=1e-4): 880 | """Determins if a Point3D is almost-equal to a Point3D in a list 881 | 882 | Args: 883 | point1: (Point3D) The Point to be checked 884 | point2: (Point3D) The Points to check agains 885 | tol: (float) Tollerance for almost-equality 886 | 887 | Returns: 888 | bool: True if almost equal to point 889 | """ 890 | return math.isclose(point1.x, point2.x, rel_tol=tol) and math.isclose(point1.y, point2.y, rel_tol=tol) and math.isclose(point1.z, point2.z, rel_tol=tol) 891 | 892 | 893 | def lerp(a, b, i): 894 | """Linearly interpolates from a to b 895 | 896 | Args: 897 | a: (float) The value to interpolate from 898 | b: (float) The value to interpolate to 899 | i: (float) Interpolation factor 900 | 901 | Returns: 902 | float: Interpolation result 903 | """ 904 | return a + (b-a)*i 905 | 906 | 907 | def run(context): 908 | try: 909 | 910 | app = adsk.core.Application.get() 911 | ui = app.userInterface 912 | 913 | commandDefinitions = ui.commandDefinitions 914 | #check the command exists or not 915 | cmdDef = commandDefinitions.itemById(COMMAND_ID) 916 | if not cmdDef: 917 | cmdDef = commandDefinitions.addButtonDefinition(COMMAND_ID, COMMAND_NAME, 918 | COMMAND_TOOLTIP, 'resources') 919 | 920 | cmdDef.tooltip = "Automatically lays out parts on a sheet optimizing for material usage" 921 | cmdDef.toolClipFilename = 'resources/description.png' 922 | #Adds the commandDefinition to the toolbar 923 | for panel in TOOLBAR_PANELS: 924 | ui.allToolbarPanels.itemById(panel).controls.addCommand(cmdDef) 925 | 926 | onCommandCreated = CommandCreatedHandler() 927 | cmdDef.commandCreated.add(onCommandCreated) 928 | _handlers.append(onCommandCreated) 929 | except: 930 | print(traceback.format_exc()) 931 | 932 | 933 | def stop(context): 934 | try: 935 | app = adsk.core.Application.get() 936 | ui = app.userInterface 937 | 938 | #Removes the commandDefinition from the toolbar 939 | for panel in TOOLBAR_PANELS: 940 | p = ui.allToolbarPanels.itemById(panel).controls.itemById(COMMAND_ID) 941 | if p: 942 | p.deleteMe() 943 | 944 | #Deletes the commandDefinition 945 | ui.commandDefinitions.itemById(COMMAND_ID).deleteMe() 946 | 947 | 948 | 949 | except: 950 | print(traceback.format_exc()) 951 | 952 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![2](https://user-images.githubusercontent.com/30301307/80922746-5edb1300-8d7f-11ea-8fee-337ab73c3d71.png) 2 | 3 | 4 | # FuseNest 5 | 2D Nesting / Packing Add-In for Autodesk Fusion360 based on SVGNest 6 | 7 | Screenshot 2020-05-03 at 20 55 45 8 | 9 | # Features 10 | * Nesting of bodies & components on rectangular sheets 11 | * Spacing between parts 12 | * Multiple sheets 13 | 14 | # How to use 15 | Launch the command and select the bodies you whish to nest. All bodies will be selected by default. We suggest having one body per component as this works the best, but having all bodies in the root component or a mix of both also works (but creates a lot of timeline objects). Bodies shouldn't overlap and should be placed further apart than the desired spacing. 16 | Set the parameters as required: 17 | 18 | **Sheet Width/Height:** 19 | Width & Height of the rectangular sheet parts will be placed on. 20 | 21 | **Sheet offset X/Y:** 22 | Distance between sheets 23 | e.g. X=-30mm Y=0mm will place the next sheet 30mm left of the previous one. 24 | 25 | **Spacing:** 26 | Approximate spacing between parts. Set this a bit higher than your minimum spacing as it can vary by small amounts. 27 | 28 | **Rotations:** 29 | The number of rotations the Algorithm will try. Setting this too high will drastically increase the time required to find a good solution, setting it too low will make it impossible to find some good solutions. Here are some suggested values: 30 | 31 | * Perfectly circular, square or Hexagonal parts: 2 32 | * Good compromise between speed and quality: 4 33 | * Odd/Organic shapes with high aspect ratio: 8+ 34 | 35 | Screenshot 2020-07-26 at 11 02 34 36 | 37 | Press "Start Nesting" to start the nesting process. 38 | This will open a new window and will start nesting after the selected bodies are loaded. This may take several minutes if the bodies are complex. 39 | The first round of nesting will take the longest. The Progress bar will indicate the approximate progress for the current iteration. After the first iteration, the algorithm will try to find better solutions in further iterations until it is stopped. 40 | 41 | Press "Apply Nest" to accept the current result or Press "Close" to go back to the previous step, deleting any progress done. Pressing "Stop Nest" will pause the process temporarily. It can be resumed by pressing "Start Nest" 42 | 43 | Screenshot 2020-07-26 at 11 16 03 44 | 45 | After pressing "Apply Nest" you will be back to the command. Press "OK" to confirm. Changing selected bodies or settings will void the previously calculated nesting data, so be careful. 46 | 47 | # Installation 48 | **Installation through the Fusion360 App Store will be available soon** 49 | 50 | * Download the Project as ZIP and extract it somewhere you can find again, but won't bother you. (or use git to clone it there) 51 | * Open Fusion360 and press ADD-INS > Scripts and Add-ins 52 | * Select the tab Add-Ins and click the green plus symbol next to "My Add-Ins" 53 | * Navigate to the extracted Project folder and hit open 54 | * The Add-in should now appear in the "My Add-Ins" list. Select it in the list. If desired check the "Run on Startup" checkbox and hit run. 55 | * The Command will appear as Modify > 2D Nest 56 | 57 | # Changelog 58 | 59 | ## 1.0 Initial Version 60 | -------------------------------------------------------------------------------- /SVGnest/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jack Qiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SVGnest/favicon16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/favicon16.gif -------------------------------------------------------------------------------- /SVGnest/favicon32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/favicon32.gif -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Bold.eot -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Bold.ttf -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Bold.woff -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Bold.woff2 -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-BoldItalic.eot -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-BoldItalic.ttf -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-BoldItalic.woff -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-BoldItalic.woff2 -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Light.eot -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Light.ttf -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Light.woff -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Light.woff2 -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Regular.eot -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Regular.ttf -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Regular.woff -------------------------------------------------------------------------------- /SVGnest/font/fonts/LatoLatin-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/fonts/LatoLatin-Regular.woff2 -------------------------------------------------------------------------------- /SVGnest/font/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"optimal","formats":["ttf","woff","eotz"],"tt_instructor":"default","fix_vertical_metrics":"Y","fix_gasp":"xy","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"basic","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","css_stylesheet":"stylesheet.css","filename_suffix":"-webfont","emsquare":"2048","spacing_adjustment":"0"} -------------------------------------------------------------------------------- /SVGnest/font/lato-hai-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-hai-webfont.eot -------------------------------------------------------------------------------- /SVGnest/font/lato-hai-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-hai-webfont.ttf -------------------------------------------------------------------------------- /SVGnest/font/lato-hai-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-hai-webfont.woff -------------------------------------------------------------------------------- /SVGnest/font/lato-lig-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-lig-webfont.eot -------------------------------------------------------------------------------- /SVGnest/font/lato-lig-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-lig-webfont.ttf -------------------------------------------------------------------------------- /SVGnest/font/lato-lig-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/font/lato-lig-webfont.woff -------------------------------------------------------------------------------- /SVGnest/font/latolatinfonts.css: -------------------------------------------------------------------------------- 1 | /* Webfont: LatoLatin-Bold */@font-face { 2 | font-family: 'LatoLatinWeb'; 3 | src: url('fonts/LatoLatin-Bold.eot'); /* IE9 Compat Modes */ 4 | src: url('fonts/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('fonts/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */ 6 | url('fonts/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */ 7 | url('fonts/LatoLatin-Bold.ttf') format('truetype'); 8 | font-style: normal; 9 | font-weight: bold; 10 | text-rendering: optimizeLegibility; 11 | } 12 | 13 | /* Webfont: LatoLatin-Regular */@font-face { 14 | font-family: 'LatoLatinWeb'; 15 | src: url('fonts/LatoLatin-Regular.eot'); /* IE9 Compat Modes */ 16 | src: url('fonts/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 17 | url('fonts/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */ 18 | url('fonts/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */ 19 | url('fonts/LatoLatin-Regular.ttf') format('truetype'); 20 | font-style: normal; 21 | font-weight: normal; 22 | text-rendering: optimizeLegibility; 23 | } 24 | 25 | /* Webfont: LatoLatin-Light */@font-face { 26 | font-family: 'LatoLatinWebLight'; 27 | src: url('fonts/LatoLatin-Light.eot'); /* IE9 Compat Modes */ 28 | src: url('fonts/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 29 | url('fonts/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */ 30 | url('fonts/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */ 31 | url('fonts/LatoLatin-Light.ttf') format('truetype'); 32 | font-style: normal; 33 | font-weight: normal; 34 | text-rendering: optimizeLegibility; 35 | } -------------------------------------------------------------------------------- /SVGnest/font/specimen_files/easytabs.js: -------------------------------------------------------------------------------- 1 | (function($){$.fn.easyTabs=function(option){var param=jQuery.extend({fadeSpeed:"fast",defaultContent:1,activeClass:'active'},option);$(this).each(function(){var thisId="#"+this.id;if(param.defaultContent==''){param.defaultContent=1;} 2 | if(typeof param.defaultContent=="number") 3 | {var defaultTab=$(thisId+" .tabs li:eq("+(param.defaultContent-1)+") a").attr('href').substr(1);}else{var defaultTab=param.defaultContent;} 4 | $(thisId+" .tabs li a").each(function(){var tabToHide=$(this).attr('href').substr(1);$("#"+tabToHide).addClass('easytabs-tab-content');});hideAll();changeContent(defaultTab);function hideAll(){$(thisId+" .easytabs-tab-content").hide();} 5 | function changeContent(tabId){hideAll();$(thisId+" .tabs li").removeClass(param.activeClass);$(thisId+" .tabs li a[href=#"+tabId+"]").closest('li').addClass(param.activeClass);if(param.fadeSpeed!="none") 6 | {$(thisId+" #"+tabId).fadeIn(param.fadeSpeed);}else{$(thisId+" #"+tabId).show();}} 7 | $(thisId+" .tabs li").click(function(){var tabId=$(this).find('a').attr('href').substr(1);changeContent(tabId);return false;});});}})(jQuery); -------------------------------------------------------------------------------- /SVGnest/font/specimen_files/grid_12-825-55-15.css: -------------------------------------------------------------------------------- 1 | /*Notes about grid: 2 | Columns: 12 3 | Grid Width: 825px 4 | Column Width: 55px 5 | Gutter Width: 15px 6 | -------------------------------*/ 7 | 8 | 9 | 10 | .section {margin-bottom: 18px; 11 | } 12 | .section:after {content: ".";display: block;height: 0;clear: both;visibility: hidden;} 13 | .section {*zoom: 1;} 14 | 15 | .section .firstcolumn, 16 | .section .firstcol {margin-left: 0;} 17 | 18 | 19 | /* Border on left hand side of a column. */ 20 | .border { 21 | padding-left: 7px; 22 | margin-left: 7px; 23 | border-left: 1px solid #eee; 24 | } 25 | 26 | /* Border with more whitespace, spans one column. */ 27 | .colborder { 28 | padding-left: 42px; 29 | margin-left: 42px; 30 | border-left: 1px solid #eee; 31 | } 32 | 33 | 34 | 35 | /* The Grid Classes */ 36 | .grid1, .grid1_2cols, .grid1_3cols, .grid1_4cols, .grid2, .grid2_3cols, .grid2_4cols, .grid3, .grid3_2cols, .grid3_4cols, .grid4, .grid4_3cols, .grid5, .grid5_2cols, .grid5_3cols, .grid5_4cols, .grid6, .grid6_4cols, .grid7, .grid7_2cols, .grid7_3cols, .grid7_4cols, .grid8, .grid8_3cols, .grid9, .grid9_2cols, .grid9_4cols, .grid10, .grid10_3cols, .grid10_4cols, .grid11, .grid11_2cols, .grid11_3cols, .grid11_4cols, .grid12 37 | {margin-left: 15px;float: left;display: inline; overflow: hidden;} 38 | 39 | 40 | .width1, .grid1, .span-1 {width: 55px;} 41 | .width1_2cols,.grid1_2cols {width: 20px;} 42 | .width1_3cols,.grid1_3cols {width: 8px;} 43 | .width1_4cols,.grid1_4cols {width: 2px;} 44 | .input_width1 {width: 49px;} 45 | 46 | .width2, .grid2, .span-2 {width: 125px;} 47 | .width2_3cols,.grid2_3cols {width: 31px;} 48 | .width2_4cols,.grid2_4cols {width: 20px;} 49 | .input_width2 {width: 119px;} 50 | 51 | .width3, .grid3, .span-3 {width: 195px;} 52 | .width3_2cols,.grid3_2cols {width: 90px;} 53 | .width3_4cols,.grid3_4cols {width: 37px;} 54 | .input_width3 {width: 189px;} 55 | 56 | .width4, .grid4, .span-4 {width: 265px;} 57 | .width4_3cols,.grid4_3cols {width: 78px;} 58 | .input_width4 {width: 259px;} 59 | 60 | .width5, .grid5, .span-5 {width: 335px;} 61 | .width5_2cols,.grid5_2cols {width: 160px;} 62 | .width5_3cols,.grid5_3cols {width: 101px;} 63 | .width5_4cols,.grid5_4cols {width: 72px;} 64 | .input_width5 {width: 329px;} 65 | 66 | .width6, .grid6, .span-6 {width: 405px;} 67 | .width6_4cols,.grid6_4cols {width: 90px;} 68 | .input_width6 {width: 399px;} 69 | 70 | .width7, .grid7, .span-7 {width: 475px;} 71 | .width7_2cols,.grid7_2cols {width: 230px;} 72 | .width7_3cols,.grid7_3cols {width: 148px;} 73 | .width7_4cols,.grid7_4cols {width: 107px;} 74 | .input_width7 {width: 469px;} 75 | 76 | .width8, .grid8, .span-8 {width: 545px;} 77 | .width8_3cols,.grid8_3cols {width: 171px;} 78 | .input_width8 {width: 539px;} 79 | 80 | .width9, .grid9, .span-9 {width: 615px;} 81 | .width9_2cols,.grid9_2cols {width: 300px;} 82 | .width9_4cols,.grid9_4cols {width: 142px;} 83 | .input_width9 {width: 609px;} 84 | 85 | .width10, .grid10, .span-10 {width: 685px;} 86 | .width10_3cols,.grid10_3cols {width: 218px;} 87 | .width10_4cols,.grid10_4cols {width: 160px;} 88 | .input_width10 {width: 679px;} 89 | 90 | .width11, .grid11, .span-11 {width: 755px;} 91 | .width11_2cols,.grid11_2cols {width: 370px;} 92 | .width11_3cols,.grid11_3cols {width: 241px;} 93 | .width11_4cols,.grid11_4cols {width: 177px;} 94 | .input_width11 {width: 749px;} 95 | 96 | .width12, .grid12, .span-12 {width: 825px;} 97 | .input_width12 {width: 819px;} 98 | 99 | /* Subdivided grid spaces */ 100 | .emptycols_left1, .prepend-1 {padding-left: 70px;} 101 | .emptycols_right1, .append-1 {padding-right: 70px;} 102 | .emptycols_left2, .prepend-2 {padding-left: 140px;} 103 | .emptycols_right2, .append-2 {padding-right: 140px;} 104 | .emptycols_left3, .prepend-3 {padding-left: 210px;} 105 | .emptycols_right3, .append-3 {padding-right: 210px;} 106 | .emptycols_left4, .prepend-4 {padding-left: 280px;} 107 | .emptycols_right4, .append-4 {padding-right: 280px;} 108 | .emptycols_left5, .prepend-5 {padding-left: 350px;} 109 | .emptycols_right5, .append-5 {padding-right: 350px;} 110 | .emptycols_left6, .prepend-6 {padding-left: 420px;} 111 | .emptycols_right6, .append-6 {padding-right: 420px;} 112 | .emptycols_left7, .prepend-7 {padding-left: 490px;} 113 | .emptycols_right7, .append-7 {padding-right: 490px;} 114 | .emptycols_left8, .prepend-8 {padding-left: 560px;} 115 | .emptycols_right8, .append-8 {padding-right: 560px;} 116 | .emptycols_left9, .prepend-9 {padding-left: 630px;} 117 | .emptycols_right9, .append-9 {padding-right: 630px;} 118 | .emptycols_left10, .prepend-10 {padding-left: 700px;} 119 | .emptycols_right10, .append-10 {padding-right: 700px;} 120 | .emptycols_left11, .prepend-11 {padding-left: 770px;} 121 | .emptycols_right11, .append-11 {padding-right: 770px;} 122 | .pull-1 {margin-left: -70px;} 123 | .push-1 {margin-right: -70px;margin-left: 18px;float: right;} 124 | .pull-2 {margin-left: -140px;} 125 | .push-2 {margin-right: -140px;margin-left: 18px;float: right;} 126 | .pull-3 {margin-left: -210px;} 127 | .push-3 {margin-right: -210px;margin-left: 18px;float: right;} 128 | .pull-4 {margin-left: -280px;} 129 | .push-4 {margin-right: -280px;margin-left: 18px;float: right;} -------------------------------------------------------------------------------- /SVGnest/font/specimen_files/specimen_stylesheet.css: -------------------------------------------------------------------------------- 1 | @import url('grid_12-825-55-15.css'); 2 | 3 | /* 4 | CSS Reset by Eric Meyer - Released under Public Domain 5 | http://meyerweb.com/eric/tools/css/reset/ 6 | */ 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, font, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, sup, tt, var, 12 | b, u, i, center, dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, table, 14 | caption, tbody, tfoot, thead, tr, th, td 15 | {margin: 0;padding: 0;border: 0;outline: 0; 16 | font-size: 100%;vertical-align: baseline; 17 | background: transparent;} 18 | body {line-height: 1;} 19 | ol, ul {list-style: none;} 20 | blockquote, q {quotes: none;} 21 | blockquote:before, blockquote:after, 22 | q:before, q:after {content: ''; content: none;} 23 | :focus {outline: 0;} 24 | ins {text-decoration: none;} 25 | del {text-decoration: line-through;} 26 | table {border-collapse: collapse;border-spacing: 0;} 27 | 28 | 29 | 30 | 31 | body { 32 | color: #000; 33 | background-color: #dcdcdc; 34 | } 35 | 36 | a { 37 | text-decoration: none; 38 | color: #1883ba; 39 | } 40 | 41 | h1{ 42 | font-size: 32px; 43 | font-weight: normal; 44 | font-style: normal; 45 | margin-bottom: 18px; 46 | } 47 | 48 | h2{ 49 | font-size: 18px; 50 | } 51 | 52 | #container { 53 | width: 865px; 54 | margin: 0px auto; 55 | } 56 | 57 | 58 | #header { 59 | padding: 20px; 60 | font-size: 36px; 61 | background-color: #000; 62 | color: #fff; 63 | } 64 | 65 | #header span { 66 | color: #666; 67 | } 68 | #main_content { 69 | background-color: #fff; 70 | padding: 60px 20px 20px; 71 | } 72 | 73 | 74 | #footer p { 75 | margin: 0; 76 | padding-top: 10px; 77 | padding-bottom: 50px; 78 | color: #333; 79 | font: 10px Arial, sans-serif; 80 | } 81 | 82 | .tabs { 83 | width: 100%; 84 | height: 31px; 85 | background-color: #444; 86 | } 87 | .tabs li { 88 | float: left; 89 | margin: 0; 90 | overflow: hidden; 91 | background-color: #444; 92 | } 93 | .tabs li a { 94 | display: block; 95 | color: #fff; 96 | text-decoration: none; 97 | font: bold 11px/11px 'Arial'; 98 | text-transform: uppercase; 99 | padding: 10px 15px; 100 | border-right: 1px solid #fff; 101 | } 102 | 103 | .tabs li a:hover { 104 | background-color: #00b3ff; 105 | 106 | } 107 | 108 | .tabs li.active a { 109 | color: #000; 110 | background-color: #fff; 111 | } 112 | 113 | 114 | 115 | div.huge { 116 | 117 | font-size: 300px; 118 | line-height: 1em; 119 | padding: 0; 120 | letter-spacing: -.02em; 121 | overflow: hidden; 122 | } 123 | div.glyph_range { 124 | font-size: 72px; 125 | line-height: 1.1em; 126 | } 127 | 128 | .size10{ font-size: 10px; } 129 | .size11{ font-size: 11px; } 130 | .size12{ font-size: 12px; } 131 | .size13{ font-size: 13px; } 132 | .size14{ font-size: 14px; } 133 | .size16{ font-size: 16px; } 134 | .size18{ font-size: 18px; } 135 | .size20{ font-size: 20px; } 136 | .size24{ font-size: 24px; } 137 | .size30{ font-size: 30px; } 138 | .size36{ font-size: 36px; } 139 | .size48{ font-size: 48px; } 140 | .size60{ font-size: 60px; } 141 | .size72{ font-size: 72px; } 142 | .size90{ font-size: 90px; } 143 | 144 | 145 | .psample_row1 { height: 120px;} 146 | .psample_row1 { height: 120px;} 147 | .psample_row2 { height: 160px;} 148 | .psample_row3 { height: 160px;} 149 | .psample_row4 { height: 160px;} 150 | 151 | .psample { 152 | overflow: hidden; 153 | position: relative; 154 | } 155 | .psample p { 156 | line-height: 1.3em; 157 | display: block; 158 | overflow: hidden; 159 | margin: 0; 160 | } 161 | 162 | .psample span { 163 | margin-right: .5em; 164 | } 165 | 166 | .white_blend { 167 | width: 100%; 168 | height: 61px; 169 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAO1JREFUeNrs3TsKgFAMRUE/eer+NxztxMYuEWQG3ECKwwUF58ycAKixOAGAyAKILAAiCyCyACILgMgCiCyAyAIgsgAiCyCyAIgsgMgCiCwAIgsgsgAiC4DIAogsACIL0CWuZ3UGgLrIhjMA1EV2OAOAJQtgyQLwjOzmDAAiCyCyAIgsQFtkd2cAEFkAkQVAZAHaIns4A4AlC2DJAiCyACILILIAiCzAV5H1dQGAJQsgsgCILIDIAvwisl58AViyAJYsACILILIAIgvAe2T9EhxAZAFEFgCRBeiL7HAGgLrIhjMAWLIAliwAt1OAAQDwygTBulLIlQAAAABJRU5ErkJggg==); 170 | position: absolute; 171 | bottom: 0; 172 | } 173 | .black_blend { 174 | width: 100%; 175 | height: 61px; 176 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPJJREFUeNrs3TEKhTAQRVGjibr/9QoxhY2N3Ywo50A28IrLwP9g6b1PAMSYTQAgsgAiC4DIAogsgMgCILIAIgsgsgCILIDIAogsACILILIAIguAyAKILIDIAiCyACILgMgCZCnjLWYAiFGvB0BQZJsZAFyyAC5ZAO6RXc0AILIAIguAyAKkRXYzA4DIAogsACILkBbZ3QwALlkAlywAIgsgsgAiC4DIArwVWf8uAHDJAogsACILILIAv4isH74AXLIALlkARBZAZAFEFoDnyPokOIDIAogsACILkBfZZgaAuMhWMwC4ZAE+p4x3mAEgxinAAJ+XBbPWGkwAAAAAAElFTkSuQmCC); 177 | position: absolute; 178 | bottom: 0; 179 | } 180 | .fullreverse { 181 | background: #000 !important; 182 | color: #fff !important; 183 | margin-left: -20px; 184 | padding-left: 20px; 185 | margin-right: -20px; 186 | padding-right: 20px; 187 | padding: 20px; 188 | margin-bottom:0; 189 | } 190 | 191 | 192 | .sample_table td { 193 | padding-top: 3px; 194 | padding-bottom:5px; 195 | padding-left: 5px; 196 | vertical-align: middle; 197 | line-height: 1.2em; 198 | } 199 | 200 | .sample_table td:first-child { 201 | background-color: #eee; 202 | text-align: right; 203 | padding-right: 5px; 204 | padding-left: 0; 205 | padding: 5px; 206 | font: 11px/12px "Courier New", Courier, mono; 207 | } 208 | 209 | code { 210 | white-space: pre; 211 | background-color: #eee; 212 | display: block; 213 | padding: 10px; 214 | margin-bottom: 18px; 215 | overflow: auto; 216 | } 217 | 218 | 219 | .bottom,.last {margin-bottom:0 !important; padding-bottom:0 !important;} 220 | 221 | .box { 222 | padding: 18px; 223 | margin-bottom: 18px; 224 | background: #eee; 225 | } 226 | 227 | .reverse,.reversed { background: #000 !important;color: #fff !important; border: none !important;} 228 | 229 | #bodycomparison { 230 | position: relative; 231 | overflow: hidden; 232 | font-size: 72px; 233 | height: 90px; 234 | white-space: nowrap; 235 | } 236 | 237 | #bodycomparison div{ 238 | font-size: 72px; 239 | line-height: 90px; 240 | display: inline; 241 | margin: 0 15px 0 0; 242 | padding: 0; 243 | } 244 | 245 | #bodycomparison div span{ 246 | font: 10px Arial; 247 | position: absolute; 248 | left: 0; 249 | } 250 | #xheight { 251 | float: none; 252 | position: absolute; 253 | color: #d9f3ff; 254 | font-size: 72px; 255 | line-height: 90px; 256 | } 257 | 258 | .fontbody { 259 | position: relative; 260 | } 261 | .arialbody{ 262 | font-family: Arial; 263 | position: relative; 264 | } 265 | .verdanabody{ 266 | font-family: Verdana; 267 | position: relative; 268 | } 269 | .georgiabody{ 270 | font-family: Georgia; 271 | position: relative; 272 | } 273 | 274 | /* @group Layout page 275 | */ 276 | 277 | #layout h1 { 278 | font-size: 36px; 279 | line-height: 42px; 280 | font-weight: normal; 281 | font-style: normal; 282 | } 283 | 284 | #layout h2 { 285 | font-size: 24px; 286 | line-height: 23px; 287 | font-weight: normal; 288 | font-style: normal; 289 | } 290 | 291 | #layout h3 { 292 | font-size: 22px; 293 | line-height: 1.4em; 294 | margin-top: 1em; 295 | font-weight: normal; 296 | font-style: normal; 297 | } 298 | 299 | 300 | #layout p.byline { 301 | font-size: 12px; 302 | margin-top: 18px; 303 | line-height: 12px; 304 | margin-bottom: 0; 305 | } 306 | #layout p { 307 | font-size: 14px; 308 | line-height: 21px; 309 | margin-bottom: .5em; 310 | } 311 | 312 | #layout p.large{ 313 | font-size: 18px; 314 | line-height: 26px; 315 | } 316 | 317 | #layout .sidebar p{ 318 | font-size: 12px; 319 | line-height: 1.4em; 320 | } 321 | 322 | #layout p.caption { 323 | font-size: 10px; 324 | margin-top: -16px; 325 | margin-bottom: 18px; 326 | } 327 | 328 | /* @end */ 329 | 330 | /* @group Glyphs */ 331 | 332 | #glyph_chart div{ 333 | background-color: #d9f3ff; 334 | color: black; 335 | float: left; 336 | font-size: 36px; 337 | height: 1.2em; 338 | line-height: 1.2em; 339 | margin-bottom: 1px; 340 | margin-right: 1px; 341 | text-align: center; 342 | width: 1.2em; 343 | position: relative; 344 | padding: .6em .2em .2em; 345 | } 346 | 347 | #glyph_chart div p { 348 | position: absolute; 349 | left: 0; 350 | top: 0; 351 | display: block; 352 | text-align: center; 353 | font: bold 9px Arial, sans-serif; 354 | background-color: #3a768f; 355 | width: 100%; 356 | color: #fff; 357 | padding: 2px 0; 358 | } 359 | 360 | 361 | #glyphs h1 { 362 | font-family: Arial, sans-serif; 363 | } 364 | /* @end */ 365 | 366 | /* @group Installing */ 367 | 368 | #installing { 369 | font: 13px Arial, sans-serif; 370 | } 371 | 372 | #installing p, 373 | #glyphs p{ 374 | line-height: 1.2em; 375 | margin-bottom: 18px; 376 | font: 13px Arial, sans-serif; 377 | } 378 | 379 | 380 | 381 | #installing h3{ 382 | font-size: 15px; 383 | margin-top: 18px; 384 | } 385 | 386 | /* @end */ 387 | 388 | #rendering h1 { 389 | font-family: Arial, sans-serif; 390 | } 391 | .render_table td { 392 | font: 11px "Courier New", Courier, mono; 393 | vertical-align: middle; 394 | } 395 | 396 | 397 | -------------------------------------------------------------------------------- /SVGnest/font/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (http://www.fontsquirrel.com) on July 12, 2014 */ 2 | 3 | 4 | 5 | @font-face { 6 | font-family: 'latohairline'; 7 | src: url('lato-hai-webfont.eot'); 8 | src: url('lato-hai-webfont.eot?#iefix') format('embedded-opentype'), 9 | url('lato-hai-webfont.woff') format('woff'), 10 | url('lato-hai-webfont.ttf') format('truetype'), 11 | url('lato-hai-webfont.svg#latohairline') format('svg'); 12 | font-weight: normal; 13 | font-style: normal; 14 | 15 | } 16 | 17 | 18 | 19 | 20 | @font-face { 21 | font-family: 'latolight'; 22 | src: url('lato-lig-webfont.eot'); 23 | src: url('lato-lig-webfont.eot?#iefix') format('embedded-opentype'), 24 | url('lato-lig-webfont.woff') format('woff'), 25 | url('lato-lig-webfont.ttf') format('truetype'), 26 | url('lato-lig-webfont.svg#latolight') format('svg'); 27 | font-weight: normal; 28 | font-style: normal; 29 | 30 | } -------------------------------------------------------------------------------- /SVGnest/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/SVGnest/img/background.png -------------------------------------------------------------------------------- /SVGnest/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /SVGnest/img/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /SVGnest/img/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SVGnest/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 26 | 27 | 28 | 31 | 32 | 33 | 35 | 36 | 37 | 39 | 40 | 41 | 43 | 44 | 45 | 47 | 48 | 49 | 51 | 52 | 53 | 55 | 56 | 57 | 60 | 61 | 62 | 65 | 66 | 67 | 69 | 70 | 71 | 73 | 74 | 75 | 77 | 78 | 79 | 81 | 82 | 83 | 85 | 86 | 87 | 89 | 90 | 91 | 94 | 95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 109 | 110 | 111 | 114 | 115 | 116 | 119 | 120 | 121 | 124 | 125 | 126 | 129 | 130 | 131 | 133 | 134 | 135 | 138 | 139 | 140 | 142 | 143 | 144 | 147 | 148 | 149 | 151 | 152 | 153 | 156 | 157 | 158 | 161 | 162 | 163 | 166 | 167 | 168 | 171 | 172 | 173 | 176 | 177 | 178 | 181 | 182 | 183 | 186 | 187 | 188 | 190 | 191 | 192 | 195 | 196 | 197 | 200 | 201 | 202 | 205 | 206 | 207 | 210 | 211 | 212 | 213 | 214 | 217 | 218 | 219 | 221 | 222 | 223 | 225 | 226 | 227 | 230 | 231 | 232 | 234 | 235 | 236 | 239 | 240 | 241 | 244 | 245 | 246 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /SVGnest/img/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SVGnest/img/spin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SVGnest/img/start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SVGnest/img/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SVGnest/img/zoomin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /SVGnest/img/zoomout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SVGnest/readme.md: -------------------------------------------------------------------------------- 1 | # ![SVGNest](http://svgnest.com/github/logo2.png) 2 | 3 | **SVGNest**: A browser-based vector nesting tool. 4 | 5 | **Demo:** http://svgnest.com 6 | 7 | (requires SVG and webworker support). Mobile warning: running the demo is CPU intensive. 8 | 9 | references (PDF): 10 | - [López-Camacho *et al.* 2013](http://www.cs.stir.ac.uk/~goc/papers/EffectiveHueristic2DAOR2013.pdf) 11 | - [Kendall 2000](http://www.graham-kendall.com/papers/k2001.pdf) 12 | - [E.K. Burke *et al.* 2006](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.440.379&rep=rep1&type=pdf) 13 | 14 | ## What is "nesting"? 15 | 16 | Given a square piece of material and some letters to be laser-cut: 17 | 18 | ![letter nesting](http://svgnest.com/github/letters.png) 19 | 20 | We want to pack all the letters into the square, using as little material as possible. If a single square is not enough, we also want to minimize the number of squares used. 21 | 22 | In the CNC world this is called "[nesting](http://sigmanest.com/)", and [software](http://www.mynesting.com/) that [does this](http://www.autodesk.com/products/trunest/overview) is typically targeted at [industrial customers](http://www.hypertherm.com/en/Products/Automated_cutting/Nesting_software/) and [very expensive](http://www.nestfab.com/pricing/). 23 | 24 | SVGnest is a free and open-source alternative that solves this problem with the orbital approach outlined in [E.K. Burke *et al.* 2006], using a genetic algorithm for global optimization. It works for arbitrary containers and concave edge cases, and performs on-par with existing commercial software. 25 | 26 | ![non-rectangular shapes](http://svgnest.com/github/shapes.png) 27 | 28 | It also features part-in-part support, for placing parts in the holes of other parts. 29 | 30 | ![non-rectangular shapes](http://svgnest.com/github/recursion.png) 31 | 32 | ## Usage 33 | 34 | Make sure all parts have been converted to outlines, and that no outlines overlap. Upload the SVG file and select one of the outlines to be used as the bin. 35 | 36 | All other outlines are automatically processed as parts for nesting. 37 | 38 | ## Outline of algorithm 39 | 40 | While [good heuristics](http://cgi.csc.liv.ac.uk/~epa/surveyhtml.html) exist for the rectangular bin packing problem, in the real world we are concerned with irregular shapes. 41 | 42 | The strategy is made of two parts: 43 | 44 | - the placement strategy (ie. how do I insert each part into a bin?) 45 | - and the optimization strategy (ie. what's the best order of insertions?) 46 | 47 | ### Placing the part 48 | 49 | The key concept here is the "No Fit Polygon". 50 | 51 | Given polygons A and B, we want to "orbit" B around A such that they always touch but do not intersect. 52 | 53 | ![No Fit Polygon example](http://svgnest.com/github/nfp.png) 54 | 55 | The resulting orbit is the NFP. The NFP contains all possible placements of B that touches the previously placed parts. We can then choose a point on the NFP as the placement position using some heuristics. 56 | 57 | Similarly we can construct an "Inner Fit Polygon" for the part and the bin. This is the same as the NFP, except the orbiting polygon is inside the stationary one. 58 | 59 | When two or more parts have already been placed, we can take the union of the NFPs of the previously placed parts. 60 | 61 | ![No Fit Polygon example](http://svgnest.com/github/nfp2.png) 62 | 63 | This means that we need to compute O(nlogn) NFPs to complete the first packing. While there are ways to mitigate this, we take the brute-force approach which has good properties for the optimization algo. 64 | 65 | ### Optimization 66 | 67 | Now that we can place the parts, we need to optimize the insertion order. Here's an example of a bad insertion order: 68 | 69 | ![Bad insertion order](http://svgnest.com/github/badnest.png) 70 | 71 | If the large "C" is placed last, the concave space inside it won't be utilized because all the parts that could have filled it have already been placed. 72 | 73 | To solve this, we use the "first-fit-decreasing" heuristic. Larger parts are placed first, and smaller parts last. This is quite intuitive, as the smaller parts tend to act as "sand" to fill the gaps left by the larger parts. 74 | 75 | ![Good insertion order](http://svgnest.com/github/goodnest.png) 76 | 77 | While this strategy gives us a good start, we want to explore more of the solution space. We could simply randomize the insertion order, but we can probably do better with a genetic algorithm. (If you don't know what a GA is, [this article](http://www.ai-junkie.com/ga/intro/gat1.html) is a very approachable read) 78 | 79 | ## Evaluating fitness 80 | 81 | In our GA the insertion order and the rotation of the parts form the gene. The fitness function follows these rules: 82 | 83 | 1. Minimize the number of unplaceable parts (parts that cannot fit any bin due to its rotation) 84 | 2. Minimize the number of bins used 85 | 3. Minimize the *width* of all placed parts 86 | 87 | The third one is rather arbitrary, as we can also optimize for rectangular bounds or a minimal concave hull. In real-world use the material to be cut tends to be rectangular, and those options tend to result in long slivers of un-used material. 88 | 89 | Because small mutations in the gene cause potentially large changes in overall fitness, the individuals of the population can be very similar. By caching NFPs new individuals can be evaluated very quickly. 90 | 91 | ## Performance 92 | 93 | ![SVGnest comparison](http://svgnest.com/github/comparison1.png) 94 | 95 | Performs similarly to commercial software, after both have run for about 5 minutes. 96 | 97 | ## Configuration parameters 98 | 99 | - **Space between parts:** Minimum space between parts (eg. for laser kerf, CNC offset etc.) 100 | - **Curve tolerance:** The maximum error allowed for linear approximations of Bezier paths and arcs, in SVG units or "pixels". Decrease this value if curved parts appear to slightly overlap. 101 | - **Part rotations:** The *possible* number of rotations to evaluate for each part. eg. 4 for only the cardinal directions. Larger values may improve results, but will be slower to converge. 102 | - **GA population:** The population size for the Genetic Algorithm 103 | - **GA mutation rate:** The probability of mutation for each gene or part placement. Values from 1-50 104 | - **Part in part:** When enabled, places parts in the holes of other parts. This is off by default as it can be resource intensive 105 | - **Explore concave areas:** When enabled, solves the concave edge case at a cost of some performance and placement robustness: 106 | 107 | ![Concave flag example](http://svgnest.com/github/concave.png) 108 | 109 | ## To-do 110 | 111 | - ~~Recursive placement (putting parts in holes of other parts)~~ 112 | - Customize fitness function (gravity direction, etc) 113 | - kill worker threads when stop button is clicked 114 | - fix certain edge cases in NFP generation -------------------------------------------------------------------------------- /SVGnest/style.css: -------------------------------------------------------------------------------- 1 | body, html{ 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | font: normal 22px/1.4 'LatoLatinWeb', helvetica, arial, verdana, sans-serif; 6 | background-color: #fff; 7 | color: #8b8b8b; 8 | } 9 | 10 | a{ 11 | color: #3bb34a; 12 | text-decoration: none; 13 | } 14 | 15 | a:hover{ 16 | color: #55c960; 17 | text-decoration: underline; 18 | } 19 | 20 | h1{ 21 | font-size: 1.5em; 22 | font-family: 'LatoLatinWebLight', helvetica, arial, verdana, sans-serif; 23 | font-weight: normal; 24 | margin: 1.5em 0 0.5em 0; 25 | color: #617bb5; 26 | } 27 | 28 | h2{ 29 | font-size: 1.1em; 30 | font-weight: bold; 31 | margin: 0 0 0.5em 0; 32 | color: #8498d1; 33 | } 34 | 35 | h3{ 36 | font-size: 1em; 37 | font-weight: bold; 38 | margin: 1em 0 0.2em 0; 39 | color: #8498d1; 40 | } 41 | 42 | #splash{ 43 | width: 28em; 44 | margin: 1% auto 0 auto; 45 | } 46 | 47 | #splash .logo{ 48 | width: 50%; 49 | margin: 0; 50 | margin-left: 25%; 51 | height: auto; 52 | } 53 | 54 | #splash h1{ 55 | color: #37b34a; 56 | } 57 | 58 | #splash h1.title{ 59 | font-size: 3.5em; 60 | margin: 0; 61 | padding: 0; 62 | text-align: center; 63 | } 64 | 65 | .subscript{ 66 | font-size: 0.75em; 67 | } 68 | 69 | #splash .subscript{ 70 | display: block; 71 | color: #3bb34a; 72 | font-size: 1.45em; 73 | text-align: center; 74 | font-style: normal; 75 | 76 | } 77 | 78 | .nav{ 79 | margin: 0; 80 | padding: 0; 81 | } 82 | 83 | li{ 84 | list-style: none; 85 | float: left; 86 | margin: 0; 87 | padding: 0; 88 | } 89 | 90 | .button{ 91 | display: block; 92 | margin: 1.5em 0.5em 0em 0.5em; 93 | padding: 0.6em 2.4em; 94 | background-color: #fff; 95 | border-radius: 5em; 96 | border: 2px solid #d7e9b7; 97 | cursor: pointer; 98 | color: #3bb34a; 99 | } 100 | 101 | .button a:hover{ 102 | text-decoration: none; 103 | } 104 | 105 | .button.start{ 106 | background: #fff url(img/start.svg) no-repeat; 107 | background-size: 1.4em 1.4em; 108 | background-position: 1.8em 50%; 109 | padding-left: 3.7em; 110 | } 111 | 112 | .button.spinner{ 113 | background: #fff url(img/spin.svg) no-repeat; 114 | background-size: 1.4em 1.4em; 115 | background-position: 1.8em 50%; 116 | padding-left: 3.7em; 117 | } 118 | 119 | .button.upload{ 120 | background: #fff url(img/spin.svg) no-repeat; 121 | background-size: 1.4em 1.4em; 122 | background-position: 1.8em 50%; 123 | padding-left: 3.7em; 124 | float: none 125 | } 126 | 127 | .button.download{ 128 | background: #fff url(img/download.svg) no-repeat; 129 | background-size: 1em 1em; 130 | background-position: 2.2em 50%; 131 | padding-left: 4em; 132 | } 133 | 134 | .button.code{ 135 | background: #fff url(img/code.svg) no-repeat; 136 | background-size: 1.2em 1.2em; 137 | background-position: 2em 50%; 138 | padding-left: 3.9em; 139 | } 140 | 141 | .button.config{ 142 | background: #fff url(img/settings.svg) no-repeat; 143 | background-size: 1.2em 1.2em; 144 | background-position: 2em 50%; 145 | padding-left: 3.9em; 146 | } 147 | 148 | .button.close{ 149 | background: #fff url(img/close.svg) no-repeat; 150 | background-size: 2em 2em; 151 | background-position: 1.8em 50%; 152 | padding-left: 3.9em; 153 | } 154 | 155 | .button.zoomin{ 156 | background: #fff url(img/zoomin.svg) no-repeat; 157 | background-size: 1.5em 1.5em; 158 | } 159 | 160 | .button.zoomout{ 161 | background: #fff url(img/zoomout.svg) no-repeat; 162 | background-size: 1.5em 1.5em; 163 | } 164 | 165 | .button.exit{ 166 | background: #fff url(img/close.svg) no-repeat; 167 | background-size: 1.5em 1.5em; 168 | } 169 | 170 | .button:hover{ 171 | color: #55c960; 172 | box-shadow: 0 2px 1px #d7dae1; 173 | text-decoration: none; 174 | } 175 | 176 | .button:active{ 177 | background-color: #dddde3; 178 | box-shadow: inset 0 2px 2px #d0d2da; 179 | } 180 | 181 | .button.disabled{ 182 | cursor: default; 183 | opacity: 0.5; 184 | color: #999; 185 | -webkit-filter: saturate(0); 186 | filter: saturate(0); 187 | } 188 | 189 | .button.disabled:hover{ 190 | box-shadow: none; 191 | } 192 | 193 | .button.disabled:active{ 194 | background-color: #fff; 195 | box-shadow: none; 196 | } 197 | 198 | #splash .nav{ 199 | margin: 2em 0 0 0; 200 | } 201 | 202 | #faq{ 203 | display: none; 204 | float: left; 205 | margin-top: 2em; 206 | padding-bottom: 5em; 207 | } 208 | 209 | /* svgnest styles */ 210 | 211 | #svgnest, #messagewrapper{ 212 | width: 95vw; 213 | } 214 | 215 | #svgnest{ 216 | display: none; 217 | margin: 0 auto 0 auto; 218 | } 219 | 220 | #svgnest .logo, #svgnest .sidebar{ 221 | float: left; 222 | width: 22%; 223 | margin-right: 8%; 224 | } 225 | 226 | #svgnest .sidebar h1{ 227 | font-size: 3em; 228 | } 229 | 230 | #svgnest .sidebar{ 231 | clear: both; 232 | width: 100%; 233 | margin-top: 1em; 234 | margin-bottom: 1em; 235 | } 236 | 237 | #svgnest .nav{ 238 | float: left; 239 | margin: 0 0 0 -0.5em; 240 | padding: 0; 241 | } 242 | 243 | #controls{ 244 | margin-top: 1em; 245 | float: left; 246 | position: relative; 247 | } 248 | 249 | /* info sidebar */ 250 | 251 | #info, #info_placement{ 252 | display: none; 253 | } 254 | 255 | h1.label{ 256 | font-size: 4em; 257 | margin: 0.2em 0 0 0; 258 | padding: 0; 259 | line-height: 1; 260 | font-weight: normal; 261 | } 262 | 263 | h1.label sup{ 264 | font-size: 0.5em; 265 | } 266 | 267 | .column{ 268 | margin: 0em 2em 0em 2em; 269 | float: left; 270 | } 271 | 272 | 273 | .progress{ 274 | width: 51%; 275 | clear: both; 276 | height: 1.2em; 277 | background-color: #fff; 278 | border: 2px solid #617bb5; 279 | border-radius: 1em; 280 | margin-bottom: 0.4em; 281 | } 282 | 283 | .progress_inner{ 284 | height: 100%; 285 | background-color: #617bb5; 286 | border-radius: 1em; 287 | } 288 | 289 | #config{ 290 | max-height: 0; 291 | overflow: hidden; 292 | width: 20em; 293 | position: absolute; 294 | top: 0; 295 | left: 24.5em; 296 | background-color: #fff; 297 | border-radius: 0.5em; 298 | transition: max-height 0.5s; 299 | } 300 | 301 | #configwrapper{ 302 | float: left; 303 | padding: 3em 0 1em 2em; 304 | } 305 | 306 | #config.active{ 307 | display: block; 308 | max-height: 50em; 309 | box-shadow: 0 2px 1px #d7dae1; 310 | } 311 | 312 | #configbutton{ 313 | position: relative; 314 | z-index: 2; 315 | width: 3em; 316 | padding: 0; 317 | height: 2.5em; 318 | background-position: 50%; 319 | } 320 | 321 | #zoominbutton, #zoomoutbutton, #exitbutton{ 322 | width: 3em; 323 | padding: 0; 324 | height: 2.5em; 325 | background-position: 50%; 326 | } 327 | 328 | #configbutton.close:hover{ 329 | box-shadow: none; 330 | } 331 | 332 | #configsave{ 333 | margin-left: 7%; 334 | } 335 | 336 | #config input, #config h3, #config .tooltip{ 337 | margin: 1em 0 0 0; 338 | height: 2em; 339 | padding: 0; 340 | } 341 | 342 | #config input{ 343 | float: left; 344 | width: 13%; 345 | font-size: 1em; 346 | border: 2px solid #8aba5a; 347 | color: #fff; 348 | color: #8aba5a; 349 | text-align: center; 350 | clear: left; 351 | border-radius: 0.4em; 352 | } 353 | 354 | #config input:hover{ 355 | background-color: #ededf0; 356 | } 357 | 358 | #config input.checkbox{ 359 | width: 7%; 360 | margin-left: 4%; 361 | margin-right: 4%; 362 | border: 1px solid #f00; 363 | } 364 | 365 | #config h3{ 366 | float: left; 367 | width: 65%; 368 | margin-left: 5%; 369 | padding: 0; 370 | font-size: 0.8em; 371 | line-height: 3em; 372 | } 373 | 374 | #config .tooltip{ 375 | float: left; 376 | max-width: 15%; 377 | width: 1.5em; 378 | height: 1.5em; 379 | font-size: 0.8em; 380 | font-weight: bold; 381 | background-color: #fff; 382 | background-color: #8aba5a; 383 | color: #fff; 384 | text-align: center; 385 | line-height: 1.5; 386 | margin-top: 1.8em; 387 | cursor: default; 388 | border-radius: 3em; 389 | } 390 | 391 | #config .button{ 392 | float: left; 393 | clear: both; 394 | margin-top: 2em; 395 | } 396 | 397 | /* svg styles*/ 398 | 399 | #select{ 400 | margin-top: 2em; 401 | } 402 | 403 | #select, #bins{ 404 | float: left; 405 | width: 69%; 406 | position: relative; 407 | } 408 | 409 | #select svg, #bins svg{ 410 | width: 100%; 411 | height: auto; 412 | position: absolute; 413 | top: 0; 414 | margin: 0; 415 | display: block; 416 | overflow: visible; 417 | pointer-events: none; 418 | } 419 | 420 | #select svg *{ 421 | fill: #fff !important; 422 | fill-opacity: 0 !important; 423 | stroke: #3bb34a !important; 424 | stroke-width: 2px !important; 425 | vector-effect: non-scaling-stroke !important; 426 | stroke-linejoin: round !important; 427 | pointer-events: fill; 428 | } 429 | 430 | #select svg *.fullRect{ 431 | fill: #eee !important; 432 | fill-opacity: 1 !important; 433 | stroke: #eee !important; 434 | stroke-width: 2px !important; 435 | vector-effect: non-scaling-stroke !important; 436 | stroke-linejoin: round !important; 437 | } 438 | 439 | #select svg *:hover{ 440 | stroke: #075911 !important; 441 | cursor: pointer !important; 442 | } 443 | 444 | #select svg *.active{ 445 | stroke: #06380c !important; 446 | stroke-width: 3px !important; 447 | } 448 | 449 | #select.disabled svg *, #select.disabled svg *:hover, #select.disabled svg *.active{ 450 | stroke: #9b9da2 !important; 451 | stroke-width: 2px !important; 452 | cursor: default !important; 453 | } 454 | 455 | #bins svg{ 456 | margin-bottom: 2em; 457 | } 458 | 459 | #bins svg.grid{ 460 | float: left; 461 | width: 45%; 462 | margin-right: 5%; 463 | min-width: 20em; 464 | } 465 | 466 | #bins svg *{ 467 | fill: #8498d1 !important; 468 | stroke: #617bb5 !important; 469 | stroke-width: 2px !important; 470 | vector-effect: non-scaling-stroke !important; 471 | stroke-linejoin: round !important; 472 | } 473 | 474 | #bins svg .bin{ 475 | fill: #ffffff !important; 476 | stroke: #8498d1 !important; 477 | } 478 | 479 | #bins svg .hole{ 480 | fill: #ffffff !important; 481 | stroke: #617bb5 !important; 482 | } 483 | 484 | /* messages */ 485 | 486 | #messagewrapper{ 487 | width: 50em; 488 | overflow: hidden; 489 | background: #8498d1 url(img/close.svg) no-repeat; 490 | background-position: 99% 0.5em; 491 | background-size: 3em 3em; 492 | line-height: 4em; 493 | position: fixed; 494 | left: 50%; 495 | margin-left: -25em; 496 | bottom: 1em; 497 | text-align: center; 498 | border-radius: 0.5em; 499 | color: #fff; 500 | } 501 | 502 | #messagewrapper:hover{ 503 | background-color: #a2b4dd; 504 | } 505 | 506 | #message{ 507 | overflow: hidden; 508 | height: 0; 509 | } 510 | 511 | #message.active, #message.error{ 512 | height: 4em; 513 | cursor: pointer; 514 | } 515 | 516 | #message.error{ 517 | color: #ff314e; 518 | font-weight: bold; 519 | } 520 | 521 | 522 | 523 | /* animations taken from animate.css */ 524 | 525 | .animated { 526 | -webkit-animation-duration: 1s; 527 | animation-duration: 1s; 528 | -webkit-animation-fill-mode: both; 529 | animation-fill-mode: both; 530 | } 531 | 532 | @-webkit-keyframes bounce { 533 | from, 20%, 53%, 80%, to { 534 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); 535 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); 536 | -webkit-transform: translate3d(0,0,0); 537 | transform: translate3d(0,0,0); 538 | } 539 | 540 | 40%, 43% { 541 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 542 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 543 | -webkit-transform: translate3d(0, -30px, 0); 544 | transform: translate3d(0, -30px, 0); 545 | } 546 | 547 | 70% { 548 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 549 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 550 | -webkit-transform: translate3d(0, -15px, 0); 551 | transform: translate3d(0, -15px, 0); 552 | } 553 | 554 | 90% { 555 | -webkit-transform: translate3d(0,-4px,0); 556 | transform: translate3d(0,-4px,0); 557 | } 558 | } 559 | 560 | @keyframes bounce { 561 | from, 20%, 53%, 80%, to { 562 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); 563 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); 564 | -webkit-transform: translate3d(0,0,0); 565 | transform: translate3d(0,0,0); 566 | } 567 | 568 | 40%, 43% { 569 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 570 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 571 | -webkit-transform: translate3d(0, -30px, 0); 572 | transform: translate3d(0, -30px, 0); 573 | } 574 | 575 | 70% { 576 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 577 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 578 | -webkit-transform: translate3d(0, -15px, 0); 579 | transform: translate3d(0, -15px, 0); 580 | } 581 | 582 | 90% { 583 | -webkit-transform: translate3d(0,-4px,0); 584 | transform: translate3d(0,-4px,0); 585 | } 586 | } 587 | 588 | .bounce { 589 | -webkit-animation-name: bounce; 590 | animation-name: bounce; 591 | -webkit-transform-origin: center bottom; 592 | transform-origin: center bottom; 593 | } 594 | 595 | @-webkit-keyframes slideInUp { 596 | from { 597 | -webkit-transform: translate3d(0, 100%, 0); 598 | transform: translate3d(0, 100%, 0); 599 | visibility: visible; 600 | } 601 | 602 | to { 603 | -webkit-transform: translate3d(0, 0, 0); 604 | transform: translate3d(0, 0, 0); 605 | } 606 | } 607 | 608 | @keyframes slideInUp { 609 | from { 610 | -webkit-transform: translate3d(0, 100%, 0); 611 | transform: translate3d(0, 100%, 0); 612 | visibility: visible; 613 | } 614 | 615 | to { 616 | -webkit-transform: translate3d(0, 0, 0); 617 | transform: translate3d(0, 0, 0); 618 | } 619 | } 620 | 621 | .slideInUp { 622 | -webkit-animation-name: slideInUp; 623 | animation-name: slideInUp; 624 | } 625 | 626 | @media only screen and (max-width: 1800px) { 627 | body { font-size: 20px; } 628 | #svgnest, #messagewrapper{ 629 | width: 95vw; 630 | } 631 | 632 | .progress{ 633 | width: 61%; 634 | } 635 | } 636 | 637 | @media only screen and (max-width: 1500px) { 638 | body { font-size: 16px; } 639 | #svgnest, #messagewrapper{ 640 | width: 95vw; 641 | } 642 | 643 | #svgnest{ 644 | margin-top: 1em; 645 | } 646 | 647 | #svgnest .logo{ 648 | width: 25%; 649 | } 650 | 651 | #controls{ 652 | margin-top: 1em; 653 | } 654 | 655 | #splash .logo{ 656 | width: 60%; 657 | margin: 0 20%; 658 | } 659 | 660 | h1.label{ 661 | font-size: 3em; 662 | } 663 | 664 | .progress{ 665 | width: 75%; 666 | } 667 | } 668 | 669 | @media only screen and (max-width: 1300px) { 670 | body { font-size: 14px; } 671 | } 672 | 673 | @media only screen and (max-width: 790px) { 674 | #splash{ 675 | width: 100%; 676 | } 677 | 678 | #splash .logo{ 679 | width: 40%; 680 | margin-left: 30%; 681 | float: left; 682 | } 683 | 684 | #splash h1.title{ 685 | margin: 0; 686 | font-size: 2em; 687 | } 688 | 689 | #splash .subscript{ 690 | font-size: 1em; 691 | } 692 | 693 | body { font-size: 18px; } 694 | 695 | #splash .nav{ 696 | width: 60%; 697 | margin-left: 20%; 698 | margin-top: 2em; 699 | } 700 | 701 | #splash .nav li{ 702 | float: none; 703 | display: block; 704 | margin-top: 1em; 705 | } 706 | 707 | #faq{ 708 | padding: 3em; 709 | } 710 | } -------------------------------------------------------------------------------- /SVGnest/svgnest.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * SvgNest 3 | * Licensed under the MIT license 4 | */ 5 | 6 | (function(root){ 7 | 'use strict'; 8 | 9 | root.SvgNest = new SvgNest(); 10 | 11 | function SvgNest(){ 12 | var self = this; 13 | 14 | var svg = null; 15 | 16 | // keep a reference to any style nodes, to maintain color/fill info 17 | this.style = null; 18 | 19 | var parts = null; 20 | 21 | var tree = null; 22 | 23 | 24 | var bin = null; 25 | var binPolygon = null; 26 | var binBounds = null; 27 | var nfpCache = {}; 28 | var config = { 29 | clipperScale: 10000000, 30 | curveTolerance: 0.3, 31 | spacing: 0, 32 | rotations: 4, 33 | populationSize: 10, 34 | mutationRate: 10, 35 | useHoles: false, 36 | exploreConcave: false 37 | }; 38 | 39 | this.working = false; 40 | 41 | var GA = null; 42 | var best = null; 43 | var workerTimer = null; 44 | var progress = 0; 45 | 46 | this.parsesvg = function(svgstring){ 47 | // reset if in progress 48 | this.stop(); 49 | 50 | bin = null; 51 | binPolygon = null; 52 | tree = null; 53 | 54 | // parse svg 55 | svg = SvgParser.load(svgstring); 56 | 57 | this.style = SvgParser.getStyle(); 58 | 59 | svg = SvgParser.clean(); 60 | 61 | tree = this.getParts(svg.childNodes); 62 | 63 | //re-order elements such that deeper elements are on top, so they can be moused over 64 | function zorder(paths){ 65 | // depth-first 66 | var length = paths.length; 67 | for(var i=0; i 0){ 69 | zorder(paths[i].children); 70 | } 71 | } 72 | } 73 | 74 | return svg; 75 | } 76 | 77 | this.setbin = function(element){ 78 | if(!svg){ 79 | return; 80 | } 81 | bin = element; 82 | } 83 | 84 | this.config = function(c){ 85 | // clean up inputs 86 | 87 | if(!c){ 88 | return config; 89 | } 90 | 91 | if(c.curveTolerance && !GeometryUtil.almostEqual(parseFloat(c.curveTolerance), 0)){ 92 | config.curveTolerance = parseFloat(c.curveTolerance); 93 | } 94 | 95 | if('spacing' in c){ 96 | config.spacing = parseFloat(c.spacing); 97 | } 98 | 99 | if(c.rotations && parseInt(c.rotations) > 0){ 100 | config.rotations = parseInt(c.rotations); 101 | } 102 | 103 | if(c.populationSize && parseInt(c.populationSize) > 2){ 104 | config.populationSize = parseInt(c.populationSize); 105 | } 106 | 107 | if(c.mutationRate && parseInt(c.mutationRate) > 0){ 108 | config.mutationRate = parseInt(c.mutationRate); 109 | } 110 | 111 | if('useHoles' in c){ 112 | config.useHoles = c.useHoles == "True"; 113 | } 114 | 115 | if('exploreConcave' in c){ 116 | config.exploreConcave = c.exploreConcave == "True"; 117 | } 118 | 119 | return config; 120 | } 121 | 122 | // progressCallback is called when progress is made 123 | // displayCallback is called when a new placement has been made 124 | this.start = function(progressCallback, displayCallback){ 125 | if(!svg || !bin){ 126 | return false; 127 | } 128 | 129 | parts = Array.prototype.slice.call(svg.childNodes); 130 | var binindex = parts.indexOf(bin); 131 | 132 | if(binindex >= 0){ 133 | // don't process bin as a part of the tree 134 | parts.splice(binindex, 1); 135 | } 136 | 137 | // build tree without bin 138 | tree = this.getParts(parts.slice(0)); 139 | 140 | offsetTree(tree, 0.5*config.spacing, this.polygonOffset.bind(this)); 141 | 142 | // offset tree recursively 143 | function offsetTree(t, offset, offsetFunction){ 144 | for(var i=0; i 0){ 152 | offsetTree(t[i].childNodes, -offset, offsetFunction); 153 | } 154 | } 155 | } 156 | 157 | binPolygon = SvgParser.polygonify(bin); 158 | binPolygon = this.cleanPolygon(binPolygon); 159 | 160 | if(!binPolygon || binPolygon.length < 3){ 161 | return false; 162 | } 163 | 164 | binBounds = GeometryUtil.getPolygonBounds(binPolygon); 165 | 166 | if(config.spacing > 0){ 167 | var offsetBin = this.polygonOffset(binPolygon, -0.5*config.spacing); 168 | if(offsetBin.length == 1){ 169 | // if the offset contains 0 or more than 1 path, something went wrong. 170 | binPolygon = offsetBin.pop(); 171 | } 172 | } 173 | 174 | binPolygon.id = -1; 175 | 176 | // put bin on origin 177 | var xbinmax = binPolygon[0].x; 178 | var xbinmin = binPolygon[0].x; 179 | var ybinmax = binPolygon[0].y; 180 | var ybinmin = binPolygon[0].y; 181 | 182 | for(var i=1; i xbinmax){ 184 | xbinmax = binPolygon[i].x; 185 | } 186 | else if(binPolygon[i].x < xbinmin){ 187 | xbinmin = binPolygon[i].x; 188 | } 189 | if(binPolygon[i].y > ybinmax){ 190 | ybinmax = binPolygon[i].y; 191 | } 192 | else if(binPolygon[i].y < ybinmin){ 193 | ybinmin = binPolygon[i].y; 194 | } 195 | } 196 | 197 | for(i=0; i 0){ 207 | binPolygon.reverse(); 208 | } 209 | 210 | // remove duplicate endpoints, ensure counterclockwise winding direction 211 | for(i=0; i 0){ 219 | tree[i].reverse(); 220 | } 221 | } 222 | 223 | var self = this; 224 | this.working = false; 225 | 226 | workerTimer = setInterval(function(){ 227 | if(!self.working){ 228 | self.launchWorkers.call(self, tree, binPolygon, config, progressCallback, displayCallback); 229 | self.working = true; 230 | } 231 | 232 | progressCallback(progress); 233 | }, 100); 234 | } 235 | 236 | this.launchWorkers = function(tree, binPolygon, config, progressCallback, displayCallback){ 237 | function shuffle(array) { 238 | var currentIndex = array.length, temporaryValue, randomIndex ; 239 | 240 | // While there remain elements to shuffle... 241 | while (0 !== currentIndex) { 242 | 243 | // Pick a remaining element... 244 | randomIndex = Math.floor(Math.random() * currentIndex); 245 | currentIndex -= 1; 246 | 247 | // And swap it with the current element. 248 | temporaryValue = array[currentIndex]; 249 | array[currentIndex] = array[randomIndex]; 250 | array[randomIndex] = temporaryValue; 251 | } 252 | 253 | return array; 254 | } 255 | 256 | var i,j; 257 | 258 | if(GA === null){ 259 | // initiate new GA 260 | var adam = tree.slice(0); 261 | 262 | // seed with decreasing area 263 | adam.sort(function(a, b){ 264 | return Math.abs(GeometryUtil.polygonArea(b)) - Math.abs(GeometryUtil.polygonArea(a)); 265 | }); 266 | 267 | GA = new GeneticAlgorithm(adam, binPolygon, config); 268 | } 269 | 270 | var individual = null; 271 | 272 | // evaluate all members of the population 273 | for(i=0; i 0){ 369 | for(var i=0; i 0){ 371 | nfp[i].reverse(); 372 | } 373 | } 374 | } 375 | else{ 376 | // warning on null inner NFP 377 | // this is not an error, as the part may simply be larger than the bin or otherwise unplaceable due to geometry 378 | log('NFP Warning: ', pair.key); 379 | } 380 | } 381 | else{ 382 | if(searchEdges){ 383 | nfp = GeometryUtil.noFitPolygon(A,B,false,searchEdges); 384 | } 385 | else{ 386 | nfp = minkowskiDifference(A,B); 387 | } 388 | // sanity check 389 | if(!nfp || nfp.length == 0){ 390 | log('NFP Error: ', pair.key); 391 | log('A: ',JSON.stringify(A)); 392 | log('B: ',JSON.stringify(B)); 393 | return null; 394 | } 395 | 396 | for(var i=0; i 0){ 416 | nfp[i].reverse(); 417 | } 418 | 419 | if(i > 0){ 420 | if(GeometryUtil.pointInPolygon(nfp[i][0], nfp[0])){ 421 | if(GeometryUtil.polygonArea(nfp[i]) < 0){ 422 | nfp[i].reverse(); 423 | } 424 | } 425 | } 426 | } 427 | 428 | // generate nfps for children (holes of parts) if any exist 429 | if(useHoles && A.childNodes && A.childNodes.length > 0){ 430 | var Bbounds = GeometryUtil.getPolygonBounds(B); 431 | 432 | for(var i=0; i Bbounds.width && Abounds.height > Bbounds.height){ 437 | 438 | var cnfp = GeometryUtil.noFitPolygon(A.childNodes[i],B,true,searchEdges); 439 | // ensure all interior NFPs have the same winding direction 440 | if(cnfp && cnfp.length > 0){ 441 | for(var j=0; j sarea){ 501 | clipperNfp = n; 502 | largestArea = sarea; 503 | } 504 | } 505 | 506 | for(var i=0; i 2 && Math.abs(GeometryUtil.polygonArea(poly)) > config.curveTolerance*config.curveTolerance){ 600 | poly.source = i; 601 | polygons.push(poly); 602 | } 603 | } 604 | 605 | // turn the list into a tree 606 | toTree(polygons); 607 | 608 | function toTree(list, idstart){ 609 | var parents = []; 610 | var i,j; 611 | 612 | // assign a unique id to each leaf 613 | var id = idstart || 0; 614 | 615 | for(i=0; i biggestarea){ 702 | biggest = simple[i]; 703 | biggestarea = area; 704 | } 705 | } 706 | 707 | // clean up singularities, coincident points and edges 708 | var clean = ClipperLib.Clipper.CleanPolygon(biggest, config.curveTolerance*config.clipperScale); 709 | 710 | if(!clean || clean.length == 0){ 711 | return null; 712 | } 713 | 714 | return this.clipperToSvg(clean); 715 | } 716 | 717 | // converts a polygon from normal float coordinates to integer coordinates used by clipper, as well as x/y -> X/Y 718 | this.svgToClipper = function(polygon){ 719 | var clip = []; 720 | for(var i=0; i 0){ 770 | var flattened = _flattenTree(part.children, true); 771 | for(k=0; k 0){ 795 | flat = flat.concat(_flattenTree(t[i].children, !hole)); 796 | } 797 | } 798 | 799 | return flat; 800 | } 801 | 802 | return svglist; 803 | } 804 | 805 | this.stop = function(){ 806 | this.working = false; 807 | if(workerTimer){ 808 | clearInterval(workerTimer); 809 | } 810 | }; 811 | } 812 | 813 | function GeneticAlgorithm(adam, bin, config){ 814 | 815 | this.config = config || { populationSize: 10, mutationRate: 10, rotations: 4 }; 816 | this.binBounds = GeometryUtil.getPolygonBounds(bin); 817 | 818 | // population is an array of individuals. Each individual is a object representing the order of insertion and the angle each part is rotated 819 | var angles = []; 820 | for(var i=0; i 0; i--) { 842 | var j = Math.floor(Math.random() * (i + 1)); 843 | var temp = array[i]; 844 | array[i] = array[j]; 845 | array[j] = temp; 846 | } 847 | return array; 848 | } 849 | 850 | angleList = shuffleArray(angleList); 851 | 852 | for(i=0; i= 0){ 960 | pop.splice(pop.indexOf(exclude),1); 961 | } 962 | 963 | var rand = Math.random(); 964 | 965 | var lower = 0; 966 | var weight = 1/pop.length; 967 | var upper = weight; 968 | 969 | for(var i=0; i lower && rand < upper){ 972 | return pop[i]; 973 | } 974 | lower = upper; 975 | upper += 2*weight * ((pop.length-i)/pop.length); 976 | } 977 | 978 | return pop[0]; 979 | } 980 | 981 | })(window); 982 | -------------------------------------------------------------------------------- /SVGnest/svgparser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * SvgParser 3 | * A library to convert an SVG string to parse-able segments for CAD/CAM use 4 | * Licensed under the MIT license 5 | */ 6 | 7 | (function(root){ 8 | 'use strict'; 9 | 10 | function SvgParser(){ 11 | // the SVG document 12 | this.svg; 13 | 14 | // the top level SVG element of the SVG document 15 | this.svgRoot; 16 | 17 | this.allowedElements = ['svg','circle','ellipse','path','polygon','polyline','rect', 'line']; 18 | 19 | this.conf = { 20 | tolerance: 2, // max bound for bezier->line segment conversion, in native SVG units 21 | toleranceSvg: 0.005 // fudge factor for browser inaccuracy in SVG unit handling 22 | }; 23 | } 24 | 25 | SvgParser.prototype.config = function(config){ 26 | this.conf.tolerance = config.tolerance; 27 | } 28 | 29 | SvgParser.prototype.load = function(svgString){ 30 | 31 | if(!svgString || typeof svgString !== 'string'){ 32 | throw Error('invalid SVG string'); 33 | } 34 | 35 | var parser = new DOMParser(); 36 | var svg = parser.parseFromString(svgString, "image/svg+xml"); 37 | 38 | this.svgRoot = false; 39 | 40 | if(svg){ 41 | this.svg = svg; 42 | 43 | for(var i=0; i 0){ 233 | var transform = this.transformParse(transformString); 234 | } 235 | 236 | if(!transform){ 237 | transform = new Matrix(); 238 | } 239 | 240 | var tarray = transform.toArray(); 241 | 242 | // decompose affine matrix to rotate, scale components (translate is just the 3rd column) 243 | var rotate = Math.atan2(tarray[1], tarray[3])*180/Math.PI; 244 | var scale = Math.sqrt(tarray[0]*tarray[0]+tarray[2]*tarray[2]); 245 | 246 | if(element.tagName == 'g' || element.tagName == 'svg' || element.tagName == 'defs' || element.tagName == 'clipPath'){ 247 | element.removeAttribute('transform'); 248 | var children = Array.prototype.slice.call(element.childNodes); 249 | 250 | for(var i=0; i 0){ 461 | element.parentElement.appendChild(element.childNodes[0]); 462 | } 463 | } 464 | } 465 | 466 | // remove all elements with tag name not in the whitelist 467 | // use this to remove , etc that don't represent shapes 468 | SvgParser.prototype.filter = function(whitelist, element){ 469 | if(!whitelist || whitelist.length == 0){ 470 | throw Error('invalid whitelist'); 471 | } 472 | 473 | element = element || this.svgRoot; 474 | 475 | for(var i=0; i=0; i--){ 504 | if(i > 0 && seglist[i].pathSegTypeAsLetter == 'M' || seglist[i].pathSegTypeAsLetter == 'm'){ 505 | lastM = i; 506 | break; 507 | } 508 | } 509 | 510 | if(lastM == 0){ 511 | return false; // only 1 M command, no need to split 512 | } 513 | 514 | for( i=0; i 1){ 554 | path.parentElement.insertBefore(paths[i], path); 555 | addedPaths.push(paths[i]); 556 | } 557 | } 558 | 559 | path.remove(); 560 | 561 | return addedPaths; 562 | } 563 | 564 | // recursively run the given function on the given element 565 | SvgParser.prototype.recurse = function(element, func){ 566 | // only operate on original DOM tree, ignore any children that are added. Avoid infinite loops 567 | var children = Array.prototype.slice.call(element.childNodes); 568 | for(var i=0; i 0 && /[QqTt]/.test(seglist.getItem(i-1).pathSegTypeAsLetter)){ 714 | x1 = prevx + (prevx-prevx1); 715 | y1 = prevy + (prevy-prevy1); 716 | } 717 | else{ 718 | x1 = prevx; 719 | y1 = prevy; 720 | } 721 | case 'q': 722 | case 'Q': 723 | var pointlist = GeometryUtil.QuadraticBezier.linearize({x: prevx, y: prevy}, {x: x, y: y}, {x: x1, y: y1}, this.conf.tolerance); 724 | pointlist.shift(); // firstpoint would already be in the poly 725 | for(var j=0; j 0 && /[CcSs]/.test(seglist.getItem(i-1).pathSegTypeAsLetter)){ 735 | x1 = prevx + (prevx-prevx2); 736 | y1 = prevy + (prevy-prevy2); 737 | } 738 | else{ 739 | x1 = prevx; 740 | y1 = prevy; 741 | } 742 | case 'c': 743 | case 'C': 744 | var pointlist = GeometryUtil.CubicBezier.linearize({x: prevx, y: prevy}, {x: x, y: y}, {x: x1, y: y1}, {x: x2, y: y2}, this.conf.tolerance); 745 | pointlist.shift(); // firstpoint would already be in the poly 746 | for(var j=0; j 0 && GeometryUtil.almostEqual(poly[0].x,poly[poly.length-1].x, this.conf.toleranceSvg) && GeometryUtil.almostEqual(poly[0].y,poly[poly.length-1].y, this.conf.toleranceSvg)){ 776 | poly.pop(); 777 | } 778 | 779 | return poly; 780 | }; 781 | 782 | // expose public methods 783 | var parser = new SvgParser(); 784 | 785 | root.SvgParser = { 786 | config: parser.config.bind(parser), 787 | load: parser.load.bind(parser), 788 | getStyle: parser.getStyle.bind(parser), 789 | clean: parser.cleanInput.bind(parser), 790 | polygonify: parser.polygonify.bind(parser) 791 | }; 792 | 793 | }(window)); -------------------------------------------------------------------------------- /SVGnest/util/domparser.js: -------------------------------------------------------------------------------- 1 | /* inspired by https://gist.github.com/1129031 */ 2 | /*global document, DOMParser*/ 3 | 4 | (function(DOMParser) { 5 | "use strict"; 6 | 7 | var 8 | proto = DOMParser.prototype 9 | , nativeParse = proto.parseFromString 10 | ; 11 | 12 | // Firefox/Opera/IE throw errors on unsupported types 13 | try { 14 | // WebKit returns null on unsupported types 15 | if ((new DOMParser()).parseFromString("", "text/html")) { 16 | // text/html parsing is natively supported 17 | return; 18 | } 19 | } catch (ex) {} 20 | 21 | proto.parseFromString = function(markup, type) { 22 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) { 23 | var 24 | doc = document.implementation.createHTMLDocument("") 25 | ; 26 | if (markup.toLowerCase().indexOf(' -1) { 27 | doc.documentElement.innerHTML = markup; 28 | } 29 | else { 30 | doc.body.innerHTML = markup; 31 | } 32 | return doc; 33 | } else { 34 | return nativeParse.apply(this, arguments); 35 | } 36 | }; 37 | }(DOMParser)); -------------------------------------------------------------------------------- /SVGnest/util/eval.js: -------------------------------------------------------------------------------- 1 | var isNode = typeof module !== 'undefined' && module.exports; 2 | 3 | if (isNode) { 4 | process.once('message', function (code) { 5 | eval(JSON.parse(code).data); 6 | }); 7 | } else { 8 | self.onmessage = function (code) { 9 | eval(code.data); 10 | }; 11 | } -------------------------------------------------------------------------------- /SVGnest/util/filesaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 1.3.2 4 | * 2016-06-16 18:25:19 5 | * 6 | * By Eli Grey, http://eligrey.com 7 | * License: MIT 8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self */ 12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 15 | 16 | var saveAs = saveAs || (function(view) { 17 | "use strict"; 18 | // IE <10 is explicitly unsupported 19 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { 20 | return; 21 | } 22 | var 23 | doc = view.document 24 | // only get URL when necessary in case Blob.js hasn't overridden it yet 25 | , get_URL = function() { 26 | return view.URL || view.webkitURL || view; 27 | } 28 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 29 | , can_use_save_link = "download" in save_link 30 | , click = function(node) { 31 | var event = new MouseEvent("click"); 32 | node.dispatchEvent(event); 33 | } 34 | , is_safari = /constructor/i.test(view.HTMLElement) || view.safari 35 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) 36 | , throw_outside = function(ex) { 37 | (view.setImmediate || view.setTimeout)(function() { 38 | throw ex; 39 | }, 0); 40 | } 41 | , force_saveable_type = "application/octet-stream" 42 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to 43 | , arbitrary_revoke_timeout = 1000 * 40 // in ms 44 | , revoke = function(file) { 45 | var revoker = function() { 46 | if (typeof file === "string") { // file is an object URL 47 | get_URL().revokeObjectURL(file); 48 | } else { // file is a File 49 | file.remove(); 50 | } 51 | }; 52 | setTimeout(revoker, arbitrary_revoke_timeout); 53 | } 54 | , dispatch = function(filesaver, event_types, event) { 55 | event_types = [].concat(event_types); 56 | var i = event_types.length; 57 | while (i--) { 58 | var listener = filesaver["on" + event_types[i]]; 59 | if (typeof listener === "function") { 60 | try { 61 | listener.call(filesaver, event || filesaver); 62 | } catch (ex) { 63 | throw_outside(ex); 64 | } 65 | } 66 | } 67 | } 68 | , auto_bom = function(blob) { 69 | // prepend BOM for UTF-8 XML and text/* types (including HTML) 70 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 71 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 72 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); 73 | } 74 | return blob; 75 | } 76 | , FileSaver = function(blob, name, no_auto_bom) { 77 | if (!no_auto_bom) { 78 | blob = auto_bom(blob); 79 | } 80 | // First try a.download, then web filesystem, then object URLs 81 | var 82 | filesaver = this 83 | , type = blob.type 84 | , force = type === force_saveable_type 85 | , object_url 86 | , dispatch_all = function() { 87 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 88 | } 89 | // on any filesys errors revert to saving with object URLs 90 | , fs_error = function() { 91 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { 92 | // Safari doesn't allow downloading of blob urls 93 | var reader = new FileReader(); 94 | reader.onloadend = function() { 95 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); 96 | var popup = view.open(url, '_blank'); 97 | if(!popup) view.location.href = url; 98 | url=undefined; // release reference before dispatching 99 | filesaver.readyState = filesaver.DONE; 100 | dispatch_all(); 101 | }; 102 | reader.readAsDataURL(blob); 103 | filesaver.readyState = filesaver.INIT; 104 | return; 105 | } 106 | // don't create more object URLs than needed 107 | if (!object_url) { 108 | object_url = get_URL().createObjectURL(blob); 109 | } 110 | if (force) { 111 | view.location.href = object_url; 112 | } else { 113 | var opened = view.open(object_url, "_blank"); 114 | if (!opened) { 115 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html 116 | view.location.href = object_url; 117 | } 118 | } 119 | filesaver.readyState = filesaver.DONE; 120 | dispatch_all(); 121 | revoke(object_url); 122 | } 123 | ; 124 | filesaver.readyState = filesaver.INIT; 125 | 126 | if (can_use_save_link) { 127 | object_url = get_URL().createObjectURL(blob); 128 | setTimeout(function() { 129 | save_link.href = object_url; 130 | save_link.download = name; 131 | click(save_link); 132 | dispatch_all(); 133 | revoke(object_url); 134 | filesaver.readyState = filesaver.DONE; 135 | }); 136 | return; 137 | } 138 | 139 | fs_error(); 140 | } 141 | , FS_proto = FileSaver.prototype 142 | , saveAs = function(blob, name, no_auto_bom) { 143 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom); 144 | } 145 | ; 146 | // IE 10+ (native saveAs) 147 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { 148 | return function(blob, name, no_auto_bom) { 149 | name = name || blob.name || "download"; 150 | 151 | if (!no_auto_bom) { 152 | blob = auto_bom(blob); 153 | } 154 | return navigator.msSaveOrOpenBlob(blob, name); 155 | }; 156 | } 157 | 158 | FS_proto.abort = function(){}; 159 | FS_proto.readyState = FS_proto.INIT = 0; 160 | FS_proto.WRITING = 1; 161 | FS_proto.DONE = 2; 162 | 163 | FS_proto.error = 164 | FS_proto.onwritestart = 165 | FS_proto.onprogress = 166 | FS_proto.onwrite = 167 | FS_proto.onabort = 168 | FS_proto.onerror = 169 | FS_proto.onwriteend = 170 | null; 171 | 172 | return saveAs; 173 | }( 174 | typeof self !== "undefined" && self 175 | || typeof window !== "undefined" && window 176 | || this.content 177 | )); 178 | // `self` is undefined in Firefox for Android content script context 179 | // while `this` is nsIContentFrameMessageManager 180 | // with an attribute `content` that corresponds to the window 181 | 182 | if (typeof module !== "undefined" && module.exports) { 183 | module.exports.saveAs = saveAs; 184 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) { 185 | define("FileSaver.js", function() { 186 | return saveAs; 187 | }); 188 | } -------------------------------------------------------------------------------- /SVGnest/util/json.js: -------------------------------------------------------------------------------- 1 | var JSON;if(!JSON){JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i 1) { 316 | ++runningWorkers; 317 | that._spawnReduceWorker([that.data[0], that.data[1]], cb, done, env, wrk); 318 | that.data.splice(0, 2); 319 | } else { 320 | if (wrk) wrk.terminate(); 321 | } 322 | } 323 | 324 | var newOp = new Operation(); 325 | this.operation.then(function () { 326 | if (that.data.length === 1) { 327 | newOp.resolve(null, that.data[0]); 328 | } else { 329 | for (var i = 0; i < that.options.maxWorkers && i < Math.floor(that.data.length / 2) ; ++i) { 330 | ++runningWorkers; 331 | that._spawnReduceWorker([that.data[i * 2], that.data[i * 2 + 1]], cb, done, env); 332 | } 333 | 334 | that.data.splice(0, i * 2); 335 | } 336 | }); 337 | this.operation = newOp; 338 | return this; 339 | }; 340 | 341 | Parallel.prototype.then = function (cb, errCb) { 342 | var that = this; 343 | var newOp = new Operation(); 344 | errCb = typeof errCb === 'function' ? errCb : function(){}; 345 | 346 | this.operation.then(function () { 347 | var retData; 348 | 349 | try { 350 | if (cb) { 351 | retData = cb(that.data); 352 | if (retData !== undefined) { 353 | that.data = retData; 354 | } 355 | } 356 | newOp.resolve(null, that.data); 357 | } catch (e) { 358 | if (errCb) { 359 | retData = errCb(e); 360 | if (retData !== undefined) { 361 | that.data = retData; 362 | } 363 | 364 | newOp.resolve(null, that.data); 365 | } else { 366 | newOp.resolve(null, e); 367 | } 368 | } 369 | }, function (err) { 370 | if (errCb) { 371 | var retData = errCb(err); 372 | if (retData !== undefined) { 373 | that.data = retData; 374 | } 375 | 376 | newOp.resolve(null, that.data); 377 | } else { 378 | newOp.resolve(null, err); 379 | } 380 | }); 381 | this.operation = newOp; 382 | return this; 383 | }; 384 | 385 | root.Parallel = Parallel; 386 | })(typeof window !== 'undefined' ? window : self); 387 | -------------------------------------------------------------------------------- /SVGnest/util/placementworker.js: -------------------------------------------------------------------------------- 1 | 2 | // jsClipper uses X/Y instead of x/y... 3 | function toClipperCoordinates(polygon){ 4 | var clone = []; 5 | for(var i=0; i 0){ 40 | rotated.children = []; 41 | for(var j=0; j 0){ 87 | 88 | var placed = []; 89 | var placements = []; 90 | fitness += 1; // add 1 for each new bin opened (lower fitness is better) 91 | 92 | for(i=0; i 2 && area > 0.1*self.config.clipperScale*self.config.clipperScale){ 173 | clipper.AddPath(clone, ClipperLib.PolyType.ptSubject, true); 174 | } 175 | } 176 | } 177 | 178 | if(!clipper.Execute(ClipperLib.ClipType.ctUnion, combinedNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){ 179 | continue; 180 | } 181 | 182 | // difference with bin polygon 183 | var finalNfp = new ClipperLib.Paths(); 184 | clipper = new ClipperLib.Clipper(); 185 | 186 | clipper.AddPaths(combinedNfp, ClipperLib.PolyType.ptClip, true); 187 | clipper.AddPaths(clipperBinNfp, ClipperLib.PolyType.ptSubject, true); 188 | if(!clipper.Execute(ClipperLib.ClipType.ctDifference, finalNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){ 189 | continue; 190 | } 191 | 192 | finalNfp = ClipperLib.Clipper.CleanPolygons(finalNfp, 0.0001*self.config.clipperScale); 193 | 194 | for(j=0; j= 0){ 273 | paths.splice(index,1); 274 | } 275 | } 276 | 277 | if(placements && placements.length > 0){ 278 | allplacements.push(placements); 279 | } 280 | else{ 281 | break; // something went wrong 282 | } 283 | } 284 | 285 | // there were parts that couldn't be placed 286 | fitness += 2*paths.length; 287 | 288 | return {placements: allplacements, fitness: fitness, paths: paths, area: binarea }; 289 | }; 290 | 291 | } 292 | (typeof window !== 'undefined' ? window : self).PlacementWorker = PlacementWorker; 293 | 294 | // clipperjs uses alerts for warnings 295 | function alert(message) { 296 | console.log('alert: ', message); 297 | } 298 | -------------------------------------------------------------------------------- /resources/16x16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/16x16-disabled.png -------------------------------------------------------------------------------- /resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/16x16.png -------------------------------------------------------------------------------- /resources/16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/16x16@2x.png -------------------------------------------------------------------------------- /resources/32x32-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/32x32-disabled.png -------------------------------------------------------------------------------- /resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/32x32.png -------------------------------------------------------------------------------- /resources/32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/32x32@2x.png -------------------------------------------------------------------------------- /resources/description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/FuseNest/5edc4650be1b984045b5d28c754ac9725027a490/resources/description.png --------------------------------------------------------------------------------