├── .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""
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 |
3 |
4 | # FuseNest
5 | 2D Nesting / Packing Add-In for Autodesk Fusion360 based on SVGNest
6 |
7 |
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 |
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 |
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 |
14 |
--------------------------------------------------------------------------------
/SVGnest/img/code.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
22 |
--------------------------------------------------------------------------------
/SVGnest/img/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SVGnest/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/SVGnest/img/zoomout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SVGnest/readme.md:
--------------------------------------------------------------------------------
1 | # 
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 | 
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 | 
27 |
28 | It also features part-in-part support, for placing parts in the holes of other parts.
29 |
30 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------