meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meerk40t/__init__.py +1 -1
- meerk40t/balormk/balor_params.py +167 -167
- meerk40t/balormk/clone_loader.py +457 -457
- meerk40t/balormk/controller.py +1566 -1512
- meerk40t/balormk/cylindermod.py +64 -0
- meerk40t/balormk/device.py +966 -1959
- meerk40t/balormk/driver.py +778 -591
- meerk40t/balormk/galvo_commands.py +1194 -0
- meerk40t/balormk/gui/balorconfig.py +237 -111
- meerk40t/balormk/gui/balorcontroller.py +191 -184
- meerk40t/balormk/gui/baloroperationproperties.py +116 -115
- meerk40t/balormk/gui/corscene.py +845 -0
- meerk40t/balormk/gui/gui.py +179 -147
- meerk40t/balormk/livelightjob.py +466 -382
- meerk40t/balormk/mock_connection.py +131 -109
- meerk40t/balormk/plugin.py +133 -135
- meerk40t/balormk/usb_connection.py +306 -301
- meerk40t/camera/__init__.py +1 -1
- meerk40t/camera/camera.py +514 -397
- meerk40t/camera/gui/camerapanel.py +1241 -1095
- meerk40t/camera/gui/gui.py +58 -58
- meerk40t/camera/plugin.py +441 -399
- meerk40t/ch341/__init__.py +27 -27
- meerk40t/ch341/ch341device.py +628 -628
- meerk40t/ch341/libusb.py +595 -589
- meerk40t/ch341/mock.py +171 -171
- meerk40t/ch341/windriver.py +157 -157
- meerk40t/constants.py +13 -0
- meerk40t/core/__init__.py +1 -1
- meerk40t/core/bindalias.py +550 -539
- meerk40t/core/core.py +47 -47
- meerk40t/core/cutcode/cubiccut.py +73 -73
- meerk40t/core/cutcode/cutcode.py +315 -312
- meerk40t/core/cutcode/cutgroup.py +141 -137
- meerk40t/core/cutcode/cutobject.py +192 -185
- meerk40t/core/cutcode/dwellcut.py +37 -37
- meerk40t/core/cutcode/gotocut.py +29 -29
- meerk40t/core/cutcode/homecut.py +29 -29
- meerk40t/core/cutcode/inputcut.py +34 -34
- meerk40t/core/cutcode/linecut.py +33 -33
- meerk40t/core/cutcode/outputcut.py +34 -34
- meerk40t/core/cutcode/plotcut.py +335 -335
- meerk40t/core/cutcode/quadcut.py +61 -61
- meerk40t/core/cutcode/rastercut.py +168 -148
- meerk40t/core/cutcode/waitcut.py +34 -34
- meerk40t/core/cutplan.py +1843 -1316
- meerk40t/core/drivers.py +330 -329
- meerk40t/core/elements/align.py +801 -669
- meerk40t/core/elements/branches.py +1858 -1507
- meerk40t/core/elements/clipboard.py +229 -219
- meerk40t/core/elements/element_treeops.py +4595 -2837
- meerk40t/core/elements/element_types.py +125 -105
- meerk40t/core/elements/elements.py +4315 -3617
- meerk40t/core/elements/files.py +117 -64
- meerk40t/core/elements/geometry.py +473 -224
- meerk40t/core/elements/grid.py +467 -316
- meerk40t/core/elements/materials.py +158 -94
- meerk40t/core/elements/notes.py +50 -38
- meerk40t/core/elements/offset_clpr.py +934 -912
- meerk40t/core/elements/offset_mk.py +963 -955
- meerk40t/core/elements/penbox.py +339 -267
- meerk40t/core/elements/placements.py +300 -83
- meerk40t/core/elements/render.py +785 -687
- meerk40t/core/elements/shapes.py +2618 -2092
- meerk40t/core/elements/testcases.py +105 -0
- meerk40t/core/elements/trace.py +651 -563
- meerk40t/core/elements/tree_commands.py +415 -409
- meerk40t/core/elements/undo_redo.py +116 -58
- meerk40t/core/elements/wordlist.py +319 -200
- meerk40t/core/exceptions.py +9 -9
- meerk40t/core/laserjob.py +220 -220
- meerk40t/core/logging.py +63 -63
- meerk40t/core/node/blobnode.py +83 -86
- meerk40t/core/node/bootstrap.py +105 -103
- meerk40t/core/node/branch_elems.py +40 -31
- meerk40t/core/node/branch_ops.py +45 -38
- meerk40t/core/node/branch_regmark.py +48 -41
- meerk40t/core/node/cutnode.py +29 -32
- meerk40t/core/node/effect_hatch.py +375 -257
- meerk40t/core/node/effect_warp.py +398 -0
- meerk40t/core/node/effect_wobble.py +441 -309
- meerk40t/core/node/elem_ellipse.py +404 -309
- meerk40t/core/node/elem_image.py +1082 -801
- meerk40t/core/node/elem_line.py +358 -292
- meerk40t/core/node/elem_path.py +259 -201
- meerk40t/core/node/elem_point.py +129 -102
- meerk40t/core/node/elem_polyline.py +310 -246
- meerk40t/core/node/elem_rect.py +376 -286
- meerk40t/core/node/elem_text.py +445 -418
- meerk40t/core/node/filenode.py +59 -40
- meerk40t/core/node/groupnode.py +138 -74
- meerk40t/core/node/image_processed.py +777 -766
- meerk40t/core/node/image_raster.py +156 -113
- meerk40t/core/node/layernode.py +31 -31
- meerk40t/core/node/mixins.py +135 -107
- meerk40t/core/node/node.py +1427 -1304
- meerk40t/core/node/nutils.py +117 -114
- meerk40t/core/node/op_cut.py +463 -335
- meerk40t/core/node/op_dots.py +296 -251
- meerk40t/core/node/op_engrave.py +414 -311
- meerk40t/core/node/op_image.py +755 -369
- meerk40t/core/node/op_raster.py +787 -522
- meerk40t/core/node/place_current.py +37 -40
- meerk40t/core/node/place_point.py +329 -126
- meerk40t/core/node/refnode.py +58 -47
- meerk40t/core/node/rootnode.py +225 -219
- meerk40t/core/node/util_console.py +48 -48
- meerk40t/core/node/util_goto.py +84 -65
- meerk40t/core/node/util_home.py +61 -61
- meerk40t/core/node/util_input.py +102 -102
- meerk40t/core/node/util_output.py +102 -102
- meerk40t/core/node/util_wait.py +65 -65
- meerk40t/core/parameters.py +709 -707
- meerk40t/core/planner.py +875 -785
- meerk40t/core/plotplanner.py +656 -652
- meerk40t/core/space.py +120 -113
- meerk40t/core/spoolers.py +706 -705
- meerk40t/core/svg_io.py +1836 -1549
- meerk40t/core/treeop.py +534 -445
- meerk40t/core/undos.py +278 -124
- meerk40t/core/units.py +784 -680
- meerk40t/core/view.py +393 -322
- meerk40t/core/webhelp.py +62 -62
- meerk40t/core/wordlist.py +513 -504
- meerk40t/cylinder/cylinder.py +247 -0
- meerk40t/cylinder/gui/cylindersettings.py +41 -0
- meerk40t/cylinder/gui/gui.py +24 -0
- meerk40t/device/__init__.py +1 -1
- meerk40t/device/basedevice.py +322 -123
- meerk40t/device/devicechoices.py +50 -0
- meerk40t/device/dummydevice.py +163 -128
- meerk40t/device/gui/defaultactions.py +618 -602
- meerk40t/device/gui/effectspanel.py +114 -0
- meerk40t/device/gui/formatterpanel.py +253 -290
- meerk40t/device/gui/warningpanel.py +337 -260
- meerk40t/device/mixins.py +13 -13
- meerk40t/dxf/__init__.py +1 -1
- meerk40t/dxf/dxf_io.py +766 -554
- meerk40t/dxf/plugin.py +47 -35
- meerk40t/external_plugins.py +79 -79
- meerk40t/external_plugins_build.py +28 -28
- meerk40t/extra/cag.py +112 -116
- meerk40t/extra/coolant.py +403 -0
- meerk40t/extra/encode_detect.py +204 -0
- meerk40t/extra/ezd.py +1165 -1165
- meerk40t/extra/hershey.py +834 -340
- meerk40t/extra/imageactions.py +322 -316
- meerk40t/extra/inkscape.py +628 -622
- meerk40t/extra/lbrn.py +424 -424
- meerk40t/extra/outerworld.py +283 -0
- meerk40t/extra/param_functions.py +1542 -1556
- meerk40t/extra/potrace.py +257 -253
- meerk40t/extra/serial_exchange.py +118 -0
- meerk40t/extra/updater.py +602 -453
- meerk40t/extra/vectrace.py +147 -146
- meerk40t/extra/winsleep.py +83 -83
- meerk40t/extra/xcs_reader.py +597 -0
- meerk40t/fill/fills.py +781 -335
- meerk40t/fill/patternfill.py +1061 -1061
- meerk40t/fill/patterns.py +614 -567
- meerk40t/grbl/control.py +87 -87
- meerk40t/grbl/controller.py +990 -903
- meerk40t/grbl/device.py +1084 -768
- meerk40t/grbl/driver.py +989 -771
- meerk40t/grbl/emulator.py +532 -497
- meerk40t/grbl/gcodejob.py +783 -767
- meerk40t/grbl/gui/grblconfiguration.py +373 -298
- meerk40t/grbl/gui/grblcontroller.py +485 -271
- meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
- meerk40t/grbl/gui/grbloperationconfig.py +105 -0
- meerk40t/grbl/gui/gui.py +147 -116
- meerk40t/grbl/interpreter.py +44 -44
- meerk40t/grbl/loader.py +22 -22
- meerk40t/grbl/mock_connection.py +56 -56
- meerk40t/grbl/plugin.py +294 -264
- meerk40t/grbl/serial_connection.py +93 -88
- meerk40t/grbl/tcp_connection.py +81 -79
- meerk40t/grbl/ws_connection.py +112 -0
- meerk40t/gui/__init__.py +1 -1
- meerk40t/gui/about.py +2042 -296
- meerk40t/gui/alignment.py +1644 -1608
- meerk40t/gui/autoexec.py +199 -0
- meerk40t/gui/basicops.py +791 -670
- meerk40t/gui/bufferview.py +77 -71
- meerk40t/gui/busy.py +232 -133
- meerk40t/gui/choicepropertypanel.py +1662 -1469
- meerk40t/gui/consolepanel.py +706 -542
- meerk40t/gui/devicepanel.py +687 -581
- meerk40t/gui/dialogoptions.py +110 -107
- meerk40t/gui/executejob.py +316 -306
- meerk40t/gui/fonts.py +90 -90
- meerk40t/gui/functionwrapper.py +252 -0
- meerk40t/gui/gui_mixins.py +729 -0
- meerk40t/gui/guicolors.py +205 -182
- meerk40t/gui/help_assets/help_assets.py +218 -201
- meerk40t/gui/helper.py +154 -0
- meerk40t/gui/hersheymanager.py +1440 -846
- meerk40t/gui/icons.py +3422 -2747
- meerk40t/gui/imagesplitter.py +555 -508
- meerk40t/gui/keymap.py +354 -344
- meerk40t/gui/laserpanel.py +897 -806
- meerk40t/gui/laserrender.py +1470 -1232
- meerk40t/gui/lasertoolpanel.py +805 -793
- meerk40t/gui/magnetoptions.py +436 -0
- meerk40t/gui/materialmanager.py +2944 -0
- meerk40t/gui/materialtest.py +1722 -1694
- meerk40t/gui/mkdebug.py +646 -359
- meerk40t/gui/mwindow.py +163 -140
- meerk40t/gui/navigationpanels.py +2605 -2467
- meerk40t/gui/notes.py +143 -142
- meerk40t/gui/opassignment.py +414 -410
- meerk40t/gui/operation_info.py +310 -299
- meerk40t/gui/plugin.py +500 -328
- meerk40t/gui/position.py +714 -669
- meerk40t/gui/preferences.py +901 -650
- meerk40t/gui/propertypanels/attributes.py +1461 -1131
- meerk40t/gui/propertypanels/blobproperty.py +117 -114
- meerk40t/gui/propertypanels/consoleproperty.py +83 -80
- meerk40t/gui/propertypanels/gotoproperty.py +77 -0
- meerk40t/gui/propertypanels/groupproperties.py +223 -217
- meerk40t/gui/propertypanels/hatchproperty.py +489 -469
- meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
- meerk40t/gui/propertypanels/inputproperty.py +59 -58
- meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
- meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
- meerk40t/gui/propertypanels/outputproperty.py +59 -58
- meerk40t/gui/propertypanels/pathproperty.py +389 -380
- meerk40t/gui/propertypanels/placementproperty.py +1214 -383
- meerk40t/gui/propertypanels/pointproperty.py +140 -136
- meerk40t/gui/propertypanels/propertywindow.py +313 -181
- meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
- meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
- meerk40t/gui/propertypanels/textproperty.py +770 -755
- meerk40t/gui/propertypanels/waitproperty.py +56 -55
- meerk40t/gui/propertypanels/warpproperty.py +121 -0
- meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
- meerk40t/gui/ribbon.py +2471 -2210
- meerk40t/gui/scene/scene.py +1100 -1051
- meerk40t/gui/scene/sceneconst.py +22 -22
- meerk40t/gui/scene/scenepanel.py +439 -349
- meerk40t/gui/scene/scenespacewidget.py +365 -365
- meerk40t/gui/scene/widget.py +518 -505
- meerk40t/gui/scenewidgets/affinemover.py +215 -215
- meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
- meerk40t/gui/scenewidgets/bedwidget.py +120 -97
- meerk40t/gui/scenewidgets/elementswidget.py +137 -107
- meerk40t/gui/scenewidgets/gridwidget.py +785 -745
- meerk40t/gui/scenewidgets/guidewidget.py +765 -765
- meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
- meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
- meerk40t/gui/scenewidgets/nodeselector.py +28 -28
- meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
- meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
- meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
- meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
- meerk40t/gui/simpleui.py +362 -333
- meerk40t/gui/simulation.py +2451 -2094
- meerk40t/gui/snapoptions.py +208 -203
- meerk40t/gui/spoolerpanel.py +1227 -1180
- meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
- meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
- meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
- meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
- meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
- meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
- meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
- meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
- meerk40t/gui/themes.py +200 -78
- meerk40t/gui/tips.py +590 -0
- meerk40t/gui/toolwidgets/circlebrush.py +35 -35
- meerk40t/gui/toolwidgets/toolcircle.py +248 -242
- meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
- meerk40t/gui/toolwidgets/tooldraw.py +97 -90
- meerk40t/gui/toolwidgets/toolellipse.py +219 -212
- meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
- meerk40t/gui/toolwidgets/toolline.py +39 -144
- meerk40t/gui/toolwidgets/toollinetext.py +79 -236
- meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
- meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
- meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
- meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
- meerk40t/gui/toolwidgets/toolparameter.py +754 -668
- meerk40t/gui/toolwidgets/toolplacement.py +108 -108
- meerk40t/gui/toolwidgets/toolpoint.py +68 -59
- meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
- meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
- meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
- meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
- meerk40t/gui/toolwidgets/toolrect.py +211 -207
- meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
- meerk40t/gui/toolwidgets/toolribbon.py +598 -113
- meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
- meerk40t/gui/toolwidgets/tooltext.py +98 -89
- meerk40t/gui/toolwidgets/toolvector.py +213 -204
- meerk40t/gui/toolwidgets/toolwidget.py +39 -39
- meerk40t/gui/usbconnect.py +98 -91
- meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
- meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
- meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
- meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
- meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
- meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
- meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
- meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
- meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
- meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
- meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
- meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
- meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
- meerk40t/gui/wordlisteditor.py +985 -931
- meerk40t/gui/wxmeerk40t.py +1447 -1169
- meerk40t/gui/wxmmain.py +5644 -4112
- meerk40t/gui/wxmribbon.py +1591 -1076
- meerk40t/gui/wxmscene.py +1631 -1453
- meerk40t/gui/wxmtree.py +2416 -2089
- meerk40t/gui/wxutils.py +1769 -1099
- meerk40t/gui/zmatrix.py +102 -102
- meerk40t/image/__init__.py +1 -1
- meerk40t/image/dither.py +429 -0
- meerk40t/image/imagetools.py +2793 -2269
- meerk40t/internal_plugins.py +150 -130
- meerk40t/kernel/__init__.py +63 -12
- meerk40t/kernel/channel.py +259 -212
- meerk40t/kernel/context.py +538 -538
- meerk40t/kernel/exceptions.py +41 -41
- meerk40t/kernel/functions.py +463 -414
- meerk40t/kernel/jobs.py +100 -100
- meerk40t/kernel/kernel.py +3828 -3571
- meerk40t/kernel/lifecycles.py +71 -71
- meerk40t/kernel/module.py +49 -49
- meerk40t/kernel/service.py +147 -147
- meerk40t/kernel/settings.py +383 -343
- meerk40t/lihuiyu/controller.py +883 -876
- meerk40t/lihuiyu/device.py +1181 -1069
- meerk40t/lihuiyu/driver.py +1466 -1372
- meerk40t/lihuiyu/gui/gui.py +127 -106
- meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
- meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
- meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
- meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
- meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
- meerk40t/lihuiyu/interpreter.py +53 -53
- meerk40t/lihuiyu/laserspeed.py +450 -450
- meerk40t/lihuiyu/loader.py +90 -90
- meerk40t/lihuiyu/parser.py +404 -404
- meerk40t/lihuiyu/plugin.py +101 -102
- meerk40t/lihuiyu/tcp_connection.py +111 -109
- meerk40t/main.py +231 -165
- meerk40t/moshi/builder.py +788 -781
- meerk40t/moshi/controller.py +505 -499
- meerk40t/moshi/device.py +495 -442
- meerk40t/moshi/driver.py +862 -696
- meerk40t/moshi/gui/gui.py +78 -76
- meerk40t/moshi/gui/moshicontrollergui.py +538 -522
- meerk40t/moshi/gui/moshidrivergui.py +87 -75
- meerk40t/moshi/plugin.py +43 -43
- meerk40t/network/console_server.py +140 -57
- meerk40t/network/kernelserver.py +10 -9
- meerk40t/network/tcp_server.py +142 -140
- meerk40t/network/udp_server.py +103 -77
- meerk40t/network/web_server.py +404 -0
- meerk40t/newly/controller.py +1158 -1144
- meerk40t/newly/device.py +874 -732
- meerk40t/newly/driver.py +540 -412
- meerk40t/newly/gui/gui.py +219 -188
- meerk40t/newly/gui/newlyconfig.py +116 -101
- meerk40t/newly/gui/newlycontroller.py +193 -186
- meerk40t/newly/gui/operationproperties.py +51 -51
- meerk40t/newly/mock_connection.py +82 -82
- meerk40t/newly/newly_params.py +56 -56
- meerk40t/newly/plugin.py +1214 -1246
- meerk40t/newly/usb_connection.py +322 -322
- meerk40t/rotary/gui/gui.py +52 -46
- meerk40t/rotary/gui/rotarysettings.py +240 -232
- meerk40t/rotary/rotary.py +202 -98
- meerk40t/ruida/control.py +291 -91
- meerk40t/ruida/controller.py +138 -1088
- meerk40t/ruida/device.py +676 -231
- meerk40t/ruida/driver.py +534 -472
- meerk40t/ruida/emulator.py +1494 -1491
- meerk40t/ruida/exceptions.py +4 -4
- meerk40t/ruida/gui/gui.py +71 -76
- meerk40t/ruida/gui/ruidaconfig.py +239 -72
- meerk40t/ruida/gui/ruidacontroller.py +187 -184
- meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
- meerk40t/ruida/loader.py +54 -52
- meerk40t/ruida/mock_connection.py +57 -109
- meerk40t/ruida/plugin.py +124 -87
- meerk40t/ruida/rdjob.py +2084 -945
- meerk40t/ruida/serial_connection.py +116 -0
- meerk40t/ruida/tcp_connection.py +146 -0
- meerk40t/ruida/udp_connection.py +73 -0
- meerk40t/svgelements.py +9671 -9669
- meerk40t/tools/driver_to_path.py +584 -579
- meerk40t/tools/geomstr.py +5583 -4680
- meerk40t/tools/jhfparser.py +357 -292
- meerk40t/tools/kerftest.py +904 -890
- meerk40t/tools/livinghinges.py +1168 -1033
- meerk40t/tools/pathtools.py +987 -949
- meerk40t/tools/pmatrix.py +234 -0
- meerk40t/tools/pointfinder.py +942 -942
- meerk40t/tools/polybool.py +941 -940
- meerk40t/tools/rasterplotter.py +1660 -547
- meerk40t/tools/shxparser.py +1047 -901
- meerk40t/tools/ttfparser.py +726 -446
- meerk40t/tools/zinglplotter.py +595 -593
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
- meerk40t-0.9.7020.dist-info/RECORD +446 -0
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
- meerk40t/balormk/elementlightjob.py +0 -159
- meerk40t-0.9.3001.dist-info/RECORD +0 -437
- test/bootstrap.py +0 -63
- test/test_cli.py +0 -12
- test/test_core_cutcode.py +0 -418
- test/test_core_elements.py +0 -144
- test/test_core_plotplanner.py +0 -397
- test/test_core_viewports.py +0 -312
- test/test_drivers_grbl.py +0 -108
- test/test_drivers_lihuiyu.py +0 -443
- test/test_drivers_newly.py +0 -113
- test/test_element_degenerate_points.py +0 -43
- test/test_elements_classify.py +0 -97
- test/test_elements_penbox.py +0 -22
- test/test_file_svg.py +0 -176
- test/test_fill.py +0 -155
- test/test_geomstr.py +0 -1523
- test/test_geomstr_nodes.py +0 -18
- test/test_imagetools_actualize.py +0 -306
- test/test_imagetools_wizard.py +0 -258
- test/test_kernel.py +0 -200
- test/test_laser_speeds.py +0 -3303
- test/test_length.py +0 -57
- test/test_lifecycle.py +0 -66
- test/test_operations.py +0 -251
- test/test_operations_hatch.py +0 -57
- test/test_ruida.py +0 -19
- test/test_spooler.py +0 -22
- test/test_tools_rasterplotter.py +0 -29
- test/test_wobble.py +0 -133
- test/test_zingl.py +0 -124
- {test → meerk40t/cylinder}/__init__.py +0 -0
- /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
meerk40t/tools/pathtools.py
CHANGED
@@ -1,949 +1,987 @@
|
|
1
|
-
from math import isinf, isnan
|
2
|
-
|
3
|
-
from meerk40t.svgelements import Point
|
4
|
-
|
5
|
-
|
6
|
-
class GraphNode(Point):
|
7
|
-
"""
|
8
|
-
GraphNodes are nodes within the graph that store a list of connections between points.
|
9
|
-
"""
|
10
|
-
|
11
|
-
def __init__(self, x, y=None):
|
12
|
-
Point.__init__(self, x, y)
|
13
|
-
self.connections = []
|
14
|
-
self.visited = 0
|
15
|
-
self.value = None
|
16
|
-
|
17
|
-
|
18
|
-
class Segment:
|
19
|
-
"""
|
20
|
-
Graphing segments are connections between nodes on the graph that store, their start and end nodes, active state
|
21
|
-
for use within the monotonic vector filling. The type of segment it is. The index of the segment around the closed
|
22
|
-
shape. A list of bisectors (to calculate the rung attachments).
|
23
|
-
"""
|
24
|
-
|
25
|
-
def __init__(self, a, b, index=0):
|
26
|
-
self.visited = 0
|
27
|
-
self.a = a
|
28
|
-
self.b = b
|
29
|
-
self.active = False
|
30
|
-
self.value = "RUNG"
|
31
|
-
self.index = index
|
32
|
-
self.bisectors = []
|
33
|
-
self.object = None
|
34
|
-
|
35
|
-
def __len__(self):
|
36
|
-
# [False, i, p0, p1, high, low, m, b, path]
|
37
|
-
return 9
|
38
|
-
|
39
|
-
def __str__(self):
|
40
|
-
return f"Segment({str(self.a)},{str(self.b)},{str(self.index)},type='{self.value}')"
|
41
|
-
|
42
|
-
def __getitem__(self, item):
|
43
|
-
if item == 0:
|
44
|
-
return self.active
|
45
|
-
if item == 1:
|
46
|
-
return self.index
|
47
|
-
if item == 2:
|
48
|
-
return self.a
|
49
|
-
if item == 3:
|
50
|
-
return self.b
|
51
|
-
if item == 4:
|
52
|
-
if self.a.y > self.b.y:
|
53
|
-
return self.a
|
54
|
-
else:
|
55
|
-
return self.b
|
56
|
-
if item == 5:
|
57
|
-
if self.a.y < self.b.y:
|
58
|
-
return self.a
|
59
|
-
else:
|
60
|
-
return self.b
|
61
|
-
if item == 6:
|
62
|
-
if self.b[0] - self.a[0] == 0:
|
63
|
-
return float("inf")
|
64
|
-
return (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
65
|
-
if item == 7:
|
66
|
-
if self.b[0] - self.a[0] == 0:
|
67
|
-
return float("inf")
|
68
|
-
im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
69
|
-
return self.a[1] - (im * self.a[0])
|
70
|
-
if item == 8:
|
71
|
-
return self.object
|
72
|
-
|
73
|
-
def intersect(self, segment):
|
74
|
-
return Segment.line_intersect(
|
75
|
-
self.a[0],
|
76
|
-
self.a[1],
|
77
|
-
self.b[0],
|
78
|
-
self.b[1],
|
79
|
-
segment.a[0],
|
80
|
-
segment.a[1],
|
81
|
-
segment.b[0],
|
82
|
-
segment.b[1],
|
83
|
-
)
|
84
|
-
|
85
|
-
def sort_bisectors(self):
|
86
|
-
def distance(a):
|
87
|
-
return self.a.distance_to(a)
|
88
|
-
|
89
|
-
self.bisectors.sort(key=distance)
|
90
|
-
|
91
|
-
def get_intercept(self, y):
|
92
|
-
im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
93
|
-
ib = self.a[1] - (im * self.a[0])
|
94
|
-
if isnan(im) or isinf(im):
|
95
|
-
return self.a[0]
|
96
|
-
return (y - ib) / im
|
97
|
-
|
98
|
-
@staticmethod
|
99
|
-
def line_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
|
100
|
-
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
101
|
-
if denom == 0:
|
102
|
-
return None # Parallel.
|
103
|
-
ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
|
104
|
-
ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
|
105
|
-
if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:
|
106
|
-
return (x1 + ua * (x2 - x1)), (y1 + ua * (y2 - y1))
|
107
|
-
return None
|
108
|
-
|
109
|
-
|
110
|
-
class Graph:
|
111
|
-
"""
|
112
|
-
A graph is a set of nodes and their connections. The nodes are points within 2d space and any number of segments
|
113
|
-
can connect any number of points. There is no order established by the graph. And for our uses here all graphs will
|
114
|
-
end up not only being Eulerian but Euloopian. All nodes should have even numbers of connecting segments so that any
|
115
|
-
walk will always return to the end location.
|
116
|
-
|
117
|
-
"""
|
118
|
-
|
119
|
-
def __init__(self):
|
120
|
-
self.nodes = []
|
121
|
-
self.links = []
|
122
|
-
|
123
|
-
def add_shape(self, series, close=True):
|
124
|
-
"""
|
125
|
-
Adds a closed shape point series to the graph in a single connected vertex path.
|
126
|
-
"""
|
127
|
-
first_node = None
|
128
|
-
last_node = None
|
129
|
-
for i in range(len(series)):
|
130
|
-
m = series[i]
|
131
|
-
current_node = self.new_node(m)
|
132
|
-
if i == 0:
|
133
|
-
first_node = current_node
|
134
|
-
if last_node is not None:
|
135
|
-
segment = self.link(last_node, current_node)
|
136
|
-
segment.index = i
|
137
|
-
segment.value = "EDGE"
|
138
|
-
last_node = current_node
|
139
|
-
if close:
|
140
|
-
segment = self.link(last_node, first_node)
|
141
|
-
segment.index = len(series)
|
142
|
-
segment.value = "EDGE"
|
143
|
-
|
144
|
-
@staticmethod
|
145
|
-
def monotone_fill(graph, outlines, min, max, distance):
|
146
|
-
"""
|
147
|
-
Find all line segments that intersect with the graph segments in the shape outlines. Add the links from right to
|
148
|
-
left side of the intersected paths. Add the bisectors to the segments that are bisected.
|
149
|
-
|
150
|
-
Sort all the bisectors and create a graph of monotone rungs and the edges that connect those rungs. Use this
|
151
|
-
graph rather than original outline graph used to find the intersections.
|
152
|
-
|
153
|
-
Adds into graph, a graph of all monotone rungs, and the path of edge nodes that connected those intersections.
|
154
|
-
"""
|
155
|
-
crawler = VectorMontonizer(low_value=min, high_value=max, start=min)
|
156
|
-
for outline in outlines:
|
157
|
-
crawler.add_segment_events(outline.links)
|
158
|
-
itr = 0
|
159
|
-
while crawler.current_is_valid_range():
|
160
|
-
crawler.scanline_increment(distance)
|
161
|
-
y = crawler.scanline
|
162
|
-
actives = crawler.actives()
|
163
|
-
for i in range(1, len(actives), 2):
|
164
|
-
left_segment = actives[i - 1]
|
165
|
-
right_segment = actives[i]
|
166
|
-
left_segment_x = crawler.intercept(left_segment)
|
167
|
-
right_segment_x = crawler.intercept(right_segment)
|
168
|
-
left_node = graph.new_node((left_segment_x, y))
|
169
|
-
right_node = graph.new_node((right_segment_x, y))
|
170
|
-
row = graph.link(left_node, right_node)
|
171
|
-
row.value = "RUNG"
|
172
|
-
row.index = itr
|
173
|
-
left_segment.bisectors.append(left_node)
|
174
|
-
right_segment.bisectors.append(right_node)
|
175
|
-
itr += 1
|
176
|
-
for outline in outlines:
|
177
|
-
itr = 0
|
178
|
-
previous = None
|
179
|
-
first = None
|
180
|
-
for i in range(len(outline.links)):
|
181
|
-
s = outline.links[i]
|
182
|
-
if len(s.bisectors) == 0:
|
183
|
-
continue
|
184
|
-
s.sort_bisectors()
|
185
|
-
for bi in s.bisectors:
|
186
|
-
if previous is not None:
|
187
|
-
segment = graph.link(previous, bi)
|
188
|
-
segment.value = "EDGE"
|
189
|
-
segment.index = itr
|
190
|
-
itr += 1
|
191
|
-
else:
|
192
|
-
first = bi
|
193
|
-
previous = bi
|
194
|
-
s.bisectors.clear()
|
195
|
-
if previous is not None and first is not None:
|
196
|
-
segment = graph.link(previous, first)
|
197
|
-
segment.value = "EDGE"
|
198
|
-
segment.index = itr
|
199
|
-
|
200
|
-
def new_node(self, point):
|
201
|
-
"""
|
202
|
-
Create and add a new node to the graph at the given point.
|
203
|
-
"""
|
204
|
-
g = GraphNode(point)
|
205
|
-
self.nodes.append(g)
|
206
|
-
return g
|
207
|
-
|
208
|
-
def new_edge(self, a, b):
|
209
|
-
"""
|
210
|
-
Create an edge connection between a and b.
|
211
|
-
"""
|
212
|
-
s = Segment(a, b)
|
213
|
-
self.links.append(s)
|
214
|
-
return s
|
215
|
-
|
216
|
-
def detach(self, segment):
|
217
|
-
"""
|
218
|
-
Remove the segment and links from the graph.
|
219
|
-
"""
|
220
|
-
self.links.remove(segment)
|
221
|
-
segment.a.connections.remove(segment)
|
222
|
-
segment.b.connections.remove(segment)
|
223
|
-
|
224
|
-
def link(self, a, b):
|
225
|
-
"""
|
226
|
-
Creates a new edge linking the points a and be and adds the newly created link to the graph.
|
227
|
-
"""
|
228
|
-
segment = self.new_edge(a, b)
|
229
|
-
segment.a.connections.append(segment)
|
230
|
-
segment.b.connections.append(segment)
|
231
|
-
return segment
|
232
|
-
|
233
|
-
def double(self):
|
234
|
-
"""
|
235
|
-
Makes any graph Eulerian. Any graph that is doubled is by definition Eulerian.
|
236
|
-
|
237
|
-
This is not used by the algorithm.
|
238
|
-
@return:
|
239
|
-
"""
|
240
|
-
for i in range(len(self.links)):
|
241
|
-
s = self.links[i]
|
242
|
-
second_copy = self.link(s.a, s.b)
|
243
|
-
if s.value == "RUNG":
|
244
|
-
second_copy.value = "SCAFFOLD_RUNG"
|
245
|
-
else:
|
246
|
-
second_copy.value = "SCAFFOLD"
|
247
|
-
second_copy.index = None
|
248
|
-
|
249
|
-
def double_odd_edge(self):
|
250
|
-
"""
|
251
|
-
Makes any outline path an Eularian path, by doubling every other edge. As each node connects with 1 rung, and
|
252
|
-
two edges this will double 1 of those edges in every instance, giving a total of 4 connections. This is makes
|
253
|
-
the graph Eulerian.
|
254
|
-
|
255
|
-
@return:
|
256
|
-
"""
|
257
|
-
for i in range(len(self.links)):
|
258
|
-
segment = self.links[i]
|
259
|
-
if segment.value == "EDGE" and segment.index & 1:
|
260
|
-
second_copy = self.link(segment.a, segment.b)
|
261
|
-
second_copy.value = "SCAFFOLD"
|
262
|
-
second_copy.index = None
|
263
|
-
|
264
|
-
def walk(self, points):
|
265
|
-
"""
|
266
|
-
We have an Eulerian graph we must walk through the graph in any direction. This results in a point series that
|
267
|
-
will cross every segment once.
|
268
|
-
|
269
|
-
Some segments are marked scaffolding or classes of edge that are not necessary. These are removed for parsimony.
|
270
|
-
|
271
|
-
"""
|
272
|
-
if len(self.nodes) == 0:
|
273
|
-
return
|
274
|
-
walker = GraphWalker(self)
|
275
|
-
walker.make_walk()
|
276
|
-
walker.clip_scaffold_ends()
|
277
|
-
walker.clip_scaffold_loops()
|
278
|
-
walker.add_walk(points)
|
279
|
-
return points
|
280
|
-
|
281
|
-
def is_eulerian(self):
|
282
|
-
ends = 0
|
283
|
-
for n in self.nodes:
|
284
|
-
if len(n.connections) & 1:
|
285
|
-
ends += 1
|
286
|
-
if ends > 2:
|
287
|
-
return False
|
288
|
-
return True
|
289
|
-
|
290
|
-
def is_euloopian(self):
|
291
|
-
for n in self.nodes:
|
292
|
-
if len(n.connections) & 1:
|
293
|
-
return False
|
294
|
-
return True
|
295
|
-
|
296
|
-
|
297
|
-
class GraphWalker:
|
298
|
-
"""
|
299
|
-
Graph Walker takes a graph object and finds walks within it.
|
300
|
-
|
301
|
-
If the graph is discontinuous it will find no segment between these elements and add a None segment between them.
|
302
|
-
"""
|
303
|
-
|
304
|
-
def __init__(self, graph):
|
305
|
-
self.graph = graph
|
306
|
-
self.walk = list()
|
307
|
-
self.flip_start = None
|
308
|
-
self.flip_end = None
|
309
|
-
|
310
|
-
def other_node_for_segment(self, current_node, next_segment):
|
311
|
-
"""
|
312
|
-
Segments have two nodes, this finds the other side of the given segment.
|
313
|
-
"""
|
314
|
-
if current_node is next_segment.a:
|
315
|
-
return next_segment.b
|
316
|
-
else:
|
317
|
-
return next_segment.a
|
318
|
-
|
319
|
-
def reset_visited(self):
|
320
|
-
for e in self.walk:
|
321
|
-
if e is None:
|
322
|
-
continue
|
323
|
-
e.visited = 0
|
324
|
-
|
325
|
-
def make_walk(self):
|
326
|
-
"""
|
327
|
-
Create the walk out of the current graph. Picks any start point and begins. Note if there
|
328
|
-
are odd node elements anywhere
|
329
|
-
"""
|
330
|
-
itr = 0
|
331
|
-
for g in self.graph.nodes:
|
332
|
-
if not g.visited:
|
333
|
-
if itr != 0:
|
334
|
-
self.walk.append(None) # Segment is None. There is no link here.
|
335
|
-
self.make_walk_node(g)
|
336
|
-
itr += 1
|
337
|
-
|
338
|
-
def make_walk_node(self, g):
|
339
|
-
"""
|
340
|
-
Starting from the given start node it makes a complete walk in an Eulerian circuit.
|
341
|
-
|
342
|
-
It adds the first loop from the start node, then walks its looped walk adding
|
343
|
-
any additional loops it finds to the current loop.
|
344
|
-
@param g:
|
345
|
-
@return:
|
346
|
-
"""
|
347
|
-
start = len(self.walk)
|
348
|
-
self.walk.append(g)
|
349
|
-
self.add_loop(start, g)
|
350
|
-
|
351
|
-
i = start
|
352
|
-
while i < len(self.walk):
|
353
|
-
node = self.walk[i]
|
354
|
-
unused = self.find_unused_connection(node)
|
355
|
-
if unused is None:
|
356
|
-
i += 2
|
357
|
-
continue
|
358
|
-
self.add_loop(i, node)
|
359
|
-
# i += 2
|
360
|
-
|
361
|
-
def add_loop(self, index, node):
|
362
|
-
"""
|
363
|
-
Adds a loop from the current graph node, without revisiting any nodes.
|
364
|
-
Returns the altered index caused by adding that loop.
|
365
|
-
|
366
|
-
Travels along unused connections until no more travel is possible. If properly Eulerian,
|
367
|
-
this will only happen when it is looped back on itself.
|
368
|
-
|
369
|
-
@param index: index we are adding loop to.
|
370
|
-
@param node: Node to find alternative path through.
|
371
|
-
@return: new index after loop is added to the walk.
|
372
|
-
"""
|
373
|
-
index += 1
|
374
|
-
i = index
|
375
|
-
while True:
|
376
|
-
node.visited += 1
|
377
|
-
unused = self.find_unused_connection(node)
|
378
|
-
if unused is None:
|
379
|
-
break
|
380
|
-
segment = node.connections[unused]
|
381
|
-
self.walk.insert(i, segment)
|
382
|
-
i += 1
|
383
|
-
segment.visited += 1
|
384
|
-
node = self.other_node_for_segment(node, segment)
|
385
|
-
self.walk.insert(i, node)
|
386
|
-
i += 1
|
387
|
-
return i - index
|
388
|
-
|
389
|
-
def find_unused_connection(self, node):
|
390
|
-
"""
|
391
|
-
Finds the first unused edge segment within the graph node, or None if all connections are used.
|
392
|
-
|
393
|
-
@param node: Node to find unused edge segment within.
|
394
|
-
@return: index of node connection within the graphnode
|
395
|
-
"""
|
396
|
-
value = None
|
397
|
-
for index, c in enumerate(node.connections):
|
398
|
-
if not c.visited:
|
399
|
-
if value is None:
|
400
|
-
value = index
|
401
|
-
if c.value == "RUNG":
|
402
|
-
return index
|
403
|
-
return value
|
404
|
-
|
405
|
-
def add_walk(self, points):
|
406
|
-
"""
|
407
|
-
Adds nodes within the walk to the points given to it.
|
408
|
-
|
409
|
-
@param points:
|
410
|
-
@return:
|
411
|
-
"""
|
412
|
-
for i in range(0, len(self.walk), 2):
|
413
|
-
segment = self.walk[i - 1]
|
414
|
-
# The first time segment will be the last value (a node) which will set value to none. This is fine.
|
415
|
-
point = self.walk[i]
|
416
|
-
if segment is None:
|
417
|
-
points.append(None)
|
418
|
-
else:
|
419
|
-
point.value = (
|
420
|
-
segment.value
|
421
|
-
) # This doesn't work, nodes are repeated, so they can't store unique values.
|
422
|
-
points.append(point)
|
423
|
-
|
424
|
-
def remove_loop(self, from_pos, to_pos):
|
425
|
-
"""
|
426
|
-
Removes values between the two given points.
|
427
|
-
Since start and end are the same node, it leaves one in place.
|
428
|
-
|
429
|
-
@param from_pos:
|
430
|
-
@param to_pos:
|
431
|
-
@return:
|
432
|
-
"""
|
433
|
-
if from_pos == to_pos:
|
434
|
-
return 0
|
435
|
-
min_pos = min(from_pos, to_pos)
|
436
|
-
max_pos = max(from_pos, to_pos)
|
437
|
-
del self.walk[min_pos:max_pos]
|
438
|
-
return max_pos - min_pos
|
439
|
-
|
440
|
-
def remove_biggest_loop_in_range(self, start, end):
|
441
|
-
"""
|
442
|
-
Checks scaffolding walk for loops, and removes them if detected.
|
443
|
-
|
444
|
-
It resets the visited values for the scaffold walk.
|
445
|
-
It iterates from the outside to the center, setting the visited value for each node.
|
446
|
-
|
447
|
-
If it finds a marked node, that is the biggest loop within the given walk.
|
448
|
-
@param start:
|
449
|
-
@param end:
|
450
|
-
@return:
|
451
|
-
"""
|
452
|
-
for i in range(start, end + 2, 2):
|
453
|
-
n = self.get_node(i)
|
454
|
-
n.visited = None
|
455
|
-
for i in range(0, int((end - start) // 2), 2):
|
456
|
-
left = start + i
|
457
|
-
right = end - i
|
458
|
-
s = self.get_node(left)
|
459
|
-
if s.visited is not None:
|
460
|
-
return self.remove_loop(left, s.visited)
|
461
|
-
# Loop Detected.
|
462
|
-
if left == right:
|
463
|
-
break
|
464
|
-
s.visited = left
|
465
|
-
e = self.get_node(right)
|
466
|
-
if e.visited is not None:
|
467
|
-
return self.remove_loop(right, e.visited)
|
468
|
-
# Loop Detected.
|
469
|
-
e.visited = right
|
470
|
-
return 0
|
471
|
-
|
472
|
-
def clip_scaffold_loops(self):
|
473
|
-
"""
|
474
|
-
Removes loops consisting of scaffolding from the walk.
|
475
|
-
|
476
|
-
Clips unneeded scaffolding.
|
477
|
-
|
478
|
-
@return:
|
479
|
-
"""
|
480
|
-
start = 0
|
481
|
-
index = 0
|
482
|
-
ie = len(self.walk)
|
483
|
-
while index < ie:
|
484
|
-
try:
|
485
|
-
segment = self.walk[index + 1]
|
486
|
-
except IndexError:
|
487
|
-
self.remove_biggest_loop_in_range(start, index)
|
488
|
-
return
|
489
|
-
if segment is None or segment.value == "RUNG":
|
490
|
-
# Segment is essential.
|
491
|
-
if start != index:
|
492
|
-
ie -= self.remove_biggest_loop_in_range(start, index)
|
493
|
-
start = index + 2
|
494
|
-
index += 2
|
495
|
-
|
496
|
-
def remove_scaffold_ends_in_range(self, start, end):
|
497
|
-
new_end = end
|
498
|
-
limit = start + 2
|
499
|
-
while new_end >= limit:
|
500
|
-
j_segment = self.walk[new_end - 1]
|
501
|
-
if j_segment is None or j_segment.value == "RUNG":
|
502
|
-
if new_end == end:
|
503
|
-
break
|
504
|
-
del self.walk[new_end + 1 : end + 1]
|
505
|
-
end = new_end
|
506
|
-
break
|
507
|
-
new_end -= 2
|
508
|
-
new_start = start
|
509
|
-
limit = end - 2
|
510
|
-
while new_start <= limit:
|
511
|
-
j_segment = self.walk[new_start + 1]
|
512
|
-
if j_segment is None or j_segment.value == "RUNG":
|
513
|
-
if new_start == start:
|
514
|
-
break
|
515
|
-
del self.walk[start:new_start]
|
516
|
-
break
|
517
|
-
new_start += 2
|
518
|
-
|
519
|
-
def clip_scaffold_ends(self):
|
520
|
-
"""Finds contiguous regions, and calls removeScaffoldEnds on that range."""
|
521
|
-
end = len(self.walk) - 1
|
522
|
-
index = end
|
523
|
-
while index >= 0:
|
524
|
-
try:
|
525
|
-
segment = self.walk[index - 1]
|
526
|
-
except IndexError:
|
527
|
-
self.remove_scaffold_ends_in_range(index, end)
|
528
|
-
return
|
529
|
-
if segment is None:
|
530
|
-
self.remove_scaffold_ends_in_range(index, end)
|
531
|
-
end = index - 2
|
532
|
-
index -= 2
|
533
|
-
|
534
|
-
def two_opt(self):
|
535
|
-
"""
|
536
|
-
Unused
|
537
|
-
"""
|
538
|
-
v = self.get_value()
|
539
|
-
while True:
|
540
|
-
new_value = self.two_opt_cycle(v)
|
541
|
-
if v == new_value:
|
542
|
-
break
|
543
|
-
|
544
|
-
def two_opt_cycle(self, value):
|
545
|
-
"""
|
546
|
-
Unused
|
547
|
-
"""
|
548
|
-
if len(self.walk) == 0:
|
549
|
-
return 0
|
550
|
-
swap_start = 0
|
551
|
-
walk_end = len(self.walk)
|
552
|
-
while swap_start < walk_end:
|
553
|
-
swap_element = self.walk[swap_start]
|
554
|
-
m = swap_element.visited
|
555
|
-
swap_end = swap_start + 2
|
556
|
-
while swap_end < walk_end:
|
557
|
-
current_element = self.walk[swap_end]
|
558
|
-
if swap_element == current_element:
|
559
|
-
m -= 1
|
560
|
-
self.flip_start = swap_start + 1
|
561
|
-
self.flip_end = swap_end - 1
|
562
|
-
new_value = self.get_value()
|
563
|
-
if new_value > value:
|
564
|
-
value = new_value
|
565
|
-
self.walk[swap_start + 1 : swap_end] = self.walk[
|
566
|
-
swap_start + 1 : swap_end : -1
|
567
|
-
] # reverse
|
568
|
-
else:
|
569
|
-
self.flip_start = None
|
570
|
-
self.flip_end = None
|
571
|
-
if m == 0:
|
572
|
-
break
|
573
|
-
swap_end += 2
|
574
|
-
swap_start += 2
|
575
|
-
return value
|
576
|
-
|
577
|
-
def get_segment(self, index):
|
578
|
-
"""
|
579
|
-
Unused
|
580
|
-
"""
|
581
|
-
if (
|
582
|
-
self.flip_start is not None
|
583
|
-
and self.flip_end is not None
|
584
|
-
and self.flip_start <= index <= self.flip_end
|
585
|
-
):
|
586
|
-
return self.walk[self.flip_end - (index - self.flip_start)]
|
587
|
-
return self.walk[index]
|
588
|
-
|
589
|
-
def get_node(self, index):
|
590
|
-
"""
|
591
|
-
Unused
|
592
|
-
"""
|
593
|
-
if (
|
594
|
-
self.flip_start is not None
|
595
|
-
and self.flip_end is not None
|
596
|
-
and self.flip_start <= index <= self.flip_end
|
597
|
-
):
|
598
|
-
return self.walk[self.flip_end - (index - self.flip_start)]
|
599
|
-
try:
|
600
|
-
return self.walk[index]
|
601
|
-
except IndexError:
|
602
|
-
return None
|
603
|
-
|
604
|
-
def get_value(self):
|
605
|
-
"""
|
606
|
-
Path values with flip.
|
607
|
-
@return: Flipped path value.
|
608
|
-
"""
|
609
|
-
if len(self.walk) == 0:
|
610
|
-
return 0
|
611
|
-
value = 0
|
612
|
-
start = 0
|
613
|
-
end = len(self.walk) - 1
|
614
|
-
while start < end:
|
615
|
-
i_segment = self.get_segment(start + 1)
|
616
|
-
if i_segment.value == "RUNG":
|
617
|
-
break
|
618
|
-
start += 2
|
619
|
-
while end >= 2:
|
620
|
-
i_segment = self.get_segment(end - 1)
|
621
|
-
if i_segment.value == "RUNG":
|
622
|
-
break
|
623
|
-
end -= 2
|
624
|
-
j = start
|
625
|
-
while j < end:
|
626
|
-
j_node = self.get_node(j)
|
627
|
-
j += 1
|
628
|
-
j_segment = self.get_segment(j)
|
629
|
-
j += 1
|
630
|
-
if j_segment.value != "RUNG":
|
631
|
-
# if the node connector is not critical, try to find and skip a loop
|
632
|
-
k = j
|
633
|
-
while k < end:
|
634
|
-
k_node = self.get_node(k)
|
635
|
-
k += 1
|
636
|
-
k_segment = self.get_segment(k)
|
637
|
-
k += 1
|
638
|
-
if k_segment.value == "RUNG":
|
639
|
-
break
|
640
|
-
if k_node == j_node:
|
641
|
-
# Only skippable nodes existed before returned to original node, so skip that loop.
|
642
|
-
value += (k - j) * 10
|
643
|
-
j = k
|
644
|
-
j_segment = k_segment
|
645
|
-
break
|
646
|
-
if j_segment.value == "SCAFFOLD":
|
647
|
-
value -= j_segment.a.distance_sq(j_segment.b)
|
648
|
-
elif j_segment.value == "RUNG":
|
649
|
-
value -= j_segment.a.distance_sq(j_segment.b)
|
650
|
-
return value
|
651
|
-
|
652
|
-
|
653
|
-
class VectorMontonizer:
|
654
|
-
"""
|
655
|
-
Sorts all segments according to their highest y values. Steps through the values in order
|
656
|
-
each step activates and deactivates the segments that are encountered such that it always has a list
|
657
|
-
of active segments. Sorting the active segments according to their x-intercepts gives a list of all
|
658
|
-
points that a ray would strike passing through that shape. Every other such area is filled. These are
|
659
|
-
given rungs, and connected to intercept points.
|
660
|
-
"""
|
661
|
-
|
662
|
-
def __init__(
|
663
|
-
self, low_value=-float("inf"), high_value=float("inf"), start=-float("inf")
|
664
|
-
):
|
665
|
-
self._event_index = 0
|
666
|
-
self._events = []
|
667
|
-
self._dirty_event_sort = True
|
668
|
-
|
669
|
-
self._actives = []
|
670
|
-
self._dirty_actives_sort = True
|
671
|
-
|
672
|
-
self._dirty_scanline = True
|
673
|
-
|
674
|
-
self.scanline = start
|
675
|
-
self.valid_low = low_value
|
676
|
-
self.valid_high = high_value
|
677
|
-
|
678
|
-
self.scanbeam_low = float("inf")
|
679
|
-
self.scanbeam_high = -float("inf")
|
680
|
-
|
681
|
-
def add_segment_events(self, links):
|
682
|
-
"""
|
683
|
-
Add segment to be processed. This segment should already exist and have the correct type
|
684
|
-
@param links:
|
685
|
-
@return:
|
686
|
-
"""
|
687
|
-
self._dirty_scanline = True
|
688
|
-
self._dirty_event_sort = True
|
689
|
-
self._dirty_actives_sort = True
|
690
|
-
for s in links:
|
691
|
-
self._events.append((s[4].y, s)) # High
|
692
|
-
self._events.append((s[5].y, s)) # Low
|
693
|
-
|
694
|
-
def add_polyline(self, path):
|
695
|
-
"""
|
696
|
-
Add segments in the form of a connected path. These positions are read and segments are created for these
|
697
|
-
points.
|
698
|
-
|
699
|
-
@param path:
|
700
|
-
@return:
|
701
|
-
"""
|
702
|
-
self._dirty_scanline = True
|
703
|
-
self._dirty_event_sort = True
|
704
|
-
self._dirty_actives_sort = True
|
705
|
-
for i in range(len(path) - 1):
|
706
|
-
p0 = path[i]
|
707
|
-
p1 = path[i + 1]
|
708
|
-
if p0.y > p1.y:
|
709
|
-
high = p0
|
710
|
-
low = p1
|
711
|
-
else:
|
712
|
-
high = p1
|
713
|
-
low = p0
|
714
|
-
|
715
|
-
# b = low.y - (m * low.x)
|
716
|
-
if self.valid_low > high.y:
|
717
|
-
# Cluster before range.
|
718
|
-
continue
|
719
|
-
if self.valid_high < low.y:
|
720
|
-
# Cluster after range.
|
721
|
-
continue
|
722
|
-
seg = Segment(p0, p1)
|
723
|
-
# cluster = [False, i, p0, p1, high, low, m, b, path]
|
724
|
-
if self.valid_low < low.y:
|
725
|
-
self._events.append((low.y, seg))
|
726
|
-
if self.valid_high > high.y:
|
727
|
-
self._events.append((high.y, seg))
|
728
|
-
if high.y >= self.scanline >= low.y:
|
729
|
-
seg.active = True
|
730
|
-
self._actives.append(seg)
|
731
|
-
|
732
|
-
def
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
return
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
self.
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
def
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
"""
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
)
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
self.
|
819
|
-
self.
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
return
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
def
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
self.
|
883
|
-
self.
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
"""
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
self.
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
1
|
+
from math import isinf, isnan
|
2
|
+
|
3
|
+
from meerk40t.svgelements import Point
|
4
|
+
|
5
|
+
|
6
|
+
class GraphNode(Point):
|
7
|
+
"""
|
8
|
+
GraphNodes are nodes within the graph that store a list of connections between points.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, x, y=None):
|
12
|
+
Point.__init__(self, x, y)
|
13
|
+
self.connections = []
|
14
|
+
self.visited = 0
|
15
|
+
self.value = None
|
16
|
+
|
17
|
+
|
18
|
+
class Segment:
|
19
|
+
"""
|
20
|
+
Graphing segments are connections between nodes on the graph that store, their start and end nodes, active state
|
21
|
+
for use within the monotonic vector filling. The type of segment it is. The index of the segment around the closed
|
22
|
+
shape. A list of bisectors (to calculate the rung attachments).
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, a, b, index=0):
|
26
|
+
self.visited = 0
|
27
|
+
self.a = a
|
28
|
+
self.b = b
|
29
|
+
self.active = False
|
30
|
+
self.value = "RUNG"
|
31
|
+
self.index = index
|
32
|
+
self.bisectors = []
|
33
|
+
self.object = None
|
34
|
+
|
35
|
+
def __len__(self):
|
36
|
+
# [False, i, p0, p1, high, low, m, b, path]
|
37
|
+
return 9
|
38
|
+
|
39
|
+
def __str__(self):
|
40
|
+
return f"Segment({str(self.a)},{str(self.b)},{str(self.index)},type='{self.value}')"
|
41
|
+
|
42
|
+
def __getitem__(self, item):
|
43
|
+
if item == 0:
|
44
|
+
return self.active
|
45
|
+
if item == 1:
|
46
|
+
return self.index
|
47
|
+
if item == 2:
|
48
|
+
return self.a
|
49
|
+
if item == 3:
|
50
|
+
return self.b
|
51
|
+
if item == 4:
|
52
|
+
if self.a.y > self.b.y:
|
53
|
+
return self.a
|
54
|
+
else:
|
55
|
+
return self.b
|
56
|
+
if item == 5:
|
57
|
+
if self.a.y < self.b.y:
|
58
|
+
return self.a
|
59
|
+
else:
|
60
|
+
return self.b
|
61
|
+
if item == 6:
|
62
|
+
if self.b[0] - self.a[0] == 0:
|
63
|
+
return float("inf")
|
64
|
+
return (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
65
|
+
if item == 7:
|
66
|
+
if self.b[0] - self.a[0] == 0:
|
67
|
+
return float("inf")
|
68
|
+
im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
69
|
+
return self.a[1] - (im * self.a[0])
|
70
|
+
if item == 8:
|
71
|
+
return self.object
|
72
|
+
|
73
|
+
def intersect(self, segment):
|
74
|
+
return Segment.line_intersect(
|
75
|
+
self.a[0],
|
76
|
+
self.a[1],
|
77
|
+
self.b[0],
|
78
|
+
self.b[1],
|
79
|
+
segment.a[0],
|
80
|
+
segment.a[1],
|
81
|
+
segment.b[0],
|
82
|
+
segment.b[1],
|
83
|
+
)
|
84
|
+
|
85
|
+
def sort_bisectors(self):
|
86
|
+
def distance(a):
|
87
|
+
return self.a.distance_to(a)
|
88
|
+
|
89
|
+
self.bisectors.sort(key=distance)
|
90
|
+
|
91
|
+
def get_intercept(self, y):
|
92
|
+
im = (self.b[1] - self.a[1]) / (self.b[0] - self.a[0])
|
93
|
+
ib = self.a[1] - (im * self.a[0])
|
94
|
+
if isnan(im) or isinf(im):
|
95
|
+
return self.a[0]
|
96
|
+
return (y - ib) / im
|
97
|
+
|
98
|
+
@staticmethod
|
99
|
+
def line_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
|
100
|
+
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
101
|
+
if denom == 0:
|
102
|
+
return None # Parallel.
|
103
|
+
ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
|
104
|
+
ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
|
105
|
+
if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:
|
106
|
+
return (x1 + ua * (x2 - x1)), (y1 + ua * (y2 - y1))
|
107
|
+
return None
|
108
|
+
|
109
|
+
|
110
|
+
class Graph:
|
111
|
+
"""
|
112
|
+
A graph is a set of nodes and their connections. The nodes are points within 2d space and any number of segments
|
113
|
+
can connect any number of points. There is no order established by the graph. And for our uses here all graphs will
|
114
|
+
end up not only being Eulerian but Euloopian. All nodes should have even numbers of connecting segments so that any
|
115
|
+
walk will always return to the end location.
|
116
|
+
|
117
|
+
"""
|
118
|
+
|
119
|
+
def __init__(self):
|
120
|
+
self.nodes = []
|
121
|
+
self.links = []
|
122
|
+
|
123
|
+
def add_shape(self, series, close=True):
|
124
|
+
"""
|
125
|
+
Adds a closed shape point series to the graph in a single connected vertex path.
|
126
|
+
"""
|
127
|
+
first_node = None
|
128
|
+
last_node = None
|
129
|
+
for i in range(len(series)):
|
130
|
+
m = series[i]
|
131
|
+
current_node = self.new_node(m)
|
132
|
+
if i == 0:
|
133
|
+
first_node = current_node
|
134
|
+
if last_node is not None:
|
135
|
+
segment = self.link(last_node, current_node)
|
136
|
+
segment.index = i
|
137
|
+
segment.value = "EDGE"
|
138
|
+
last_node = current_node
|
139
|
+
if close:
|
140
|
+
segment = self.link(last_node, first_node)
|
141
|
+
segment.index = len(series)
|
142
|
+
segment.value = "EDGE"
|
143
|
+
|
144
|
+
@staticmethod
|
145
|
+
def monotone_fill(graph, outlines, min, max, distance):
|
146
|
+
"""
|
147
|
+
Find all line segments that intersect with the graph segments in the shape outlines. Add the links from right to
|
148
|
+
left side of the intersected paths. Add the bisectors to the segments that are bisected.
|
149
|
+
|
150
|
+
Sort all the bisectors and create a graph of monotone rungs and the edges that connect those rungs. Use this
|
151
|
+
graph rather than original outline graph used to find the intersections.
|
152
|
+
|
153
|
+
Adds into graph, a graph of all monotone rungs, and the path of edge nodes that connected those intersections.
|
154
|
+
"""
|
155
|
+
crawler = VectorMontonizer(low_value=min, high_value=max, start=min)
|
156
|
+
for outline in outlines:
|
157
|
+
crawler.add_segment_events(outline.links)
|
158
|
+
itr = 0
|
159
|
+
while crawler.current_is_valid_range():
|
160
|
+
crawler.scanline_increment(distance)
|
161
|
+
y = crawler.scanline
|
162
|
+
actives = crawler.actives()
|
163
|
+
for i in range(1, len(actives), 2):
|
164
|
+
left_segment = actives[i - 1]
|
165
|
+
right_segment = actives[i]
|
166
|
+
left_segment_x = crawler.intercept(left_segment)
|
167
|
+
right_segment_x = crawler.intercept(right_segment)
|
168
|
+
left_node = graph.new_node((left_segment_x, y))
|
169
|
+
right_node = graph.new_node((right_segment_x, y))
|
170
|
+
row = graph.link(left_node, right_node)
|
171
|
+
row.value = "RUNG"
|
172
|
+
row.index = itr
|
173
|
+
left_segment.bisectors.append(left_node)
|
174
|
+
right_segment.bisectors.append(right_node)
|
175
|
+
itr += 1
|
176
|
+
for outline in outlines:
|
177
|
+
itr = 0
|
178
|
+
previous = None
|
179
|
+
first = None
|
180
|
+
for i in range(len(outline.links)):
|
181
|
+
s = outline.links[i]
|
182
|
+
if len(s.bisectors) == 0:
|
183
|
+
continue
|
184
|
+
s.sort_bisectors()
|
185
|
+
for bi in s.bisectors:
|
186
|
+
if previous is not None:
|
187
|
+
segment = graph.link(previous, bi)
|
188
|
+
segment.value = "EDGE"
|
189
|
+
segment.index = itr
|
190
|
+
itr += 1
|
191
|
+
else:
|
192
|
+
first = bi
|
193
|
+
previous = bi
|
194
|
+
s.bisectors.clear()
|
195
|
+
if previous is not None and first is not None:
|
196
|
+
segment = graph.link(previous, first)
|
197
|
+
segment.value = "EDGE"
|
198
|
+
segment.index = itr
|
199
|
+
|
200
|
+
def new_node(self, point):
|
201
|
+
"""
|
202
|
+
Create and add a new node to the graph at the given point.
|
203
|
+
"""
|
204
|
+
g = GraphNode(point)
|
205
|
+
self.nodes.append(g)
|
206
|
+
return g
|
207
|
+
|
208
|
+
def new_edge(self, a, b):
|
209
|
+
"""
|
210
|
+
Create an edge connection between a and b.
|
211
|
+
"""
|
212
|
+
s = Segment(a, b)
|
213
|
+
self.links.append(s)
|
214
|
+
return s
|
215
|
+
|
216
|
+
def detach(self, segment):
|
217
|
+
"""
|
218
|
+
Remove the segment and links from the graph.
|
219
|
+
"""
|
220
|
+
self.links.remove(segment)
|
221
|
+
segment.a.connections.remove(segment)
|
222
|
+
segment.b.connections.remove(segment)
|
223
|
+
|
224
|
+
def link(self, a, b):
|
225
|
+
"""
|
226
|
+
Creates a new edge linking the points a and be and adds the newly created link to the graph.
|
227
|
+
"""
|
228
|
+
segment = self.new_edge(a, b)
|
229
|
+
segment.a.connections.append(segment)
|
230
|
+
segment.b.connections.append(segment)
|
231
|
+
return segment
|
232
|
+
|
233
|
+
def double(self):
|
234
|
+
"""
|
235
|
+
Makes any graph Eulerian. Any graph that is doubled is by definition Eulerian.
|
236
|
+
|
237
|
+
This is not used by the algorithm.
|
238
|
+
@return:
|
239
|
+
"""
|
240
|
+
for i in range(len(self.links)):
|
241
|
+
s = self.links[i]
|
242
|
+
second_copy = self.link(s.a, s.b)
|
243
|
+
if s.value == "RUNG":
|
244
|
+
second_copy.value = "SCAFFOLD_RUNG"
|
245
|
+
else:
|
246
|
+
second_copy.value = "SCAFFOLD"
|
247
|
+
second_copy.index = None
|
248
|
+
|
249
|
+
def double_odd_edge(self):
|
250
|
+
"""
|
251
|
+
Makes any outline path an Eularian path, by doubling every other edge. As each node connects with 1 rung, and
|
252
|
+
two edges this will double 1 of those edges in every instance, giving a total of 4 connections. This is makes
|
253
|
+
the graph Eulerian.
|
254
|
+
|
255
|
+
@return:
|
256
|
+
"""
|
257
|
+
for i in range(len(self.links)):
|
258
|
+
segment = self.links[i]
|
259
|
+
if segment.value == "EDGE" and segment.index & 1:
|
260
|
+
second_copy = self.link(segment.a, segment.b)
|
261
|
+
second_copy.value = "SCAFFOLD"
|
262
|
+
second_copy.index = None
|
263
|
+
|
264
|
+
def walk(self, points):
|
265
|
+
"""
|
266
|
+
We have an Eulerian graph we must walk through the graph in any direction. This results in a point series that
|
267
|
+
will cross every segment once.
|
268
|
+
|
269
|
+
Some segments are marked scaffolding or classes of edge that are not necessary. These are removed for parsimony.
|
270
|
+
|
271
|
+
"""
|
272
|
+
if len(self.nodes) == 0:
|
273
|
+
return
|
274
|
+
walker = GraphWalker(self)
|
275
|
+
walker.make_walk()
|
276
|
+
walker.clip_scaffold_ends()
|
277
|
+
walker.clip_scaffold_loops()
|
278
|
+
walker.add_walk(points)
|
279
|
+
return points
|
280
|
+
|
281
|
+
def is_eulerian(self):
|
282
|
+
ends = 0
|
283
|
+
for n in self.nodes:
|
284
|
+
if len(n.connections) & 1:
|
285
|
+
ends += 1
|
286
|
+
if ends > 2:
|
287
|
+
return False
|
288
|
+
return True
|
289
|
+
|
290
|
+
def is_euloopian(self):
|
291
|
+
for n in self.nodes:
|
292
|
+
if len(n.connections) & 1:
|
293
|
+
return False
|
294
|
+
return True
|
295
|
+
|
296
|
+
|
297
|
+
class GraphWalker:
|
298
|
+
"""
|
299
|
+
Graph Walker takes a graph object and finds walks within it.
|
300
|
+
|
301
|
+
If the graph is discontinuous it will find no segment between these elements and add a None segment between them.
|
302
|
+
"""
|
303
|
+
|
304
|
+
def __init__(self, graph):
|
305
|
+
self.graph = graph
|
306
|
+
self.walk = list()
|
307
|
+
self.flip_start = None
|
308
|
+
self.flip_end = None
|
309
|
+
|
310
|
+
def other_node_for_segment(self, current_node, next_segment):
|
311
|
+
"""
|
312
|
+
Segments have two nodes, this finds the other side of the given segment.
|
313
|
+
"""
|
314
|
+
if current_node is next_segment.a:
|
315
|
+
return next_segment.b
|
316
|
+
else:
|
317
|
+
return next_segment.a
|
318
|
+
|
319
|
+
def reset_visited(self):
|
320
|
+
for e in self.walk:
|
321
|
+
if e is None:
|
322
|
+
continue
|
323
|
+
e.visited = 0
|
324
|
+
|
325
|
+
def make_walk(self):
|
326
|
+
"""
|
327
|
+
Create the walk out of the current graph. Picks any start point and begins. Note if there
|
328
|
+
are odd node elements anywhere
|
329
|
+
"""
|
330
|
+
itr = 0
|
331
|
+
for g in self.graph.nodes:
|
332
|
+
if not g.visited:
|
333
|
+
if itr != 0:
|
334
|
+
self.walk.append(None) # Segment is None. There is no link here.
|
335
|
+
self.make_walk_node(g)
|
336
|
+
itr += 1
|
337
|
+
|
338
|
+
def make_walk_node(self, g):
|
339
|
+
"""
|
340
|
+
Starting from the given start node it makes a complete walk in an Eulerian circuit.
|
341
|
+
|
342
|
+
It adds the first loop from the start node, then walks its looped walk adding
|
343
|
+
any additional loops it finds to the current loop.
|
344
|
+
@param g:
|
345
|
+
@return:
|
346
|
+
"""
|
347
|
+
start = len(self.walk)
|
348
|
+
self.walk.append(g)
|
349
|
+
self.add_loop(start, g)
|
350
|
+
|
351
|
+
i = start
|
352
|
+
while i < len(self.walk):
|
353
|
+
node = self.walk[i]
|
354
|
+
unused = self.find_unused_connection(node)
|
355
|
+
if unused is None:
|
356
|
+
i += 2
|
357
|
+
continue
|
358
|
+
self.add_loop(i, node)
|
359
|
+
# i += 2
|
360
|
+
|
361
|
+
def add_loop(self, index, node):
|
362
|
+
"""
|
363
|
+
Adds a loop from the current graph node, without revisiting any nodes.
|
364
|
+
Returns the altered index caused by adding that loop.
|
365
|
+
|
366
|
+
Travels along unused connections until no more travel is possible. If properly Eulerian,
|
367
|
+
this will only happen when it is looped back on itself.
|
368
|
+
|
369
|
+
@param index: index we are adding loop to.
|
370
|
+
@param node: Node to find alternative path through.
|
371
|
+
@return: new index after loop is added to the walk.
|
372
|
+
"""
|
373
|
+
index += 1
|
374
|
+
i = index
|
375
|
+
while True:
|
376
|
+
node.visited += 1
|
377
|
+
unused = self.find_unused_connection(node)
|
378
|
+
if unused is None:
|
379
|
+
break
|
380
|
+
segment = node.connections[unused]
|
381
|
+
self.walk.insert(i, segment)
|
382
|
+
i += 1
|
383
|
+
segment.visited += 1
|
384
|
+
node = self.other_node_for_segment(node, segment)
|
385
|
+
self.walk.insert(i, node)
|
386
|
+
i += 1
|
387
|
+
return i - index
|
388
|
+
|
389
|
+
def find_unused_connection(self, node):
|
390
|
+
"""
|
391
|
+
Finds the first unused edge segment within the graph node, or None if all connections are used.
|
392
|
+
|
393
|
+
@param node: Node to find unused edge segment within.
|
394
|
+
@return: index of node connection within the graphnode
|
395
|
+
"""
|
396
|
+
value = None
|
397
|
+
for index, c in enumerate(node.connections):
|
398
|
+
if not c.visited:
|
399
|
+
if value is None:
|
400
|
+
value = index
|
401
|
+
if c.value == "RUNG":
|
402
|
+
return index
|
403
|
+
return value
|
404
|
+
|
405
|
+
def add_walk(self, points):
|
406
|
+
"""
|
407
|
+
Adds nodes within the walk to the points given to it.
|
408
|
+
|
409
|
+
@param points:
|
410
|
+
@return:
|
411
|
+
"""
|
412
|
+
for i in range(0, len(self.walk), 2):
|
413
|
+
segment = self.walk[i - 1]
|
414
|
+
# The first time segment will be the last value (a node) which will set value to none. This is fine.
|
415
|
+
point = self.walk[i]
|
416
|
+
if segment is None:
|
417
|
+
points.append(None)
|
418
|
+
else:
|
419
|
+
point.value = (
|
420
|
+
segment.value
|
421
|
+
) # This doesn't work, nodes are repeated, so they can't store unique values.
|
422
|
+
points.append(point)
|
423
|
+
|
424
|
+
def remove_loop(self, from_pos, to_pos):
|
425
|
+
"""
|
426
|
+
Removes values between the two given points.
|
427
|
+
Since start and end are the same node, it leaves one in place.
|
428
|
+
|
429
|
+
@param from_pos:
|
430
|
+
@param to_pos:
|
431
|
+
@return:
|
432
|
+
"""
|
433
|
+
if from_pos == to_pos:
|
434
|
+
return 0
|
435
|
+
min_pos = min(from_pos, to_pos)
|
436
|
+
max_pos = max(from_pos, to_pos)
|
437
|
+
del self.walk[min_pos:max_pos]
|
438
|
+
return max_pos - min_pos
|
439
|
+
|
440
|
+
def remove_biggest_loop_in_range(self, start, end):
|
441
|
+
"""
|
442
|
+
Checks scaffolding walk for loops, and removes them if detected.
|
443
|
+
|
444
|
+
It resets the visited values for the scaffold walk.
|
445
|
+
It iterates from the outside to the center, setting the visited value for each node.
|
446
|
+
|
447
|
+
If it finds a marked node, that is the biggest loop within the given walk.
|
448
|
+
@param start:
|
449
|
+
@param end:
|
450
|
+
@return:
|
451
|
+
"""
|
452
|
+
for i in range(start, end + 2, 2):
|
453
|
+
n = self.get_node(i)
|
454
|
+
n.visited = None
|
455
|
+
for i in range(0, int((end - start) // 2), 2):
|
456
|
+
left = start + i
|
457
|
+
right = end - i
|
458
|
+
s = self.get_node(left)
|
459
|
+
if s.visited is not None:
|
460
|
+
return self.remove_loop(left, s.visited)
|
461
|
+
# Loop Detected.
|
462
|
+
if left == right:
|
463
|
+
break
|
464
|
+
s.visited = left
|
465
|
+
e = self.get_node(right)
|
466
|
+
if e.visited is not None:
|
467
|
+
return self.remove_loop(right, e.visited)
|
468
|
+
# Loop Detected.
|
469
|
+
e.visited = right
|
470
|
+
return 0
|
471
|
+
|
472
|
+
def clip_scaffold_loops(self):
|
473
|
+
"""
|
474
|
+
Removes loops consisting of scaffolding from the walk.
|
475
|
+
|
476
|
+
Clips unneeded scaffolding.
|
477
|
+
|
478
|
+
@return:
|
479
|
+
"""
|
480
|
+
start = 0
|
481
|
+
index = 0
|
482
|
+
ie = len(self.walk)
|
483
|
+
while index < ie:
|
484
|
+
try:
|
485
|
+
segment = self.walk[index + 1]
|
486
|
+
except IndexError:
|
487
|
+
self.remove_biggest_loop_in_range(start, index)
|
488
|
+
return
|
489
|
+
if segment is None or segment.value == "RUNG":
|
490
|
+
# Segment is essential.
|
491
|
+
if start != index:
|
492
|
+
ie -= self.remove_biggest_loop_in_range(start, index)
|
493
|
+
start = index + 2
|
494
|
+
index += 2
|
495
|
+
|
496
|
+
def remove_scaffold_ends_in_range(self, start, end):
|
497
|
+
new_end = end
|
498
|
+
limit = start + 2
|
499
|
+
while new_end >= limit:
|
500
|
+
j_segment = self.walk[new_end - 1]
|
501
|
+
if j_segment is None or j_segment.value == "RUNG":
|
502
|
+
if new_end == end:
|
503
|
+
break
|
504
|
+
del self.walk[new_end + 1 : end + 1]
|
505
|
+
end = new_end
|
506
|
+
break
|
507
|
+
new_end -= 2
|
508
|
+
new_start = start
|
509
|
+
limit = end - 2
|
510
|
+
while new_start <= limit:
|
511
|
+
j_segment = self.walk[new_start + 1]
|
512
|
+
if j_segment is None or j_segment.value == "RUNG":
|
513
|
+
if new_start == start:
|
514
|
+
break
|
515
|
+
del self.walk[start:new_start]
|
516
|
+
break
|
517
|
+
new_start += 2
|
518
|
+
|
519
|
+
def clip_scaffold_ends(self):
|
520
|
+
"""Finds contiguous regions, and calls removeScaffoldEnds on that range."""
|
521
|
+
end = len(self.walk) - 1
|
522
|
+
index = end
|
523
|
+
while index >= 0:
|
524
|
+
try:
|
525
|
+
segment = self.walk[index - 1]
|
526
|
+
except IndexError:
|
527
|
+
self.remove_scaffold_ends_in_range(index, end)
|
528
|
+
return
|
529
|
+
if segment is None:
|
530
|
+
self.remove_scaffold_ends_in_range(index, end)
|
531
|
+
end = index - 2
|
532
|
+
index -= 2
|
533
|
+
|
534
|
+
def two_opt(self):
|
535
|
+
"""
|
536
|
+
Unused
|
537
|
+
"""
|
538
|
+
v = self.get_value()
|
539
|
+
while True:
|
540
|
+
new_value = self.two_opt_cycle(v)
|
541
|
+
if v == new_value:
|
542
|
+
break
|
543
|
+
|
544
|
+
def two_opt_cycle(self, value):
|
545
|
+
"""
|
546
|
+
Unused
|
547
|
+
"""
|
548
|
+
if len(self.walk) == 0:
|
549
|
+
return 0
|
550
|
+
swap_start = 0
|
551
|
+
walk_end = len(self.walk)
|
552
|
+
while swap_start < walk_end:
|
553
|
+
swap_element = self.walk[swap_start]
|
554
|
+
m = swap_element.visited
|
555
|
+
swap_end = swap_start + 2
|
556
|
+
while swap_end < walk_end:
|
557
|
+
current_element = self.walk[swap_end]
|
558
|
+
if swap_element == current_element:
|
559
|
+
m -= 1
|
560
|
+
self.flip_start = swap_start + 1
|
561
|
+
self.flip_end = swap_end - 1
|
562
|
+
new_value = self.get_value()
|
563
|
+
if new_value > value:
|
564
|
+
value = new_value
|
565
|
+
self.walk[swap_start + 1 : swap_end] = self.walk[
|
566
|
+
swap_start + 1 : swap_end : -1
|
567
|
+
] # reverse
|
568
|
+
else:
|
569
|
+
self.flip_start = None
|
570
|
+
self.flip_end = None
|
571
|
+
if m == 0:
|
572
|
+
break
|
573
|
+
swap_end += 2
|
574
|
+
swap_start += 2
|
575
|
+
return value
|
576
|
+
|
577
|
+
def get_segment(self, index):
|
578
|
+
"""
|
579
|
+
Unused
|
580
|
+
"""
|
581
|
+
if (
|
582
|
+
self.flip_start is not None
|
583
|
+
and self.flip_end is not None
|
584
|
+
and self.flip_start <= index <= self.flip_end
|
585
|
+
):
|
586
|
+
return self.walk[self.flip_end - (index - self.flip_start)]
|
587
|
+
return self.walk[index]
|
588
|
+
|
589
|
+
def get_node(self, index):
|
590
|
+
"""
|
591
|
+
Unused
|
592
|
+
"""
|
593
|
+
if (
|
594
|
+
self.flip_start is not None
|
595
|
+
and self.flip_end is not None
|
596
|
+
and self.flip_start <= index <= self.flip_end
|
597
|
+
):
|
598
|
+
return self.walk[self.flip_end - (index - self.flip_start)]
|
599
|
+
try:
|
600
|
+
return self.walk[index]
|
601
|
+
except IndexError:
|
602
|
+
return None
|
603
|
+
|
604
|
+
def get_value(self):
|
605
|
+
"""
|
606
|
+
Path values with flip.
|
607
|
+
@return: Flipped path value.
|
608
|
+
"""
|
609
|
+
if len(self.walk) == 0:
|
610
|
+
return 0
|
611
|
+
value = 0
|
612
|
+
start = 0
|
613
|
+
end = len(self.walk) - 1
|
614
|
+
while start < end:
|
615
|
+
i_segment = self.get_segment(start + 1)
|
616
|
+
if i_segment.value == "RUNG":
|
617
|
+
break
|
618
|
+
start += 2
|
619
|
+
while end >= 2:
|
620
|
+
i_segment = self.get_segment(end - 1)
|
621
|
+
if i_segment.value == "RUNG":
|
622
|
+
break
|
623
|
+
end -= 2
|
624
|
+
j = start
|
625
|
+
while j < end:
|
626
|
+
j_node = self.get_node(j)
|
627
|
+
j += 1
|
628
|
+
j_segment = self.get_segment(j)
|
629
|
+
j += 1
|
630
|
+
if j_segment.value != "RUNG":
|
631
|
+
# if the node connector is not critical, try to find and skip a loop
|
632
|
+
k = j
|
633
|
+
while k < end:
|
634
|
+
k_node = self.get_node(k)
|
635
|
+
k += 1
|
636
|
+
k_segment = self.get_segment(k)
|
637
|
+
k += 1
|
638
|
+
if k_segment.value == "RUNG":
|
639
|
+
break
|
640
|
+
if k_node == j_node:
|
641
|
+
# Only skippable nodes existed before returned to original node, so skip that loop.
|
642
|
+
value += (k - j) * 10
|
643
|
+
j = k
|
644
|
+
j_segment = k_segment
|
645
|
+
break
|
646
|
+
if j_segment.value == "SCAFFOLD":
|
647
|
+
value -= j_segment.a.distance_sq(j_segment.b)
|
648
|
+
elif j_segment.value == "RUNG":
|
649
|
+
value -= j_segment.a.distance_sq(j_segment.b)
|
650
|
+
return value
|
651
|
+
|
652
|
+
|
653
|
+
class VectorMontonizer:
|
654
|
+
"""
|
655
|
+
Sorts all segments according to their highest y values. Steps through the values in order
|
656
|
+
each step activates and deactivates the segments that are encountered such that it always has a list
|
657
|
+
of active segments. Sorting the active segments according to their x-intercepts gives a list of all
|
658
|
+
points that a ray would strike passing through that shape. Every other such area is filled. These are
|
659
|
+
given rungs, and connected to intercept points.
|
660
|
+
"""
|
661
|
+
|
662
|
+
def __init__(
|
663
|
+
self, low_value=-float("inf"), high_value=float("inf"), start=-float("inf")
|
664
|
+
):
|
665
|
+
self._event_index = 0
|
666
|
+
self._events = []
|
667
|
+
self._dirty_event_sort = True
|
668
|
+
|
669
|
+
self._actives = []
|
670
|
+
self._dirty_actives_sort = True
|
671
|
+
|
672
|
+
self._dirty_scanline = True
|
673
|
+
|
674
|
+
self.scanline = start
|
675
|
+
self.valid_low = low_value
|
676
|
+
self.valid_high = high_value
|
677
|
+
|
678
|
+
self.scanbeam_low = float("inf")
|
679
|
+
self.scanbeam_high = -float("inf")
|
680
|
+
|
681
|
+
def add_segment_events(self, links):
|
682
|
+
"""
|
683
|
+
Add segment to be processed. This segment should already exist and have the correct type
|
684
|
+
@param links:
|
685
|
+
@return:
|
686
|
+
"""
|
687
|
+
self._dirty_scanline = True
|
688
|
+
self._dirty_event_sort = True
|
689
|
+
self._dirty_actives_sort = True
|
690
|
+
for s in links:
|
691
|
+
self._events.append((s[4].y, s)) # High
|
692
|
+
self._events.append((s[5].y, s)) # Low
|
693
|
+
|
694
|
+
def add_polyline(self, path):
|
695
|
+
"""
|
696
|
+
Add segments in the form of a connected path. These positions are read and segments are created for these
|
697
|
+
points.
|
698
|
+
|
699
|
+
@param path:
|
700
|
+
@return:
|
701
|
+
"""
|
702
|
+
self._dirty_scanline = True
|
703
|
+
self._dirty_event_sort = True
|
704
|
+
self._dirty_actives_sort = True
|
705
|
+
for i in range(len(path) - 1):
|
706
|
+
p0 = path[i]
|
707
|
+
p1 = path[i + 1]
|
708
|
+
if p0.y > p1.y:
|
709
|
+
high = p0
|
710
|
+
low = p1
|
711
|
+
else:
|
712
|
+
high = p1
|
713
|
+
low = p0
|
714
|
+
|
715
|
+
# b = low.y - (m * low.x)
|
716
|
+
if self.valid_low > high.y:
|
717
|
+
# Cluster before range.
|
718
|
+
continue
|
719
|
+
if self.valid_high < low.y:
|
720
|
+
# Cluster after range.
|
721
|
+
continue
|
722
|
+
seg = Segment(p0, p1)
|
723
|
+
# cluster = [False, i, p0, p1, high, low, m, b, path]
|
724
|
+
if self.valid_low < low.y:
|
725
|
+
self._events.append((low.y, seg))
|
726
|
+
if self.valid_high > high.y:
|
727
|
+
self._events.append((high.y, seg))
|
728
|
+
if high.y >= self.scanline >= low.y:
|
729
|
+
seg.active = True
|
730
|
+
self._actives.append(seg)
|
731
|
+
|
732
|
+
def add_pointlist(self, path):
|
733
|
+
"""
|
734
|
+
Add segments in the form of a connected path. These positions are read and segments are created for these
|
735
|
+
points.
|
736
|
+
|
737
|
+
@param path:
|
738
|
+
@return:
|
739
|
+
"""
|
740
|
+
self._dirty_scanline = True
|
741
|
+
self._dirty_event_sort = True
|
742
|
+
self._dirty_actives_sort = True
|
743
|
+
for i in range(len(path) - 1):
|
744
|
+
p0 = Point(path[i])
|
745
|
+
p1 = Point(path[i + 1])
|
746
|
+
if p0.y > p1.y:
|
747
|
+
high = p0
|
748
|
+
low = p1
|
749
|
+
else:
|
750
|
+
high = p1
|
751
|
+
low = p0
|
752
|
+
|
753
|
+
# b = low.y - (m * low.x)
|
754
|
+
if self.valid_low > high.y:
|
755
|
+
# Cluster before range.
|
756
|
+
continue
|
757
|
+
if self.valid_high < low.y:
|
758
|
+
# Cluster after range.
|
759
|
+
continue
|
760
|
+
seg = Segment(p0, p1)
|
761
|
+
# cluster = [False, i, p0, p1, high, low, m, b, path]
|
762
|
+
if self.valid_low < low.y:
|
763
|
+
self._events.append((low.y, seg))
|
764
|
+
if self.valid_high > high.y:
|
765
|
+
self._events.append((high.y, seg))
|
766
|
+
if high.y >= self.scanline >= low.y:
|
767
|
+
seg.active = True
|
768
|
+
self._actives.append(seg)
|
769
|
+
|
770
|
+
def current_is_valid_range(self):
|
771
|
+
return self.valid_high >= self.scanline >= self.valid_low
|
772
|
+
|
773
|
+
def scanline_increment(self, delta):
|
774
|
+
self.scanline_to(self.scanline + delta)
|
775
|
+
self._sort_actives()
|
776
|
+
return self.current_is_valid_range()
|
777
|
+
|
778
|
+
def scanline_to(self, scan):
|
779
|
+
"""
|
780
|
+
Move the scanline to the scan position.
|
781
|
+
@param scan:
|
782
|
+
@return:
|
783
|
+
"""
|
784
|
+
self._dirty_actives_sort = True
|
785
|
+
self._sort_events()
|
786
|
+
self._find_scanbeam()
|
787
|
+
|
788
|
+
while self._below_scanbeam(scan):
|
789
|
+
c = self.scanbeam_higher()
|
790
|
+
if c.active:
|
791
|
+
c.active = False
|
792
|
+
self._actives.remove(c)
|
793
|
+
else:
|
794
|
+
c.active = True
|
795
|
+
self._actives.append(c)
|
796
|
+
|
797
|
+
while self._above_scanbeam(scan):
|
798
|
+
c = self.scanbeam_lower()
|
799
|
+
if c.active:
|
800
|
+
c.active = False
|
801
|
+
self._actives.remove(c)
|
802
|
+
else:
|
803
|
+
c.active = True
|
804
|
+
self._actives.append(c)
|
805
|
+
|
806
|
+
self.scanline = scan
|
807
|
+
|
808
|
+
def is_point_inside(self, x, y, tolerance=0):
|
809
|
+
"""
|
810
|
+
Determine if the x/y point is with the segments of a closed shape polygon.
|
811
|
+
|
812
|
+
This assumes that add_polyline added a closed point class.
|
813
|
+
@param x: x location of point
|
814
|
+
@param y: y location of point
|
815
|
+
@param tolerance: wiggle room
|
816
|
+
@return:
|
817
|
+
"""
|
818
|
+
self.scanline_to(y)
|
819
|
+
self._sort_actives()
|
820
|
+
for i in range(1, len(self._actives), 2):
|
821
|
+
prior = self._actives[i - 1]
|
822
|
+
after = self._actives[i]
|
823
|
+
if (
|
824
|
+
self.intercept(prior, y) - tolerance
|
825
|
+
<= x
|
826
|
+
<= self.intercept(after, y) + tolerance
|
827
|
+
):
|
828
|
+
return True
|
829
|
+
return False
|
830
|
+
|
831
|
+
def actives(self):
|
832
|
+
"""
|
833
|
+
Get the active list at the current scanline.
|
834
|
+
|
835
|
+
@return:
|
836
|
+
"""
|
837
|
+
self._sort_actives()
|
838
|
+
return self._actives
|
839
|
+
|
840
|
+
def event_range(self):
|
841
|
+
"""
|
842
|
+
Returns the range of events from the lowest to the highest in y-value.
|
843
|
+
|
844
|
+
@return:
|
845
|
+
"""
|
846
|
+
if len(self._events) == 0:
|
847
|
+
return None, None
|
848
|
+
self._sort_events()
|
849
|
+
y_min = self._events[0][0]
|
850
|
+
y_max = self._events[-1][0]
|
851
|
+
return y_min, y_max
|
852
|
+
|
853
|
+
def _sort_events(self):
|
854
|
+
if not self._dirty_event_sort:
|
855
|
+
return
|
856
|
+
self._events.sort(key=lambda e: e[0])
|
857
|
+
self._dirty_event_sort = False
|
858
|
+
|
859
|
+
def _sort_actives(self):
|
860
|
+
if not self._dirty_actives_sort:
|
861
|
+
return
|
862
|
+
self._actives.sort(key=self.intercept)
|
863
|
+
self._dirty_actives_sort = False
|
864
|
+
|
865
|
+
def intercept(self, e, y=None):
|
866
|
+
if y is None:
|
867
|
+
y = self.scanline
|
868
|
+
m = e[6]
|
869
|
+
b = e[7]
|
870
|
+
if isnan(m) or isinf(m):
|
871
|
+
low = e[5]
|
872
|
+
return low.x
|
873
|
+
return (y - b) / m
|
874
|
+
|
875
|
+
def _find_scanbeam(self):
|
876
|
+
if not self._dirty_scanline:
|
877
|
+
return
|
878
|
+
self._dirty_scanline = False
|
879
|
+
self._sort_events()
|
880
|
+
|
881
|
+
self._event_index = -1
|
882
|
+
self.scanbeam_high = -float("inf")
|
883
|
+
self.scanbeam_lower()
|
884
|
+
|
885
|
+
while self._above_scanbeam(self.scanline):
|
886
|
+
self.scanbeam_lower()
|
887
|
+
|
888
|
+
def within_scanbeam(self, v):
|
889
|
+
"""
|
890
|
+
Is the value within the current scanbeam?
|
891
|
+
@param v:
|
892
|
+
@return:
|
893
|
+
"""
|
894
|
+
|
895
|
+
return not self._below_scanbeam(v) and not self._above_scanbeam(v)
|
896
|
+
|
897
|
+
def _below_scanbeam(self, v):
|
898
|
+
"""
|
899
|
+
Is the value below the current scanbeam?
|
900
|
+
@param v:
|
901
|
+
@return:
|
902
|
+
"""
|
903
|
+
return v < self.scanbeam_low
|
904
|
+
|
905
|
+
def _above_scanbeam(self, v):
|
906
|
+
"""
|
907
|
+
Is the value above the current scanbeam?
|
908
|
+
|
909
|
+
@param v:
|
910
|
+
@return:
|
911
|
+
"""
|
912
|
+
return v > self.scanbeam_high
|
913
|
+
|
914
|
+
def scanbeam_lower(self):
|
915
|
+
"""
|
916
|
+
Move the scanbeam lower through the events.
|
917
|
+
|
918
|
+
@return:
|
919
|
+
"""
|
920
|
+
self._event_index += 1
|
921
|
+
self.scanbeam_low = self.scanbeam_high
|
922
|
+
if self._event_index < len(self._events):
|
923
|
+
self.scanbeam_high = self._events[self._event_index][0]
|
924
|
+
else:
|
925
|
+
self.scanbeam_high = float("inf")
|
926
|
+
if self._event_index > 0:
|
927
|
+
return self._events[self._event_index - 1][1]
|
928
|
+
else:
|
929
|
+
return None
|
930
|
+
|
931
|
+
def scanbeam_higher(self):
|
932
|
+
"""
|
933
|
+
Move the scanbeam higher in the events.
|
934
|
+
|
935
|
+
@return:
|
936
|
+
"""
|
937
|
+
self._event_index -= 1
|
938
|
+
self.scanbeam_high = self.scanbeam_low
|
939
|
+
if self._event_index > 0:
|
940
|
+
self.scanbeam_low = self._events[self._event_index - 1][0]
|
941
|
+
else:
|
942
|
+
self.scanbeam_low = -float("inf")
|
943
|
+
return self._events[self._event_index][1]
|
944
|
+
|
945
|
+
|
946
|
+
class EulerianFill:
|
947
|
+
"""Eulerian fill given some outline shapes, creates a fill."""
|
948
|
+
|
949
|
+
def __init__(self, distance):
|
950
|
+
self.distance = distance
|
951
|
+
self.outlines = []
|
952
|
+
|
953
|
+
def __iadd__(self, other):
|
954
|
+
self.outlines.append(other)
|
955
|
+
return self
|
956
|
+
|
957
|
+
def estimate(self):
|
958
|
+
min_y = float("inf")
|
959
|
+
max_y = -float("inf")
|
960
|
+
for outline in self.outlines:
|
961
|
+
o_min_y = min([p[1] for p in outline])
|
962
|
+
o_max_y = max([p[1] for p in outline])
|
963
|
+
min_y = min(min_y, o_min_y)
|
964
|
+
max_y = max(max_y, o_max_y)
|
965
|
+
try:
|
966
|
+
return (max_y - min_y) / self.distance
|
967
|
+
except ZeroDivisionError:
|
968
|
+
return float("inf")
|
969
|
+
|
970
|
+
def get_fill(self):
|
971
|
+
min_y = float("inf")
|
972
|
+
max_y = -float("inf")
|
973
|
+
outline_graphs = list()
|
974
|
+
for outline in self.outlines:
|
975
|
+
outline_graph = Graph()
|
976
|
+
outline_graph.add_shape(outline, True)
|
977
|
+
o_min_y = min([p[1] for p in outline])
|
978
|
+
o_max_y = max([p[1] for p in outline])
|
979
|
+
min_y = min(min_y, o_min_y)
|
980
|
+
max_y = max(max_y, o_max_y)
|
981
|
+
outline_graphs.append(outline_graph)
|
982
|
+
graph = Graph()
|
983
|
+
Graph.monotone_fill(graph, outline_graphs, min_y, max_y, self.distance)
|
984
|
+
graph.double_odd_edge()
|
985
|
+
walk = list()
|
986
|
+
graph.walk(walk)
|
987
|
+
return walk
|