meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7010__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 +1195 -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 +1844 -1507
- meerk40t/core/elements/clipboard.py +229 -219
- meerk40t/core/elements/element_treeops.py +4561 -2837
- meerk40t/core/elements/element_types.py +125 -105
- meerk40t/core/elements/elements.py +4329 -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 +933 -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/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 +462 -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 +198 -0
- meerk40t/extra/ezd.py +1165 -1165
- meerk40t/extra/hershey.py +835 -340
- meerk40t/extra/imageactions.py +322 -316
- meerk40t/extra/inkscape.py +630 -622
- meerk40t/extra/lbrn.py +424 -424
- meerk40t/extra/outerworld.py +284 -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 +1081 -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 +170 -133
- meerk40t/gui/choicepropertypanel.py +1673 -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 +1430 -846
- meerk40t/gui/icons.py +3422 -2747
- meerk40t/gui/imagesplitter.py +555 -508
- meerk40t/gui/keymap.py +354 -344
- meerk40t/gui/laserpanel.py +892 -806
- meerk40t/gui/laserrender.py +1470 -1232
- meerk40t/gui/lasertoolpanel.py +805 -793
- meerk40t/gui/magnetoptions.py +436 -0
- meerk40t/gui/materialmanager.py +2917 -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 +494 -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 +2468 -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 +589 -346
- meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
- meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
- meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
- meerk40t/gui/simpleui.py +357 -333
- meerk40t/gui/simulation.py +2431 -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 +591 -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 +160 -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 +1444 -1169
- meerk40t/gui/wxmmain.py +5578 -4112
- meerk40t/gui/wxmribbon.py +1591 -1076
- meerk40t/gui/wxmscene.py +1635 -1453
- meerk40t/gui/wxmtree.py +2410 -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 +2778 -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 +3809 -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 +102 -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 +390 -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 +672 -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 +940 -940
- meerk40t/tools/rasterplotter.py +1660 -547
- meerk40t/tools/shxparser.py +989 -901
- meerk40t/tools/ttfparser.py +726 -446
- meerk40t/tools/zinglplotter.py +595 -593
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
- meerk40t-0.9.7010.dist-info/RECORD +445 -0
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.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.7010.dist-info}/entry_points.txt +0 -0
meerk40t/tools/polybool.py
CHANGED
@@ -1,940 +1,940 @@
|
|
1
|
-
"""
|
2
|
-
https://github.com/KaivnD/pypolybool
|
3
|
-
MIT License
|
4
|
-
"""
|
5
|
-
|
6
|
-
import typing
|
7
|
-
|
8
|
-
tolerance = 1e-10
|
9
|
-
|
10
|
-
T = typing.TypeVar("T")
|
11
|
-
TPoint = typing.TypeVar("TPoint", bound="Point")
|
12
|
-
|
13
|
-
|
14
|
-
class PolyBoolException(Exception):
|
15
|
-
pass
|
16
|
-
|
17
|
-
|
18
|
-
class Point:
|
19
|
-
def __init__(self: TPoint, x: float, y: float) -> None:
|
20
|
-
self.x = x
|
21
|
-
self.y = y
|
22
|
-
|
23
|
-
@staticmethod
|
24
|
-
def collinear(pt1: TPoint, pt2: TPoint, pt3: TPoint) -> bool:
|
25
|
-
dx1 = pt1.x - pt2.x
|
26
|
-
dy1 = pt1.y - pt2.y
|
27
|
-
dx2 = pt2.x - pt3.x
|
28
|
-
dy2 = pt2.y - pt3.y
|
29
|
-
return abs(dx1 * dy2 - dx2 * dy1) < tolerance
|
30
|
-
|
31
|
-
@staticmethod
|
32
|
-
def compare(pt1: TPoint, pt2: TPoint):
|
33
|
-
if abs(pt1.x - pt2.x) < tolerance:
|
34
|
-
return 0 if abs(pt1.y - pt2.y) < tolerance else -1 if pt1.y < pt2.y else 1
|
35
|
-
return -1 if pt1.x < pt2.x else 1
|
36
|
-
|
37
|
-
@staticmethod
|
38
|
-
def pointAboveOrOnLine(point: TPoint, left: TPoint, right: TPoint):
|
39
|
-
return (right.x - left.x) * (point.y - left.y) - (right.y - left.y) * (
|
40
|
-
point.x - left.x
|
41
|
-
) >= -tolerance
|
42
|
-
|
43
|
-
@staticmethod
|
44
|
-
def between(point: TPoint, left: TPoint, right: TPoint):
|
45
|
-
dPyLy = point.y - left.y
|
46
|
-
dRxLx = right.x - left.x
|
47
|
-
dPxLx = point.x - left.x
|
48
|
-
dRyLy = right.y - left.y
|
49
|
-
|
50
|
-
dot = dPxLx * dRxLx + dPyLy * dRyLy
|
51
|
-
if dot < tolerance:
|
52
|
-
return False
|
53
|
-
|
54
|
-
sqlen = dRxLx * dRxLx + dRyLy * dRyLy
|
55
|
-
if dot - sqlen > -tolerance:
|
56
|
-
return False
|
57
|
-
|
58
|
-
return True
|
59
|
-
|
60
|
-
@staticmethod
|
61
|
-
def linesIntersect(a0: TPoint, a1: TPoint, b0: TPoint, b1: TPoint):
|
62
|
-
adx = a1.x - a0.x
|
63
|
-
ady = a1.y - a0.y
|
64
|
-
bdx = b1.x - b0.x
|
65
|
-
bdy = b1.y - b0.y
|
66
|
-
|
67
|
-
axb = adx * bdy - ady * bdx
|
68
|
-
|
69
|
-
if abs(axb) < tolerance:
|
70
|
-
return None
|
71
|
-
|
72
|
-
dx = a0.x - b0.x
|
73
|
-
dy = a0.y - b0.y
|
74
|
-
|
75
|
-
a = (bdx * dy - bdy * dx) / axb
|
76
|
-
b = (adx * dy - ady * dx) / axb
|
77
|
-
|
78
|
-
return IntersectionPoint(
|
79
|
-
Point.__calcAlongUsingValue(a),
|
80
|
-
Point.__calcAlongUsingValue(b),
|
81
|
-
Point(a0.x + a * adx, a0.y + a * ady),
|
82
|
-
)
|
83
|
-
|
84
|
-
@staticmethod
|
85
|
-
def __calcAlongUsingValue(value: float):
|
86
|
-
if value <= -tolerance:
|
87
|
-
return -2
|
88
|
-
elif value < tolerance:
|
89
|
-
return -1
|
90
|
-
elif value - 1 <= -tolerance:
|
91
|
-
return 0
|
92
|
-
elif value - 1 < tolerance:
|
93
|
-
return 1
|
94
|
-
else:
|
95
|
-
return 2
|
96
|
-
|
97
|
-
def __eq__(self, __o: object) -> bool:
|
98
|
-
if not isinstance(__o, Point):
|
99
|
-
return False
|
100
|
-
return abs(self.x - __o.x) < tolerance and abs(self.y - __o.y) < tolerance
|
101
|
-
|
102
|
-
def __repr__(self) -> str:
|
103
|
-
return f"{self.x},{self.y}"
|
104
|
-
|
105
|
-
def __str__(self) -> str:
|
106
|
-
return f"{self.x},{self.y}"
|
107
|
-
|
108
|
-
|
109
|
-
class Fill:
|
110
|
-
def __init__(self, below: bool = None, above: bool = None) -> None:
|
111
|
-
self.below = below
|
112
|
-
self.above = above
|
113
|
-
|
114
|
-
def __repr__(self) -> str:
|
115
|
-
return f"{self.above},{self.below}"
|
116
|
-
|
117
|
-
def __str__(self) -> str:
|
118
|
-
return f"{self.above},{self.below}"
|
119
|
-
|
120
|
-
|
121
|
-
class Segment:
|
122
|
-
def __init__(
|
123
|
-
self, start: Point, end: Point, myfill: Fill = None, otherfill: Fill = None
|
124
|
-
):
|
125
|
-
self.start = start
|
126
|
-
self.end = end
|
127
|
-
self.myfill = myfill
|
128
|
-
self.otherfill = otherfill
|
129
|
-
|
130
|
-
def __repr__(self) -> str:
|
131
|
-
return f"S: {self.start}, E: {self.end}"
|
132
|
-
|
133
|
-
def __str__(self) -> str:
|
134
|
-
return f"S: {self.start}, E: {self.end}"
|
135
|
-
|
136
|
-
|
137
|
-
class PolySegments:
|
138
|
-
def __init__(
|
139
|
-
self, segments: typing.List[Segment] = None, isInverted: bool = False
|
140
|
-
) -> None:
|
141
|
-
self.segments = segments
|
142
|
-
self.isInverted = isInverted
|
143
|
-
|
144
|
-
|
145
|
-
class CombinedPolySegments:
|
146
|
-
def __init__(
|
147
|
-
self,
|
148
|
-
combined: typing.List[Segment] = None,
|
149
|
-
isInverted1: bool = False,
|
150
|
-
isInverted2: bool = False,
|
151
|
-
) -> None:
|
152
|
-
self.combined = combined
|
153
|
-
self.isInverted1 = isInverted1
|
154
|
-
self.isInverted2 = isInverted2
|
155
|
-
|
156
|
-
|
157
|
-
class Matcher:
|
158
|
-
def __init__(
|
159
|
-
self,
|
160
|
-
index: int,
|
161
|
-
matchesHead: bool,
|
162
|
-
matchesPt1: bool,
|
163
|
-
) -> None:
|
164
|
-
self.index = index
|
165
|
-
self.matchesHead = matchesHead
|
166
|
-
self.matchesPt1 = matchesPt1
|
167
|
-
|
168
|
-
|
169
|
-
class IntersectionPoint:
|
170
|
-
def __init__(self, alongA: int, alongB: int, pt: Point) -> None:
|
171
|
-
self.alongA = alongA
|
172
|
-
self.alongB = alongB
|
173
|
-
self.pt = pt
|
174
|
-
|
175
|
-
|
176
|
-
TNode = typing.TypeVar("TNode", bound="Node")
|
177
|
-
|
178
|
-
|
179
|
-
class Node:
|
180
|
-
def __init__(
|
181
|
-
self: TNode,
|
182
|
-
isRoot: bool = False,
|
183
|
-
isStart: bool = False,
|
184
|
-
pt: Point = None,
|
185
|
-
seg: Segment = None,
|
186
|
-
primary: bool = False,
|
187
|
-
next: TNode = None,
|
188
|
-
previous: TNode = None,
|
189
|
-
other: TNode = None,
|
190
|
-
ev: TNode = None,
|
191
|
-
status: TNode = None,
|
192
|
-
remove: typing.Callable = None,
|
193
|
-
):
|
194
|
-
self.status = status
|
195
|
-
self.other = other
|
196
|
-
self.ev = ev
|
197
|
-
self.previous = previous
|
198
|
-
self.next = next
|
199
|
-
self.isRoot = isRoot
|
200
|
-
self.remove = remove
|
201
|
-
self.isStart = isStart
|
202
|
-
self.pt = pt
|
203
|
-
self.seg = seg
|
204
|
-
self.primary = primary
|
205
|
-
|
206
|
-
|
207
|
-
class Transition:
|
208
|
-
def __init__(
|
209
|
-
self, after: Node, before: Node, insert: typing.Callable[[Node], Node]
|
210
|
-
) -> None:
|
211
|
-
self.after = after
|
212
|
-
self.before = before
|
213
|
-
self.insert = insert
|
214
|
-
|
215
|
-
|
216
|
-
class LinkedList:
|
217
|
-
def __init__(self) -> None:
|
218
|
-
self.__root = Node(isRoot=True)
|
219
|
-
|
220
|
-
def exists(self, node: Node):
|
221
|
-
if node is None or node is self.__root:
|
222
|
-
return False
|
223
|
-
return True
|
224
|
-
|
225
|
-
def isEmpty(self):
|
226
|
-
return self.__root.next is None
|
227
|
-
|
228
|
-
def getHead(self):
|
229
|
-
return self.__root.next
|
230
|
-
|
231
|
-
def insertBefore(self, node: Node, check: typing.Callable[[Node], bool]):
|
232
|
-
last = self.__root
|
233
|
-
here = self.__root.next
|
234
|
-
|
235
|
-
while here is not None:
|
236
|
-
if check(here):
|
237
|
-
node.previous = here.previous
|
238
|
-
node.next = here
|
239
|
-
here.previous.next = node
|
240
|
-
here.previous = node
|
241
|
-
return
|
242
|
-
last = here
|
243
|
-
here = here.next
|
244
|
-
last.next = node
|
245
|
-
node.previous = last
|
246
|
-
node.next = None
|
247
|
-
|
248
|
-
def findTransition(self, check: typing.Callable[[Node], bool]):
|
249
|
-
previous = self.__root
|
250
|
-
here = self.__root.next
|
251
|
-
|
252
|
-
while here is not None:
|
253
|
-
if check(here):
|
254
|
-
break
|
255
|
-
previous = here
|
256
|
-
here = here.next
|
257
|
-
|
258
|
-
def insert_func(node: Node):
|
259
|
-
node.previous = previous
|
260
|
-
node.next = here
|
261
|
-
previous.next = node
|
262
|
-
if here is not None:
|
263
|
-
here.previous = node
|
264
|
-
return node
|
265
|
-
|
266
|
-
return Transition(
|
267
|
-
before=(None if previous is self.__root else previous),
|
268
|
-
after=here,
|
269
|
-
insert=insert_func,
|
270
|
-
)
|
271
|
-
|
272
|
-
@staticmethod
|
273
|
-
def node(data: Node):
|
274
|
-
data.previous = None
|
275
|
-
data.next = None
|
276
|
-
|
277
|
-
def remove_func():
|
278
|
-
data.previous.next = data.next
|
279
|
-
if data.next is not None:
|
280
|
-
data.next.previous = data.previous
|
281
|
-
data.previous = None
|
282
|
-
data.next = None
|
283
|
-
|
284
|
-
data.remove = remove_func
|
285
|
-
return data
|
286
|
-
|
287
|
-
|
288
|
-
RegionInput = typing.Union[typing.List[Point], typing.List[typing.Tuple[float, float]]]
|
289
|
-
Region = typing.List[Point]
|
290
|
-
|
291
|
-
|
292
|
-
class Polygon:
|
293
|
-
def __init__(self, regions: typing.List[RegionInput], isInverted=False) -> None:
|
294
|
-
_regions: typing.List[Region] = []
|
295
|
-
for region in regions:
|
296
|
-
tmp: Region = []
|
297
|
-
for pt in region:
|
298
|
-
if isinstance(pt, Point):
|
299
|
-
tmp.append(pt)
|
300
|
-
elif isinstance(pt, tuple):
|
301
|
-
x, y = pt
|
302
|
-
tmp.append(Point(x, y))
|
303
|
-
_regions.append(tmp)
|
304
|
-
|
305
|
-
self.regions = _regions
|
306
|
-
self.isInverted = isInverted
|
307
|
-
|
308
|
-
|
309
|
-
class Intersecter:
|
310
|
-
def __init__(self, selfIntersection: bool) -> None:
|
311
|
-
self.selfIntersection = selfIntersection
|
312
|
-
self.__eventRoot = LinkedList()
|
313
|
-
|
314
|
-
def newsegment(self, start: Point, end: Point):
|
315
|
-
return Segment(start=start, end=end, myfill=Fill())
|
316
|
-
|
317
|
-
def segmentCopy(self, start: Point, end: Point, seg: Segment):
|
318
|
-
return Segment(
|
319
|
-
start=start, end=end, myfill=Fill(seg.myfill.below, seg.myfill.above)
|
320
|
-
)
|
321
|
-
|
322
|
-
def __eventCompare(
|
323
|
-
self,
|
324
|
-
p1IsStart: bool,
|
325
|
-
p11: Point,
|
326
|
-
p12: Point,
|
327
|
-
p2IsStart: bool,
|
328
|
-
p21: Point,
|
329
|
-
p22: Point,
|
330
|
-
):
|
331
|
-
comp = Point.compare(p11, p21)
|
332
|
-
if comp != 0:
|
333
|
-
return comp
|
334
|
-
|
335
|
-
if p12 == p22:
|
336
|
-
return 0
|
337
|
-
|
338
|
-
if p1IsStart != p2IsStart:
|
339
|
-
return 1 if p1IsStart else -1
|
340
|
-
|
341
|
-
return (
|
342
|
-
1
|
343
|
-
if Point.pointAboveOrOnLine(
|
344
|
-
p12, p21 if p2IsStart else p22, p22 if p2IsStart else p21
|
345
|
-
)
|
346
|
-
else -1
|
347
|
-
)
|
348
|
-
|
349
|
-
def __eventAdd(self, ev: Node, otherPt: Point):
|
350
|
-
def check_func(here: Node):
|
351
|
-
comp = self.__eventCompare(
|
352
|
-
ev.isStart, ev.pt, otherPt, here.isStart, here.pt, here.other.pt
|
353
|
-
)
|
354
|
-
return comp < 0
|
355
|
-
|
356
|
-
self.__eventRoot.insertBefore(ev, check_func)
|
357
|
-
|
358
|
-
def __eventAddSegmentStart(self, segment: Segment, primary: bool):
|
359
|
-
evStart = LinkedList.node(
|
360
|
-
Node(
|
361
|
-
isStart=True,
|
362
|
-
pt=segment.start,
|
363
|
-
seg=segment,
|
364
|
-
primary=primary,
|
365
|
-
)
|
366
|
-
)
|
367
|
-
self.__eventAdd(evStart, segment.end)
|
368
|
-
return evStart
|
369
|
-
|
370
|
-
def __eventAddSegmentEnd(self, evStart: Node, segment: Segment, primary: bool):
|
371
|
-
evEnd = LinkedList.node(
|
372
|
-
Node(
|
373
|
-
isStart=False,
|
374
|
-
pt=segment.end,
|
375
|
-
seg=segment,
|
376
|
-
primary=primary,
|
377
|
-
other=evStart,
|
378
|
-
)
|
379
|
-
)
|
380
|
-
evStart.other = evEnd
|
381
|
-
self.__eventAdd(evEnd, evStart.pt)
|
382
|
-
|
383
|
-
def eventAddSegment(self, segment: Segment, primary: bool):
|
384
|
-
evStart = self.__eventAddSegmentStart(segment, primary)
|
385
|
-
self.__eventAddSegmentEnd(evStart, segment, primary)
|
386
|
-
return evStart
|
387
|
-
|
388
|
-
def __eventUpdateEnd(self, ev: Node, end: Point):
|
389
|
-
ev.other.remove()
|
390
|
-
ev.seg.end = end
|
391
|
-
ev.other.pt = end
|
392
|
-
self.__eventAdd(ev.other, ev.pt)
|
393
|
-
|
394
|
-
def __eventDivide(self, ev: Node, pt: Point):
|
395
|
-
ns = self.segmentCopy(pt, ev.seg.end, ev.seg)
|
396
|
-
self.__eventUpdateEnd(ev, pt)
|
397
|
-
return self.eventAddSegment(ns, ev.primary)
|
398
|
-
|
399
|
-
def __statusCompare(self, ev1: Node, ev2: Node):
|
400
|
-
a1 = ev1.seg.start
|
401
|
-
a2 = ev1.seg.end
|
402
|
-
b1 = ev2.seg.start
|
403
|
-
b2 = ev2.seg.end
|
404
|
-
|
405
|
-
if Point.collinear(a1, b1, b2):
|
406
|
-
if Point.collinear(a2, b1, b2):
|
407
|
-
return 1
|
408
|
-
return 1 if Point.pointAboveOrOnLine(a2, b1, b2) else -1
|
409
|
-
return 1 if Point.pointAboveOrOnLine(a1, b1, b2) else -1
|
410
|
-
|
411
|
-
def __statusFindSurrounding(self, statusRoot: LinkedList, ev: Node):
|
412
|
-
def check_func(here: Node):
|
413
|
-
return self.__statusCompare(ev, here.ev) > 0
|
414
|
-
|
415
|
-
return statusRoot.findTransition(check_func)
|
416
|
-
|
417
|
-
def __checkIntersection(self, ev1: Node, ev2: Node):
|
418
|
-
seg1 = ev1.seg
|
419
|
-
seg2 = ev2.seg
|
420
|
-
a1 = seg1.start
|
421
|
-
a2 = seg1.end
|
422
|
-
b1 = seg2.start
|
423
|
-
b2 = seg2.end
|
424
|
-
|
425
|
-
i = Point.linesIntersect(a1, a2, b1, b2)
|
426
|
-
if i is None:
|
427
|
-
if not Point.collinear(a1, a2, b1):
|
428
|
-
return None
|
429
|
-
if a1 == b2 or a2 == b1:
|
430
|
-
return None
|
431
|
-
a1EquB1 = a1 == b1
|
432
|
-
a2EquB2 = a2 == b2
|
433
|
-
if a1EquB1 and a2EquB2:
|
434
|
-
return ev2
|
435
|
-
|
436
|
-
a1Between = not a1EquB1 and Point.between(a1, b1, b2)
|
437
|
-
a2Between = not a2EquB2 and Point.between(a2, b1, b2)
|
438
|
-
|
439
|
-
if a1EquB1:
|
440
|
-
if a2Between:
|
441
|
-
self.__eventDivide(ev2, a2)
|
442
|
-
else:
|
443
|
-
self.__eventDivide(ev1, b2)
|
444
|
-
|
445
|
-
return ev2
|
446
|
-
elif a1Between:
|
447
|
-
if not a2EquB2:
|
448
|
-
if a2Between:
|
449
|
-
self.__eventDivide(ev2, a2)
|
450
|
-
else:
|
451
|
-
self.__eventDivide(ev1, b2)
|
452
|
-
self.__eventDivide(ev2, a1)
|
453
|
-
else:
|
454
|
-
if i.alongA == 0:
|
455
|
-
if i.alongB == -1:
|
456
|
-
self.__eventDivide(ev1, b1)
|
457
|
-
elif i.alongB == 0:
|
458
|
-
self.__eventDivide(ev1, i.pt)
|
459
|
-
elif i.alongB == 1:
|
460
|
-
self.__eventDivide(ev1, b2)
|
461
|
-
if i.alongB == 0:
|
462
|
-
if i.alongA == -1:
|
463
|
-
self.__eventDivide(ev2, a1)
|
464
|
-
elif i.alongA == 0:
|
465
|
-
self.__eventDivide(ev2, i.pt)
|
466
|
-
elif i.alongA == 1:
|
467
|
-
self.__eventDivide(ev2, a2)
|
468
|
-
return None
|
469
|
-
|
470
|
-
def __checkBothIntersections(self, above: Node, ev: Node, below: Node):
|
471
|
-
if above is not None:
|
472
|
-
eve = self.__checkIntersection(ev, above)
|
473
|
-
if eve is not None:
|
474
|
-
return eve
|
475
|
-
if below is not None:
|
476
|
-
return self.__checkIntersection(ev, below)
|
477
|
-
|
478
|
-
return None
|
479
|
-
|
480
|
-
def calculate(self, primaryPolyInverted: bool, secondaryPolyInverted: bool):
|
481
|
-
statusRoot = LinkedList()
|
482
|
-
segments: typing.List[Segment] = []
|
483
|
-
|
484
|
-
cnt = 0
|
485
|
-
|
486
|
-
while not self.__eventRoot.isEmpty():
|
487
|
-
cnt += 1
|
488
|
-
ev = self.__eventRoot.getHead()
|
489
|
-
if ev.isStart:
|
490
|
-
surrounding = self.__statusFindSurrounding(statusRoot, ev)
|
491
|
-
above = (
|
492
|
-
surrounding.before.ev if surrounding.before is not None else None
|
493
|
-
)
|
494
|
-
below = surrounding.after.ev if surrounding.after is not None else None
|
495
|
-
|
496
|
-
eve = self.__checkBothIntersections(above, ev, below)
|
497
|
-
if eve is not None:
|
498
|
-
if self.selfIntersection:
|
499
|
-
toggle = False
|
500
|
-
if ev.seg.myfill.below is None:
|
501
|
-
toggle = True
|
502
|
-
else:
|
503
|
-
toggle = ev.seg.myfill.above != ev.seg.myfill.below
|
504
|
-
|
505
|
-
if toggle:
|
506
|
-
eve.seg.myfill.above = not eve.seg.myfill.above
|
507
|
-
else:
|
508
|
-
eve.seg.otherfill = ev.seg.myfill
|
509
|
-
ev.other.remove()
|
510
|
-
ev.remove()
|
511
|
-
|
512
|
-
if self.__eventRoot.getHead() is not ev:
|
513
|
-
continue
|
514
|
-
|
515
|
-
if self.selfIntersection:
|
516
|
-
toggle = False
|
517
|
-
if ev.seg.myfill.below is None:
|
518
|
-
toggle = True
|
519
|
-
else:
|
520
|
-
toggle = ev.seg.myfill.above != ev.seg.myfill.below
|
521
|
-
|
522
|
-
if below is None:
|
523
|
-
ev.seg.myfill.below = primaryPolyInverted
|
524
|
-
else:
|
525
|
-
ev.seg.myfill.below = below.seg.myfill.above
|
526
|
-
|
527
|
-
if toggle:
|
528
|
-
ev.seg.myfill.above = not ev.seg.myfill.below
|
529
|
-
else:
|
530
|
-
ev.seg.myfill.above = ev.seg.myfill.below
|
531
|
-
else:
|
532
|
-
if ev.seg.otherfill is None:
|
533
|
-
inside = False
|
534
|
-
if below is None:
|
535
|
-
inside = (
|
536
|
-
secondaryPolyInverted
|
537
|
-
if ev.primary
|
538
|
-
else primaryPolyInverted
|
539
|
-
)
|
540
|
-
else:
|
541
|
-
if ev.primary == below.primary:
|
542
|
-
inside = below.seg.otherfill.above
|
543
|
-
else:
|
544
|
-
inside = below.seg.myfill.above
|
545
|
-
ev.seg.otherfill = Fill(inside, inside)
|
546
|
-
ev.other.status = surrounding.insert(LinkedList.node(Node(ev=ev)))
|
547
|
-
else:
|
548
|
-
st = ev.status
|
549
|
-
if st is None:
|
550
|
-
raise PolyBoolException(
|
551
|
-
"PolyBool: Zero-length segment detected; your epsilon is probably too small or too large"
|
552
|
-
)
|
553
|
-
if statusRoot.exists(st.previous) and statusRoot.exists(st.next):
|
554
|
-
self.__checkIntersection(st.previous.ev, st.next.ev)
|
555
|
-
st.remove()
|
556
|
-
|
557
|
-
if not ev.primary:
|
558
|
-
s = ev.seg.myfill
|
559
|
-
ev.seg.myfill = ev.seg.otherfill
|
560
|
-
ev.seg.otherfill = s
|
561
|
-
segments.append(ev.seg)
|
562
|
-
self.__eventRoot.getHead().remove()
|
563
|
-
return segments
|
564
|
-
|
565
|
-
|
566
|
-
class RegionIntersecter(Intersecter):
|
567
|
-
def __init__(self) -> None:
|
568
|
-
super().__init__(True)
|
569
|
-
|
570
|
-
def addRegion(self, region: Region):
|
571
|
-
pt1: Point
|
572
|
-
pt2 = region[-1]
|
573
|
-
for i in range(len(region)):
|
574
|
-
pt1 = pt2
|
575
|
-
pt2 = region[i]
|
576
|
-
forward = Point.compare(pt1, pt2)
|
577
|
-
|
578
|
-
if forward == 0:
|
579
|
-
continue
|
580
|
-
|
581
|
-
seg = self.newsegment(
|
582
|
-
pt1 if forward < 0 else pt2, pt2 if forward < 0 else pt1
|
583
|
-
)
|
584
|
-
|
585
|
-
self.eventAddSegment(seg, True)
|
586
|
-
|
587
|
-
def calculate(self, inverted: bool):
|
588
|
-
return super().calculate(inverted, False)
|
589
|
-
|
590
|
-
|
591
|
-
class SegmentIntersecter(Intersecter):
|
592
|
-
def __init__(self) -> None:
|
593
|
-
super().__init__(False)
|
594
|
-
|
595
|
-
def calculate(
|
596
|
-
self,
|
597
|
-
segments1: typing.List[Segment],
|
598
|
-
isInverted1: bool,
|
599
|
-
segments2: typing.List[Segment],
|
600
|
-
isInverted2: bool,
|
601
|
-
):
|
602
|
-
for seg in segments1:
|
603
|
-
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), True)
|
604
|
-
|
605
|
-
for seg in segments2:
|
606
|
-
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), False)
|
607
|
-
|
608
|
-
return super().calculate(isInverted1, isInverted2)
|
609
|
-
|
610
|
-
|
611
|
-
class SegmentChainerMatcher:
|
612
|
-
def __init__(self) -> None:
|
613
|
-
self.firstMatch = Matcher(0, False, False)
|
614
|
-
self.secondMatch = Matcher(0, False, False)
|
615
|
-
|
616
|
-
self.nextMatch = self.firstMatch
|
617
|
-
|
618
|
-
def setMatch(self, index: int, matchesHead: bool, matchesPt1: bool):
|
619
|
-
self.nextMatch.index = index
|
620
|
-
self.nextMatch.matchesHead = matchesHead
|
621
|
-
self.nextMatch.matchesPt1 = matchesPt1
|
622
|
-
if self.nextMatch is self.firstMatch:
|
623
|
-
self.nextMatch = self.secondMatch
|
624
|
-
return False
|
625
|
-
self.nextMatch = None
|
626
|
-
return True
|
627
|
-
|
628
|
-
|
629
|
-
def list_shift(list: typing.List):
|
630
|
-
list.pop(0)
|
631
|
-
|
632
|
-
|
633
|
-
def list_pop(list: typing.List):
|
634
|
-
list.pop()
|
635
|
-
|
636
|
-
|
637
|
-
def list_splice(list: typing.List, index: int, count: int):
|
638
|
-
del list[index : index + count]
|
639
|
-
|
640
|
-
|
641
|
-
def list_unshift(list: typing.List[T], element: T):
|
642
|
-
list.insert(0, element)
|
643
|
-
|
644
|
-
|
645
|
-
def segmentChainer(segments: typing.List[Segment]) -> typing.List[Region]:
|
646
|
-
regions: typing.List[Region] = []
|
647
|
-
chains: typing.List[typing.List[Point]] = []
|
648
|
-
|
649
|
-
for seg in segments:
|
650
|
-
pt1 = seg.start
|
651
|
-
pt2 = seg.end
|
652
|
-
if pt1 == pt2:
|
653
|
-
continue
|
654
|
-
|
655
|
-
scm = SegmentChainerMatcher()
|
656
|
-
|
657
|
-
for i in range(len(chains)):
|
658
|
-
chain = chains[i]
|
659
|
-
head = chain[0]
|
660
|
-
tail = chain[-1]
|
661
|
-
|
662
|
-
if head == pt1:
|
663
|
-
if scm.setMatch(i, True, True):
|
664
|
-
break
|
665
|
-
elif head == pt2:
|
666
|
-
if scm.setMatch(i, True, False):
|
667
|
-
break
|
668
|
-
elif tail == pt1:
|
669
|
-
if scm.setMatch(i, False, True):
|
670
|
-
break
|
671
|
-
elif tail == pt2:
|
672
|
-
if scm.setMatch(i, False, False):
|
673
|
-
break
|
674
|
-
|
675
|
-
if scm.nextMatch is scm.firstMatch:
|
676
|
-
chains.append([pt1, pt2])
|
677
|
-
continue
|
678
|
-
|
679
|
-
if scm.nextMatch is scm.secondMatch:
|
680
|
-
index = scm.firstMatch.index
|
681
|
-
pt = pt2 if scm.firstMatch.matchesPt1 else pt1
|
682
|
-
addToHead = scm.firstMatch.matchesHead
|
683
|
-
|
684
|
-
chain = chains[index]
|
685
|
-
grow = chain[0] if addToHead else chain[-1]
|
686
|
-
grow2 = chain[1] if addToHead else chain[-2]
|
687
|
-
oppo = chain[-1] if addToHead else chain[0]
|
688
|
-
oppo2 = chain[-2] if addToHead else chain[1]
|
689
|
-
|
690
|
-
if Point.collinear(grow2, grow, pt):
|
691
|
-
if addToHead:
|
692
|
-
list_shift(chain)
|
693
|
-
else:
|
694
|
-
list_pop(chain)
|
695
|
-
grow = grow2
|
696
|
-
if oppo == pt:
|
697
|
-
list_splice(chains, index, 1)
|
698
|
-
if Point.collinear(oppo2, oppo, grow):
|
699
|
-
if addToHead:
|
700
|
-
list_pop(chain)
|
701
|
-
else:
|
702
|
-
list_shift(chain)
|
703
|
-
regions.append(chain)
|
704
|
-
continue
|
705
|
-
if addToHead:
|
706
|
-
list_unshift(chain, pt)
|
707
|
-
else:
|
708
|
-
chain.append(pt)
|
709
|
-
continue
|
710
|
-
|
711
|
-
def reverseChain(index: int):
|
712
|
-
chains[index].reverse()
|
713
|
-
|
714
|
-
def appendChain(index1: int, index2: int):
|
715
|
-
chain1 = chains[index1]
|
716
|
-
chain2 = chains[index2]
|
717
|
-
tail = chain1[-1]
|
718
|
-
tail2 = chain1[-2]
|
719
|
-
head = chain2[0]
|
720
|
-
head2 = chain2[1]
|
721
|
-
|
722
|
-
if Point.collinear(tail2, tail, head):
|
723
|
-
list_pop(chain1)
|
724
|
-
tail = tail2
|
725
|
-
if Point.collinear(tail, head, head2):
|
726
|
-
list_shift(chain2)
|
727
|
-
|
728
|
-
chains[index1] = chain1 + chain2
|
729
|
-
list_splice(chains, index2, 1)
|
730
|
-
|
731
|
-
f = scm.firstMatch.index
|
732
|
-
s = scm.secondMatch.index
|
733
|
-
|
734
|
-
reverseF = len(chains[f]) < len(chains[s])
|
735
|
-
if scm.firstMatch.matchesHead:
|
736
|
-
if scm.secondMatch.matchesHead:
|
737
|
-
if reverseF:
|
738
|
-
reverseChain(f)
|
739
|
-
appendChain(f, s)
|
740
|
-
else:
|
741
|
-
reverseChain(s)
|
742
|
-
appendChain(s, f)
|
743
|
-
else:
|
744
|
-
appendChain(s, f)
|
745
|
-
else:
|
746
|
-
if scm.secondMatch.matchesHead:
|
747
|
-
appendChain(f, s)
|
748
|
-
else:
|
749
|
-
if reverseF:
|
750
|
-
reverseChain(f)
|
751
|
-
appendChain(s, f)
|
752
|
-
else:
|
753
|
-
reverseChain(s)
|
754
|
-
appendChain(f, s)
|
755
|
-
|
756
|
-
return regions
|
757
|
-
|
758
|
-
|
759
|
-
def __select(segments: typing.List[Segment], selection: typing.List[int]):
|
760
|
-
result: typing.List[Segment] = []
|
761
|
-
for seg in segments:
|
762
|
-
index = (
|
763
|
-
(8 if seg.myfill.above else 0)
|
764
|
-
+ (4 if seg.myfill.below else 0)
|
765
|
-
+ (2 if seg.otherfill is not None and seg.otherfill.above else 0)
|
766
|
-
+ (1 if seg.otherfill is not None and seg.otherfill.below else 0)
|
767
|
-
)
|
768
|
-
|
769
|
-
if selection[index] != 0:
|
770
|
-
result.append(
|
771
|
-
Segment(
|
772
|
-
start=seg.start,
|
773
|
-
end=seg.end,
|
774
|
-
myfill=Fill(selection[index] == 2, above=selection[index] == 1),
|
775
|
-
)
|
776
|
-
)
|
777
|
-
return result
|
778
|
-
|
779
|
-
|
780
|
-
# core API
|
781
|
-
def segments(poly: Polygon) -> PolySegments:
|
782
|
-
i = RegionIntersecter()
|
783
|
-
for region in poly.regions:
|
784
|
-
i.addRegion(region)
|
785
|
-
return PolySegments(i.calculate(poly.isInverted), poly.isInverted)
|
786
|
-
|
787
|
-
|
788
|
-
def combine(segments1: PolySegments, segments2: PolySegments) -> CombinedPolySegments:
|
789
|
-
i = SegmentIntersecter()
|
790
|
-
return CombinedPolySegments(
|
791
|
-
i.calculate(
|
792
|
-
segments1.segments,
|
793
|
-
segments1.isInverted,
|
794
|
-
segments2.segments,
|
795
|
-
segments2.isInverted,
|
796
|
-
),
|
797
|
-
segments1.isInverted,
|
798
|
-
segments2.isInverted,
|
799
|
-
)
|
800
|
-
|
801
|
-
|
802
|
-
def selectUnion(polyseg: CombinedPolySegments) -> PolySegments:
|
803
|
-
return PolySegments(
|
804
|
-
segments=__select(
|
805
|
-
# fmt:off
|
806
|
-
polyseg.combined, [
|
807
|
-
0, 2, 1, 0,
|
808
|
-
2, 2, 0, 0,
|
809
|
-
1, 0, 1, 0,
|
810
|
-
0, 0, 0, 0,
|
811
|
-
]
|
812
|
-
# fmt:on
|
813
|
-
),
|
814
|
-
isInverted=(polyseg.isInverted1 or polyseg.isInverted2),
|
815
|
-
)
|
816
|
-
|
817
|
-
|
818
|
-
def selectIntersect(polyseg: CombinedPolySegments) -> PolySegments:
|
819
|
-
return PolySegments(
|
820
|
-
segments=__select(
|
821
|
-
# fmt:off
|
822
|
-
polyseg.combined, [
|
823
|
-
0, 0, 0, 0,
|
824
|
-
0, 2, 0, 2,
|
825
|
-
0, 0, 1, 1,
|
826
|
-
0, 2, 1, 0
|
827
|
-
]
|
828
|
-
# fmt:on
|
829
|
-
),
|
830
|
-
isInverted=(polyseg.isInverted1 and polyseg.isInverted2),
|
831
|
-
)
|
832
|
-
|
833
|
-
|
834
|
-
def selectDifference(polyseg: CombinedPolySegments) -> PolySegments:
|
835
|
-
return PolySegments(
|
836
|
-
segments=__select(
|
837
|
-
# fmt:off
|
838
|
-
polyseg.combined, [
|
839
|
-
0, 0, 0, 0,
|
840
|
-
2, 0, 2, 0,
|
841
|
-
1, 1, 0, 0,
|
842
|
-
0, 1, 2, 0
|
843
|
-
]
|
844
|
-
# fmt:on
|
845
|
-
),
|
846
|
-
isInverted=(polyseg.isInverted1 and not polyseg.isInverted2),
|
847
|
-
)
|
848
|
-
|
849
|
-
|
850
|
-
def selectDifferenceRev(polyseg: CombinedPolySegments) -> PolySegments:
|
851
|
-
return PolySegments(
|
852
|
-
segments=__select(
|
853
|
-
# fmt:off
|
854
|
-
polyseg.combined, [
|
855
|
-
0, 2, 1, 0,
|
856
|
-
0, 0, 1, 1,
|
857
|
-
0, 2, 0, 2,
|
858
|
-
0, 0, 0, 0
|
859
|
-
]
|
860
|
-
# fmt:on
|
861
|
-
),
|
862
|
-
isInverted=(not polyseg.isInverted1 and polyseg.isInverted2),
|
863
|
-
)
|
864
|
-
|
865
|
-
|
866
|
-
def selectXor(polyseg: CombinedPolySegments) -> PolySegments:
|
867
|
-
return PolySegments(
|
868
|
-
segments=__select(
|
869
|
-
# fmt:off
|
870
|
-
polyseg.combined, [
|
871
|
-
0, 2, 1, 0,
|
872
|
-
2, 0, 0, 1,
|
873
|
-
1, 0, 0, 2,
|
874
|
-
0, 1, 2, 0
|
875
|
-
]
|
876
|
-
# fmt:on
|
877
|
-
),
|
878
|
-
isInverted=(polyseg.isInverted1 != polyseg.isInverted2),
|
879
|
-
)
|
880
|
-
|
881
|
-
|
882
|
-
def polygon(segments: PolySegments):
|
883
|
-
return Polygon(segmentChainer(segments.segments), segments.isInverted)
|
884
|
-
|
885
|
-
|
886
|
-
def __operate(
|
887
|
-
poly1: Polygon,
|
888
|
-
poly2: Polygon,
|
889
|
-
selector: typing.Callable[[CombinedPolySegments], PolySegments],
|
890
|
-
):
|
891
|
-
firstPolygonRegions = segments(poly1)
|
892
|
-
secondPolygonRegions = segments(poly2)
|
893
|
-
combinedSegments = combine(firstPolygonRegions, secondPolygonRegions)
|
894
|
-
seg = selector(combinedSegments)
|
895
|
-
return polygon(seg)
|
896
|
-
|
897
|
-
|
898
|
-
# helper functions for common operations
|
899
|
-
|
900
|
-
|
901
|
-
@typing.overload
|
902
|
-
def union(polygons: typing.List[Polygon]) -> Polygon:
|
903
|
-
...
|
904
|
-
|
905
|
-
|
906
|
-
@typing.overload
|
907
|
-
def union(poly1: Polygon, poly2: Polygon) -> Polygon:
|
908
|
-
...
|
909
|
-
|
910
|
-
|
911
|
-
def union(*args):
|
912
|
-
if len(args) == 1 and isinstance(args[0], list):
|
913
|
-
polygons = args[0]
|
914
|
-
seg1 = segments(polygons[0])
|
915
|
-
for i in range(1, len(polygons)):
|
916
|
-
seg2 = segments(polygons[i])
|
917
|
-
comb = combine(seg1, seg2)
|
918
|
-
seg1 = selectUnion(comb)
|
919
|
-
|
920
|
-
return polygon(seg1)
|
921
|
-
elif (
|
922
|
-
len(args) == 2 and isinstance(args[0], Polygon) and isinstance(args[1], Polygon)
|
923
|
-
):
|
924
|
-
return __operate(args[0], args[1], selectUnion)
|
925
|
-
|
926
|
-
|
927
|
-
def intersect(poly1: Polygon, poly2: Polygon):
|
928
|
-
return __operate(poly1, poly2, selectIntersect)
|
929
|
-
|
930
|
-
|
931
|
-
def difference(poly1: Polygon, poly2: Polygon):
|
932
|
-
return __operate(poly1, poly2, selectDifference)
|
933
|
-
|
934
|
-
|
935
|
-
def differenceRev(poly1: Polygon, poly2: Polygon):
|
936
|
-
return __operate(poly1, poly2, selectDifferenceRev)
|
937
|
-
|
938
|
-
|
939
|
-
def xor(poly1: Polygon, poly2: Polygon):
|
940
|
-
return __operate(poly1, poly2, selectXor)
|
1
|
+
"""
|
2
|
+
https://github.com/KaivnD/pypolybool
|
3
|
+
MIT License
|
4
|
+
"""
|
5
|
+
|
6
|
+
import typing
|
7
|
+
|
8
|
+
tolerance = 1e-10
|
9
|
+
|
10
|
+
T = typing.TypeVar("T")
|
11
|
+
TPoint = typing.TypeVar("TPoint", bound="Point")
|
12
|
+
|
13
|
+
|
14
|
+
class PolyBoolException(Exception):
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
class Point:
|
19
|
+
def __init__(self: TPoint, x: float, y: float) -> None:
|
20
|
+
self.x = x
|
21
|
+
self.y = y
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def collinear(pt1: TPoint, pt2: TPoint, pt3: TPoint) -> bool:
|
25
|
+
dx1 = pt1.x - pt2.x
|
26
|
+
dy1 = pt1.y - pt2.y
|
27
|
+
dx2 = pt2.x - pt3.x
|
28
|
+
dy2 = pt2.y - pt3.y
|
29
|
+
return abs(dx1 * dy2 - dx2 * dy1) < tolerance
|
30
|
+
|
31
|
+
@staticmethod
|
32
|
+
def compare(pt1: TPoint, pt2: TPoint):
|
33
|
+
if abs(pt1.x - pt2.x) < tolerance:
|
34
|
+
return 0 if abs(pt1.y - pt2.y) < tolerance else -1 if pt1.y < pt2.y else 1
|
35
|
+
return -1 if pt1.x < pt2.x else 1
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def pointAboveOrOnLine(point: TPoint, left: TPoint, right: TPoint):
|
39
|
+
return (right.x - left.x) * (point.y - left.y) - (right.y - left.y) * (
|
40
|
+
point.x - left.x
|
41
|
+
) >= -tolerance
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def between(point: TPoint, left: TPoint, right: TPoint):
|
45
|
+
dPyLy = point.y - left.y
|
46
|
+
dRxLx = right.x - left.x
|
47
|
+
dPxLx = point.x - left.x
|
48
|
+
dRyLy = right.y - left.y
|
49
|
+
|
50
|
+
dot = dPxLx * dRxLx + dPyLy * dRyLy
|
51
|
+
if dot < tolerance:
|
52
|
+
return False
|
53
|
+
|
54
|
+
sqlen = dRxLx * dRxLx + dRyLy * dRyLy
|
55
|
+
if dot - sqlen > -tolerance:
|
56
|
+
return False
|
57
|
+
|
58
|
+
return True
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def linesIntersect(a0: TPoint, a1: TPoint, b0: TPoint, b1: TPoint):
|
62
|
+
adx = a1.x - a0.x
|
63
|
+
ady = a1.y - a0.y
|
64
|
+
bdx = b1.x - b0.x
|
65
|
+
bdy = b1.y - b0.y
|
66
|
+
|
67
|
+
axb = adx * bdy - ady * bdx
|
68
|
+
|
69
|
+
if abs(axb) < tolerance:
|
70
|
+
return None
|
71
|
+
|
72
|
+
dx = a0.x - b0.x
|
73
|
+
dy = a0.y - b0.y
|
74
|
+
|
75
|
+
a = (bdx * dy - bdy * dx) / axb
|
76
|
+
b = (adx * dy - ady * dx) / axb
|
77
|
+
|
78
|
+
return IntersectionPoint(
|
79
|
+
Point.__calcAlongUsingValue(a),
|
80
|
+
Point.__calcAlongUsingValue(b),
|
81
|
+
Point(a0.x + a * adx, a0.y + a * ady),
|
82
|
+
)
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
def __calcAlongUsingValue(value: float):
|
86
|
+
if value <= -tolerance:
|
87
|
+
return -2
|
88
|
+
elif value < tolerance:
|
89
|
+
return -1
|
90
|
+
elif value - 1 <= -tolerance:
|
91
|
+
return 0
|
92
|
+
elif value - 1 < tolerance:
|
93
|
+
return 1
|
94
|
+
else:
|
95
|
+
return 2
|
96
|
+
|
97
|
+
def __eq__(self, __o: object) -> bool:
|
98
|
+
if not isinstance(__o, Point):
|
99
|
+
return False
|
100
|
+
return abs(self.x - __o.x) < tolerance and abs(self.y - __o.y) < tolerance
|
101
|
+
|
102
|
+
def __repr__(self) -> str:
|
103
|
+
return f"{self.x},{self.y}"
|
104
|
+
|
105
|
+
def __str__(self) -> str:
|
106
|
+
return f"{self.x},{self.y}"
|
107
|
+
|
108
|
+
|
109
|
+
class Fill:
|
110
|
+
def __init__(self, below: bool = None, above: bool = None) -> None:
|
111
|
+
self.below = below
|
112
|
+
self.above = above
|
113
|
+
|
114
|
+
def __repr__(self) -> str:
|
115
|
+
return f"{self.above},{self.below}"
|
116
|
+
|
117
|
+
def __str__(self) -> str:
|
118
|
+
return f"{self.above},{self.below}"
|
119
|
+
|
120
|
+
|
121
|
+
class Segment:
|
122
|
+
def __init__(
|
123
|
+
self, start: Point, end: Point, myfill: Fill = None, otherfill: Fill = None
|
124
|
+
):
|
125
|
+
self.start = start
|
126
|
+
self.end = end
|
127
|
+
self.myfill = myfill
|
128
|
+
self.otherfill = otherfill
|
129
|
+
|
130
|
+
def __repr__(self) -> str:
|
131
|
+
return f"S: {self.start}, E: {self.end}"
|
132
|
+
|
133
|
+
def __str__(self) -> str:
|
134
|
+
return f"S: {self.start}, E: {self.end}"
|
135
|
+
|
136
|
+
|
137
|
+
class PolySegments:
|
138
|
+
def __init__(
|
139
|
+
self, segments: typing.List[Segment] = None, isInverted: bool = False
|
140
|
+
) -> None:
|
141
|
+
self.segments = segments
|
142
|
+
self.isInverted = isInverted
|
143
|
+
|
144
|
+
|
145
|
+
class CombinedPolySegments:
|
146
|
+
def __init__(
|
147
|
+
self,
|
148
|
+
combined: typing.List[Segment] = None,
|
149
|
+
isInverted1: bool = False,
|
150
|
+
isInverted2: bool = False,
|
151
|
+
) -> None:
|
152
|
+
self.combined = combined
|
153
|
+
self.isInverted1 = isInverted1
|
154
|
+
self.isInverted2 = isInverted2
|
155
|
+
|
156
|
+
|
157
|
+
class Matcher:
|
158
|
+
def __init__(
|
159
|
+
self,
|
160
|
+
index: int,
|
161
|
+
matchesHead: bool,
|
162
|
+
matchesPt1: bool,
|
163
|
+
) -> None:
|
164
|
+
self.index = index
|
165
|
+
self.matchesHead = matchesHead
|
166
|
+
self.matchesPt1 = matchesPt1
|
167
|
+
|
168
|
+
|
169
|
+
class IntersectionPoint:
|
170
|
+
def __init__(self, alongA: int, alongB: int, pt: Point) -> None:
|
171
|
+
self.alongA = alongA
|
172
|
+
self.alongB = alongB
|
173
|
+
self.pt = pt
|
174
|
+
|
175
|
+
|
176
|
+
TNode = typing.TypeVar("TNode", bound="Node")
|
177
|
+
|
178
|
+
|
179
|
+
class Node:
|
180
|
+
def __init__(
|
181
|
+
self: TNode,
|
182
|
+
isRoot: bool = False,
|
183
|
+
isStart: bool = False,
|
184
|
+
pt: Point = None,
|
185
|
+
seg: Segment = None,
|
186
|
+
primary: bool = False,
|
187
|
+
next: TNode = None,
|
188
|
+
previous: TNode = None,
|
189
|
+
other: TNode = None,
|
190
|
+
ev: TNode = None,
|
191
|
+
status: TNode = None,
|
192
|
+
remove: typing.Callable = None,
|
193
|
+
):
|
194
|
+
self.status = status
|
195
|
+
self.other = other
|
196
|
+
self.ev = ev
|
197
|
+
self.previous = previous
|
198
|
+
self.next = next
|
199
|
+
self.isRoot = isRoot
|
200
|
+
self.remove = remove
|
201
|
+
self.isStart = isStart
|
202
|
+
self.pt = pt
|
203
|
+
self.seg = seg
|
204
|
+
self.primary = primary
|
205
|
+
|
206
|
+
|
207
|
+
class Transition:
|
208
|
+
def __init__(
|
209
|
+
self, after: Node, before: Node, insert: typing.Callable[[Node], Node]
|
210
|
+
) -> None:
|
211
|
+
self.after = after
|
212
|
+
self.before = before
|
213
|
+
self.insert = insert
|
214
|
+
|
215
|
+
|
216
|
+
class LinkedList:
|
217
|
+
def __init__(self) -> None:
|
218
|
+
self.__root = Node(isRoot=True)
|
219
|
+
|
220
|
+
def exists(self, node: Node):
|
221
|
+
if node is None or node is self.__root:
|
222
|
+
return False
|
223
|
+
return True
|
224
|
+
|
225
|
+
def isEmpty(self):
|
226
|
+
return self.__root.next is None
|
227
|
+
|
228
|
+
def getHead(self):
|
229
|
+
return self.__root.next
|
230
|
+
|
231
|
+
def insertBefore(self, node: Node, check: typing.Callable[[Node], bool]):
|
232
|
+
last = self.__root
|
233
|
+
here = self.__root.next
|
234
|
+
|
235
|
+
while here is not None:
|
236
|
+
if check(here):
|
237
|
+
node.previous = here.previous
|
238
|
+
node.next = here
|
239
|
+
here.previous.next = node
|
240
|
+
here.previous = node
|
241
|
+
return
|
242
|
+
last = here
|
243
|
+
here = here.next
|
244
|
+
last.next = node
|
245
|
+
node.previous = last
|
246
|
+
node.next = None
|
247
|
+
|
248
|
+
def findTransition(self, check: typing.Callable[[Node], bool]):
|
249
|
+
previous = self.__root
|
250
|
+
here = self.__root.next
|
251
|
+
|
252
|
+
while here is not None:
|
253
|
+
if check(here):
|
254
|
+
break
|
255
|
+
previous = here
|
256
|
+
here = here.next
|
257
|
+
|
258
|
+
def insert_func(node: Node):
|
259
|
+
node.previous = previous
|
260
|
+
node.next = here
|
261
|
+
previous.next = node
|
262
|
+
if here is not None:
|
263
|
+
here.previous = node
|
264
|
+
return node
|
265
|
+
|
266
|
+
return Transition(
|
267
|
+
before=(None if previous is self.__root else previous),
|
268
|
+
after=here,
|
269
|
+
insert=insert_func,
|
270
|
+
)
|
271
|
+
|
272
|
+
@staticmethod
|
273
|
+
def node(data: Node):
|
274
|
+
data.previous = None
|
275
|
+
data.next = None
|
276
|
+
|
277
|
+
def remove_func():
|
278
|
+
data.previous.next = data.next
|
279
|
+
if data.next is not None:
|
280
|
+
data.next.previous = data.previous
|
281
|
+
data.previous = None
|
282
|
+
data.next = None
|
283
|
+
|
284
|
+
data.remove = remove_func
|
285
|
+
return data
|
286
|
+
|
287
|
+
|
288
|
+
RegionInput = typing.Union[typing.List[Point], typing.List[typing.Tuple[float, float]]]
|
289
|
+
Region = typing.List[Point]
|
290
|
+
|
291
|
+
|
292
|
+
class Polygon:
|
293
|
+
def __init__(self, regions: typing.List[RegionInput], isInverted=False) -> None:
|
294
|
+
_regions: typing.List[Region] = []
|
295
|
+
for region in regions:
|
296
|
+
tmp: Region = []
|
297
|
+
for pt in region:
|
298
|
+
if isinstance(pt, Point):
|
299
|
+
tmp.append(pt)
|
300
|
+
elif isinstance(pt, tuple):
|
301
|
+
x, y = pt
|
302
|
+
tmp.append(Point(x, y))
|
303
|
+
_regions.append(tmp)
|
304
|
+
|
305
|
+
self.regions = _regions
|
306
|
+
self.isInverted = isInverted
|
307
|
+
|
308
|
+
|
309
|
+
class Intersecter:
|
310
|
+
def __init__(self, selfIntersection: bool) -> None:
|
311
|
+
self.selfIntersection = selfIntersection
|
312
|
+
self.__eventRoot = LinkedList()
|
313
|
+
|
314
|
+
def newsegment(self, start: Point, end: Point):
|
315
|
+
return Segment(start=start, end=end, myfill=Fill())
|
316
|
+
|
317
|
+
def segmentCopy(self, start: Point, end: Point, seg: Segment):
|
318
|
+
return Segment(
|
319
|
+
start=start, end=end, myfill=Fill(seg.myfill.below, seg.myfill.above)
|
320
|
+
)
|
321
|
+
|
322
|
+
def __eventCompare(
|
323
|
+
self,
|
324
|
+
p1IsStart: bool,
|
325
|
+
p11: Point,
|
326
|
+
p12: Point,
|
327
|
+
p2IsStart: bool,
|
328
|
+
p21: Point,
|
329
|
+
p22: Point,
|
330
|
+
):
|
331
|
+
comp = Point.compare(p11, p21)
|
332
|
+
if comp != 0:
|
333
|
+
return comp
|
334
|
+
|
335
|
+
if p12 == p22:
|
336
|
+
return 0
|
337
|
+
|
338
|
+
if p1IsStart != p2IsStart:
|
339
|
+
return 1 if p1IsStart else -1
|
340
|
+
|
341
|
+
return (
|
342
|
+
1
|
343
|
+
if Point.pointAboveOrOnLine(
|
344
|
+
p12, p21 if p2IsStart else p22, p22 if p2IsStart else p21
|
345
|
+
)
|
346
|
+
else -1
|
347
|
+
)
|
348
|
+
|
349
|
+
def __eventAdd(self, ev: Node, otherPt: Point):
|
350
|
+
def check_func(here: Node):
|
351
|
+
comp = self.__eventCompare(
|
352
|
+
ev.isStart, ev.pt, otherPt, here.isStart, here.pt, here.other.pt
|
353
|
+
)
|
354
|
+
return comp < 0
|
355
|
+
|
356
|
+
self.__eventRoot.insertBefore(ev, check_func)
|
357
|
+
|
358
|
+
def __eventAddSegmentStart(self, segment: Segment, primary: bool):
|
359
|
+
evStart = LinkedList.node(
|
360
|
+
Node(
|
361
|
+
isStart=True,
|
362
|
+
pt=segment.start,
|
363
|
+
seg=segment,
|
364
|
+
primary=primary,
|
365
|
+
)
|
366
|
+
)
|
367
|
+
self.__eventAdd(evStart, segment.end)
|
368
|
+
return evStart
|
369
|
+
|
370
|
+
def __eventAddSegmentEnd(self, evStart: Node, segment: Segment, primary: bool):
|
371
|
+
evEnd = LinkedList.node(
|
372
|
+
Node(
|
373
|
+
isStart=False,
|
374
|
+
pt=segment.end,
|
375
|
+
seg=segment,
|
376
|
+
primary=primary,
|
377
|
+
other=evStart,
|
378
|
+
)
|
379
|
+
)
|
380
|
+
evStart.other = evEnd
|
381
|
+
self.__eventAdd(evEnd, evStart.pt)
|
382
|
+
|
383
|
+
def eventAddSegment(self, segment: Segment, primary: bool):
|
384
|
+
evStart = self.__eventAddSegmentStart(segment, primary)
|
385
|
+
self.__eventAddSegmentEnd(evStart, segment, primary)
|
386
|
+
return evStart
|
387
|
+
|
388
|
+
def __eventUpdateEnd(self, ev: Node, end: Point):
|
389
|
+
ev.other.remove()
|
390
|
+
ev.seg.end = end
|
391
|
+
ev.other.pt = end
|
392
|
+
self.__eventAdd(ev.other, ev.pt)
|
393
|
+
|
394
|
+
def __eventDivide(self, ev: Node, pt: Point):
|
395
|
+
ns = self.segmentCopy(pt, ev.seg.end, ev.seg)
|
396
|
+
self.__eventUpdateEnd(ev, pt)
|
397
|
+
return self.eventAddSegment(ns, ev.primary)
|
398
|
+
|
399
|
+
def __statusCompare(self, ev1: Node, ev2: Node):
|
400
|
+
a1 = ev1.seg.start
|
401
|
+
a2 = ev1.seg.end
|
402
|
+
b1 = ev2.seg.start
|
403
|
+
b2 = ev2.seg.end
|
404
|
+
|
405
|
+
if Point.collinear(a1, b1, b2):
|
406
|
+
if Point.collinear(a2, b1, b2):
|
407
|
+
return 1
|
408
|
+
return 1 if Point.pointAboveOrOnLine(a2, b1, b2) else -1
|
409
|
+
return 1 if Point.pointAboveOrOnLine(a1, b1, b2) else -1
|
410
|
+
|
411
|
+
def __statusFindSurrounding(self, statusRoot: LinkedList, ev: Node):
|
412
|
+
def check_func(here: Node):
|
413
|
+
return self.__statusCompare(ev, here.ev) > 0
|
414
|
+
|
415
|
+
return statusRoot.findTransition(check_func)
|
416
|
+
|
417
|
+
def __checkIntersection(self, ev1: Node, ev2: Node):
|
418
|
+
seg1 = ev1.seg
|
419
|
+
seg2 = ev2.seg
|
420
|
+
a1 = seg1.start
|
421
|
+
a2 = seg1.end
|
422
|
+
b1 = seg2.start
|
423
|
+
b2 = seg2.end
|
424
|
+
|
425
|
+
i = Point.linesIntersect(a1, a2, b1, b2)
|
426
|
+
if i is None:
|
427
|
+
if not Point.collinear(a1, a2, b1):
|
428
|
+
return None
|
429
|
+
if a1 == b2 or a2 == b1:
|
430
|
+
return None
|
431
|
+
a1EquB1 = a1 == b1
|
432
|
+
a2EquB2 = a2 == b2
|
433
|
+
if a1EquB1 and a2EquB2:
|
434
|
+
return ev2
|
435
|
+
|
436
|
+
a1Between = not a1EquB1 and Point.between(a1, b1, b2)
|
437
|
+
a2Between = not a2EquB2 and Point.between(a2, b1, b2)
|
438
|
+
|
439
|
+
if a1EquB1:
|
440
|
+
if a2Between:
|
441
|
+
self.__eventDivide(ev2, a2)
|
442
|
+
else:
|
443
|
+
self.__eventDivide(ev1, b2)
|
444
|
+
|
445
|
+
return ev2
|
446
|
+
elif a1Between:
|
447
|
+
if not a2EquB2:
|
448
|
+
if a2Between:
|
449
|
+
self.__eventDivide(ev2, a2)
|
450
|
+
else:
|
451
|
+
self.__eventDivide(ev1, b2)
|
452
|
+
self.__eventDivide(ev2, a1)
|
453
|
+
else:
|
454
|
+
if i.alongA == 0:
|
455
|
+
if i.alongB == -1:
|
456
|
+
self.__eventDivide(ev1, b1)
|
457
|
+
elif i.alongB == 0:
|
458
|
+
self.__eventDivide(ev1, i.pt)
|
459
|
+
elif i.alongB == 1:
|
460
|
+
self.__eventDivide(ev1, b2)
|
461
|
+
if i.alongB == 0:
|
462
|
+
if i.alongA == -1:
|
463
|
+
self.__eventDivide(ev2, a1)
|
464
|
+
elif i.alongA == 0:
|
465
|
+
self.__eventDivide(ev2, i.pt)
|
466
|
+
elif i.alongA == 1:
|
467
|
+
self.__eventDivide(ev2, a2)
|
468
|
+
return None
|
469
|
+
|
470
|
+
def __checkBothIntersections(self, above: Node, ev: Node, below: Node):
|
471
|
+
if above is not None:
|
472
|
+
eve = self.__checkIntersection(ev, above)
|
473
|
+
if eve is not None:
|
474
|
+
return eve
|
475
|
+
if below is not None:
|
476
|
+
return self.__checkIntersection(ev, below)
|
477
|
+
|
478
|
+
return None
|
479
|
+
|
480
|
+
def calculate(self, primaryPolyInverted: bool, secondaryPolyInverted: bool):
|
481
|
+
statusRoot = LinkedList()
|
482
|
+
segments: typing.List[Segment] = []
|
483
|
+
|
484
|
+
cnt = 0
|
485
|
+
|
486
|
+
while not self.__eventRoot.isEmpty():
|
487
|
+
cnt += 1
|
488
|
+
ev = self.__eventRoot.getHead()
|
489
|
+
if ev.isStart:
|
490
|
+
surrounding = self.__statusFindSurrounding(statusRoot, ev)
|
491
|
+
above = (
|
492
|
+
surrounding.before.ev if surrounding.before is not None else None
|
493
|
+
)
|
494
|
+
below = surrounding.after.ev if surrounding.after is not None else None
|
495
|
+
|
496
|
+
eve = self.__checkBothIntersections(above, ev, below)
|
497
|
+
if eve is not None:
|
498
|
+
if self.selfIntersection:
|
499
|
+
toggle = False
|
500
|
+
if ev.seg.myfill.below is None:
|
501
|
+
toggle = True
|
502
|
+
else:
|
503
|
+
toggle = ev.seg.myfill.above != ev.seg.myfill.below
|
504
|
+
|
505
|
+
if toggle:
|
506
|
+
eve.seg.myfill.above = not eve.seg.myfill.above
|
507
|
+
else:
|
508
|
+
eve.seg.otherfill = ev.seg.myfill
|
509
|
+
ev.other.remove()
|
510
|
+
ev.remove()
|
511
|
+
|
512
|
+
if self.__eventRoot.getHead() is not ev:
|
513
|
+
continue
|
514
|
+
|
515
|
+
if self.selfIntersection:
|
516
|
+
toggle = False
|
517
|
+
if ev.seg.myfill.below is None:
|
518
|
+
toggle = True
|
519
|
+
else:
|
520
|
+
toggle = ev.seg.myfill.above != ev.seg.myfill.below
|
521
|
+
|
522
|
+
if below is None:
|
523
|
+
ev.seg.myfill.below = primaryPolyInverted
|
524
|
+
else:
|
525
|
+
ev.seg.myfill.below = below.seg.myfill.above
|
526
|
+
|
527
|
+
if toggle:
|
528
|
+
ev.seg.myfill.above = not ev.seg.myfill.below
|
529
|
+
else:
|
530
|
+
ev.seg.myfill.above = ev.seg.myfill.below
|
531
|
+
else:
|
532
|
+
if ev.seg.otherfill is None:
|
533
|
+
inside = False
|
534
|
+
if below is None:
|
535
|
+
inside = (
|
536
|
+
secondaryPolyInverted
|
537
|
+
if ev.primary
|
538
|
+
else primaryPolyInverted
|
539
|
+
)
|
540
|
+
else:
|
541
|
+
if ev.primary == below.primary:
|
542
|
+
inside = below.seg.otherfill.above
|
543
|
+
else:
|
544
|
+
inside = below.seg.myfill.above
|
545
|
+
ev.seg.otherfill = Fill(inside, inside)
|
546
|
+
ev.other.status = surrounding.insert(LinkedList.node(Node(ev=ev)))
|
547
|
+
else:
|
548
|
+
st = ev.status
|
549
|
+
if st is None:
|
550
|
+
raise PolyBoolException(
|
551
|
+
"PolyBool: Zero-length segment detected; your epsilon is probably too small or too large"
|
552
|
+
)
|
553
|
+
if statusRoot.exists(st.previous) and statusRoot.exists(st.next):
|
554
|
+
self.__checkIntersection(st.previous.ev, st.next.ev)
|
555
|
+
st.remove()
|
556
|
+
|
557
|
+
if not ev.primary:
|
558
|
+
s = ev.seg.myfill
|
559
|
+
ev.seg.myfill = ev.seg.otherfill
|
560
|
+
ev.seg.otherfill = s
|
561
|
+
segments.append(ev.seg)
|
562
|
+
self.__eventRoot.getHead().remove()
|
563
|
+
return segments
|
564
|
+
|
565
|
+
|
566
|
+
class RegionIntersecter(Intersecter):
|
567
|
+
def __init__(self) -> None:
|
568
|
+
super().__init__(True)
|
569
|
+
|
570
|
+
def addRegion(self, region: Region):
|
571
|
+
pt1: Point
|
572
|
+
pt2 = region[-1]
|
573
|
+
for i in range(len(region)):
|
574
|
+
pt1 = pt2
|
575
|
+
pt2 = region[i]
|
576
|
+
forward = Point.compare(pt1, pt2)
|
577
|
+
|
578
|
+
if forward == 0:
|
579
|
+
continue
|
580
|
+
|
581
|
+
seg = self.newsegment(
|
582
|
+
pt1 if forward < 0 else pt2, pt2 if forward < 0 else pt1
|
583
|
+
)
|
584
|
+
|
585
|
+
self.eventAddSegment(seg, True)
|
586
|
+
|
587
|
+
def calculate(self, inverted: bool):
|
588
|
+
return super().calculate(inverted, False)
|
589
|
+
|
590
|
+
|
591
|
+
class SegmentIntersecter(Intersecter):
|
592
|
+
def __init__(self) -> None:
|
593
|
+
super().__init__(False)
|
594
|
+
|
595
|
+
def calculate(
|
596
|
+
self,
|
597
|
+
segments1: typing.List[Segment],
|
598
|
+
isInverted1: bool,
|
599
|
+
segments2: typing.List[Segment],
|
600
|
+
isInverted2: bool,
|
601
|
+
):
|
602
|
+
for seg in segments1:
|
603
|
+
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), True)
|
604
|
+
|
605
|
+
for seg in segments2:
|
606
|
+
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), False)
|
607
|
+
|
608
|
+
return super().calculate(isInverted1, isInverted2)
|
609
|
+
|
610
|
+
|
611
|
+
class SegmentChainerMatcher:
|
612
|
+
def __init__(self) -> None:
|
613
|
+
self.firstMatch = Matcher(0, False, False)
|
614
|
+
self.secondMatch = Matcher(0, False, False)
|
615
|
+
|
616
|
+
self.nextMatch = self.firstMatch
|
617
|
+
|
618
|
+
def setMatch(self, index: int, matchesHead: bool, matchesPt1: bool):
|
619
|
+
self.nextMatch.index = index
|
620
|
+
self.nextMatch.matchesHead = matchesHead
|
621
|
+
self.nextMatch.matchesPt1 = matchesPt1
|
622
|
+
if self.nextMatch is self.firstMatch:
|
623
|
+
self.nextMatch = self.secondMatch
|
624
|
+
return False
|
625
|
+
self.nextMatch = None
|
626
|
+
return True
|
627
|
+
|
628
|
+
|
629
|
+
def list_shift(list: typing.List):
|
630
|
+
list.pop(0)
|
631
|
+
|
632
|
+
|
633
|
+
def list_pop(list: typing.List):
|
634
|
+
list.pop()
|
635
|
+
|
636
|
+
|
637
|
+
def list_splice(list: typing.List, index: int, count: int):
|
638
|
+
del list[index : index + count]
|
639
|
+
|
640
|
+
|
641
|
+
def list_unshift(list: typing.List[T], element: T):
|
642
|
+
list.insert(0, element)
|
643
|
+
|
644
|
+
|
645
|
+
def segmentChainer(segments: typing.List[Segment]) -> typing.List[Region]:
|
646
|
+
regions: typing.List[Region] = []
|
647
|
+
chains: typing.List[typing.List[Point]] = []
|
648
|
+
|
649
|
+
for seg in segments:
|
650
|
+
pt1 = seg.start
|
651
|
+
pt2 = seg.end
|
652
|
+
if pt1 == pt2:
|
653
|
+
continue
|
654
|
+
|
655
|
+
scm = SegmentChainerMatcher()
|
656
|
+
|
657
|
+
for i in range(len(chains)):
|
658
|
+
chain = chains[i]
|
659
|
+
head = chain[0]
|
660
|
+
tail = chain[-1]
|
661
|
+
|
662
|
+
if head == pt1:
|
663
|
+
if scm.setMatch(i, True, True):
|
664
|
+
break
|
665
|
+
elif head == pt2:
|
666
|
+
if scm.setMatch(i, True, False):
|
667
|
+
break
|
668
|
+
elif tail == pt1:
|
669
|
+
if scm.setMatch(i, False, True):
|
670
|
+
break
|
671
|
+
elif tail == pt2:
|
672
|
+
if scm.setMatch(i, False, False):
|
673
|
+
break
|
674
|
+
|
675
|
+
if scm.nextMatch is scm.firstMatch:
|
676
|
+
chains.append([pt1, pt2])
|
677
|
+
continue
|
678
|
+
|
679
|
+
if scm.nextMatch is scm.secondMatch:
|
680
|
+
index = scm.firstMatch.index
|
681
|
+
pt = pt2 if scm.firstMatch.matchesPt1 else pt1
|
682
|
+
addToHead = scm.firstMatch.matchesHead
|
683
|
+
|
684
|
+
chain = chains[index]
|
685
|
+
grow = chain[0] if addToHead else chain[-1]
|
686
|
+
grow2 = chain[1] if addToHead else chain[-2]
|
687
|
+
oppo = chain[-1] if addToHead else chain[0]
|
688
|
+
oppo2 = chain[-2] if addToHead else chain[1]
|
689
|
+
|
690
|
+
if Point.collinear(grow2, grow, pt):
|
691
|
+
if addToHead:
|
692
|
+
list_shift(chain)
|
693
|
+
else:
|
694
|
+
list_pop(chain)
|
695
|
+
grow = grow2
|
696
|
+
if oppo == pt:
|
697
|
+
list_splice(chains, index, 1)
|
698
|
+
if Point.collinear(oppo2, oppo, grow):
|
699
|
+
if addToHead:
|
700
|
+
list_pop(chain)
|
701
|
+
else:
|
702
|
+
list_shift(chain)
|
703
|
+
regions.append(chain)
|
704
|
+
continue
|
705
|
+
if addToHead:
|
706
|
+
list_unshift(chain, pt)
|
707
|
+
else:
|
708
|
+
chain.append(pt)
|
709
|
+
continue
|
710
|
+
|
711
|
+
def reverseChain(index: int):
|
712
|
+
chains[index].reverse()
|
713
|
+
|
714
|
+
def appendChain(index1: int, index2: int):
|
715
|
+
chain1 = chains[index1]
|
716
|
+
chain2 = chains[index2]
|
717
|
+
tail = chain1[-1]
|
718
|
+
tail2 = chain1[-2]
|
719
|
+
head = chain2[0]
|
720
|
+
head2 = chain2[1]
|
721
|
+
|
722
|
+
if Point.collinear(tail2, tail, head):
|
723
|
+
list_pop(chain1)
|
724
|
+
tail = tail2
|
725
|
+
if Point.collinear(tail, head, head2):
|
726
|
+
list_shift(chain2)
|
727
|
+
|
728
|
+
chains[index1] = chain1 + chain2
|
729
|
+
list_splice(chains, index2, 1)
|
730
|
+
|
731
|
+
f = scm.firstMatch.index
|
732
|
+
s = scm.secondMatch.index
|
733
|
+
|
734
|
+
reverseF = len(chains[f]) < len(chains[s])
|
735
|
+
if scm.firstMatch.matchesHead:
|
736
|
+
if scm.secondMatch.matchesHead:
|
737
|
+
if reverseF:
|
738
|
+
reverseChain(f)
|
739
|
+
appendChain(f, s)
|
740
|
+
else:
|
741
|
+
reverseChain(s)
|
742
|
+
appendChain(s, f)
|
743
|
+
else:
|
744
|
+
appendChain(s, f)
|
745
|
+
else:
|
746
|
+
if scm.secondMatch.matchesHead:
|
747
|
+
appendChain(f, s)
|
748
|
+
else:
|
749
|
+
if reverseF:
|
750
|
+
reverseChain(f)
|
751
|
+
appendChain(s, f)
|
752
|
+
else:
|
753
|
+
reverseChain(s)
|
754
|
+
appendChain(f, s)
|
755
|
+
|
756
|
+
return regions
|
757
|
+
|
758
|
+
|
759
|
+
def __select(segments: typing.List[Segment], selection: typing.List[int]):
|
760
|
+
result: typing.List[Segment] = []
|
761
|
+
for seg in segments:
|
762
|
+
index = (
|
763
|
+
(8 if seg.myfill.above else 0)
|
764
|
+
+ (4 if seg.myfill.below else 0)
|
765
|
+
+ (2 if seg.otherfill is not None and seg.otherfill.above else 0)
|
766
|
+
+ (1 if seg.otherfill is not None and seg.otherfill.below else 0)
|
767
|
+
)
|
768
|
+
|
769
|
+
if selection[index] != 0:
|
770
|
+
result.append(
|
771
|
+
Segment(
|
772
|
+
start=seg.start,
|
773
|
+
end=seg.end,
|
774
|
+
myfill=Fill(selection[index] == 2, above=selection[index] == 1),
|
775
|
+
)
|
776
|
+
)
|
777
|
+
return result
|
778
|
+
|
779
|
+
|
780
|
+
# core API
|
781
|
+
def segments(poly: Polygon) -> PolySegments:
|
782
|
+
i = RegionIntersecter()
|
783
|
+
for region in poly.regions:
|
784
|
+
i.addRegion(region)
|
785
|
+
return PolySegments(i.calculate(poly.isInverted), poly.isInverted)
|
786
|
+
|
787
|
+
|
788
|
+
def combine(segments1: PolySegments, segments2: PolySegments) -> CombinedPolySegments:
|
789
|
+
i = SegmentIntersecter()
|
790
|
+
return CombinedPolySegments(
|
791
|
+
i.calculate(
|
792
|
+
segments1.segments,
|
793
|
+
segments1.isInverted,
|
794
|
+
segments2.segments,
|
795
|
+
segments2.isInverted,
|
796
|
+
),
|
797
|
+
segments1.isInverted,
|
798
|
+
segments2.isInverted,
|
799
|
+
)
|
800
|
+
|
801
|
+
|
802
|
+
def selectUnion(polyseg: CombinedPolySegments) -> PolySegments:
|
803
|
+
return PolySegments(
|
804
|
+
segments=__select(
|
805
|
+
# fmt:off
|
806
|
+
polyseg.combined, [
|
807
|
+
0, 2, 1, 0,
|
808
|
+
2, 2, 0, 0,
|
809
|
+
1, 0, 1, 0,
|
810
|
+
0, 0, 0, 0,
|
811
|
+
]
|
812
|
+
# fmt:on
|
813
|
+
),
|
814
|
+
isInverted=(polyseg.isInverted1 or polyseg.isInverted2),
|
815
|
+
)
|
816
|
+
|
817
|
+
|
818
|
+
def selectIntersect(polyseg: CombinedPolySegments) -> PolySegments:
|
819
|
+
return PolySegments(
|
820
|
+
segments=__select(
|
821
|
+
# fmt:off
|
822
|
+
polyseg.combined, [
|
823
|
+
0, 0, 0, 0,
|
824
|
+
0, 2, 0, 2,
|
825
|
+
0, 0, 1, 1,
|
826
|
+
0, 2, 1, 0
|
827
|
+
]
|
828
|
+
# fmt:on
|
829
|
+
),
|
830
|
+
isInverted=(polyseg.isInverted1 and polyseg.isInverted2),
|
831
|
+
)
|
832
|
+
|
833
|
+
|
834
|
+
def selectDifference(polyseg: CombinedPolySegments) -> PolySegments:
|
835
|
+
return PolySegments(
|
836
|
+
segments=__select(
|
837
|
+
# fmt:off
|
838
|
+
polyseg.combined, [
|
839
|
+
0, 0, 0, 0,
|
840
|
+
2, 0, 2, 0,
|
841
|
+
1, 1, 0, 0,
|
842
|
+
0, 1, 2, 0
|
843
|
+
]
|
844
|
+
# fmt:on
|
845
|
+
),
|
846
|
+
isInverted=(polyseg.isInverted1 and not polyseg.isInverted2),
|
847
|
+
)
|
848
|
+
|
849
|
+
|
850
|
+
def selectDifferenceRev(polyseg: CombinedPolySegments) -> PolySegments:
|
851
|
+
return PolySegments(
|
852
|
+
segments=__select(
|
853
|
+
# fmt:off
|
854
|
+
polyseg.combined, [
|
855
|
+
0, 2, 1, 0,
|
856
|
+
0, 0, 1, 1,
|
857
|
+
0, 2, 0, 2,
|
858
|
+
0, 0, 0, 0
|
859
|
+
]
|
860
|
+
# fmt:on
|
861
|
+
),
|
862
|
+
isInverted=(not polyseg.isInverted1 and polyseg.isInverted2),
|
863
|
+
)
|
864
|
+
|
865
|
+
|
866
|
+
def selectXor(polyseg: CombinedPolySegments) -> PolySegments:
|
867
|
+
return PolySegments(
|
868
|
+
segments=__select(
|
869
|
+
# fmt:off
|
870
|
+
polyseg.combined, [
|
871
|
+
0, 2, 1, 0,
|
872
|
+
2, 0, 0, 1,
|
873
|
+
1, 0, 0, 2,
|
874
|
+
0, 1, 2, 0
|
875
|
+
]
|
876
|
+
# fmt:on
|
877
|
+
),
|
878
|
+
isInverted=(polyseg.isInverted1 != polyseg.isInverted2),
|
879
|
+
)
|
880
|
+
|
881
|
+
|
882
|
+
def polygon(segments: PolySegments):
|
883
|
+
return Polygon(segmentChainer(segments.segments), segments.isInverted)
|
884
|
+
|
885
|
+
|
886
|
+
def __operate(
|
887
|
+
poly1: Polygon,
|
888
|
+
poly2: Polygon,
|
889
|
+
selector: typing.Callable[[CombinedPolySegments], PolySegments],
|
890
|
+
):
|
891
|
+
firstPolygonRegions = segments(poly1)
|
892
|
+
secondPolygonRegions = segments(poly2)
|
893
|
+
combinedSegments = combine(firstPolygonRegions, secondPolygonRegions)
|
894
|
+
seg = selector(combinedSegments)
|
895
|
+
return polygon(seg)
|
896
|
+
|
897
|
+
|
898
|
+
# helper functions for common operations
|
899
|
+
|
900
|
+
|
901
|
+
@typing.overload
|
902
|
+
def union(polygons: typing.List[Polygon]) -> Polygon:
|
903
|
+
...
|
904
|
+
|
905
|
+
|
906
|
+
@typing.overload
|
907
|
+
def union(poly1: Polygon, poly2: Polygon) -> Polygon:
|
908
|
+
...
|
909
|
+
|
910
|
+
|
911
|
+
def union(*args):
|
912
|
+
if len(args) == 1 and isinstance(args[0], list):
|
913
|
+
polygons = args[0]
|
914
|
+
seg1 = segments(polygons[0])
|
915
|
+
for i in range(1, len(polygons)):
|
916
|
+
seg2 = segments(polygons[i])
|
917
|
+
comb = combine(seg1, seg2)
|
918
|
+
seg1 = selectUnion(comb)
|
919
|
+
|
920
|
+
return polygon(seg1)
|
921
|
+
elif (
|
922
|
+
len(args) == 2 and isinstance(args[0], Polygon) and isinstance(args[1], Polygon)
|
923
|
+
):
|
924
|
+
return __operate(args[0], args[1], selectUnion)
|
925
|
+
|
926
|
+
|
927
|
+
def intersect(poly1: Polygon, poly2: Polygon):
|
928
|
+
return __operate(poly1, poly2, selectIntersect)
|
929
|
+
|
930
|
+
|
931
|
+
def difference(poly1: Polygon, poly2: Polygon):
|
932
|
+
return __operate(poly1, poly2, selectDifference)
|
933
|
+
|
934
|
+
|
935
|
+
def differenceRev(poly1: Polygon, poly2: Polygon):
|
936
|
+
return __operate(poly1, poly2, selectDifferenceRev)
|
937
|
+
|
938
|
+
|
939
|
+
def xor(poly1: Polygon, poly2: Polygon):
|
940
|
+
return __operate(poly1, poly2, selectXor)
|