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
@@ -1,955 +1,963 @@
|
|
1
|
-
"""
|
2
|
-
This adds console commands that deal with the creation of an offset
|
3
|
-
"""
|
4
|
-
from copy import copy
|
5
|
-
from math import atan2, tau
|
6
|
-
|
7
|
-
from meerk40t.core.node.node import Linejoin
|
8
|
-
from meerk40t.core.units import UNITS_PER_PIXEL, Length
|
9
|
-
from meerk40t.svgelements import (
|
10
|
-
Arc,
|
11
|
-
Close,
|
12
|
-
CubicBezier,
|
13
|
-
Line,
|
14
|
-
Move,
|
15
|
-
Path,
|
16
|
-
Point,
|
17
|
-
QuadraticBezier,
|
18
|
-
)
|
19
|
-
from meerk40t.tools.geomstr import Geomstr
|
20
|
-
|
21
|
-
"""
|
22
|
-
The following routines deal with the offset of an SVG path at a given distance D.
|
23
|
-
An offset or parallel curve can easily be established:
|
24
|
-
- for a line segment by another line parallel and in distance D:
|
25
|
-
Establish the two normals with length D on the end points and
|
26
|
-
create the two new endpoints
|
27
|
-
- for an arc segment: elongate rx and ry by D
|
28
|
-
To establish an offset for a quadratic or cubic bezier by another cubic bezier
|
29
|
-
is not possible so this requires approximation.
|
30
|
-
An acceptable approximation is proposed by Tiller and Hanson:
|
31
|
-
P1 start point
|
32
|
-
P2 end point
|
33
|
-
C1 control point 1
|
34
|
-
C2 control point 2
|
35
|
-
You create the offset version of these 3 lines and look for their intersections:
|
36
|
-
- offset to (P1 C1) -> helper 1
|
37
|
-
- offset to (C1 C2) -> helper 2
|
38
|
-
- offset to (P2 C2) -> helper 3
|
39
|
-
we establish P1-new
|
40
|
-
the intersections between helper 1 and helper 2 is our new control point C1-new
|
41
|
-
the intersections between helper 2 and helper 3 is our new control point C2-new
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
A good visual representation can be seen here:
|
46
|
-
https://feirell.github.io/offset-bezier/
|
47
|
-
|
48
|
-
The algorithm deals with the challenge as follows:
|
49
|
-
a) It walks through the subpaths of a given path so that we have a continuous curve
|
50
|
-
b) It looks at the different segment typs and deals with them,
|
51
|
-
generating a new offseted
|
52
|
-
c) Finally it stitches those segments together,
|
53
|
-
"""
|
54
|
-
|
55
|
-
|
56
|
-
def norm_vector(p1, p2, target_len):
|
57
|
-
line_vector = p2 - p1
|
58
|
-
# if line_vector.x == 0 and line_vector.y == 0:
|
59
|
-
# return Point(target_len, 0)
|
60
|
-
factor = target_len
|
61
|
-
normal_vector = Point(-1 * line_vector.y, line_vector.x)
|
62
|
-
normlen = abs(normal_vector)
|
63
|
-
if normlen != 0:
|
64
|
-
factor = target_len / normlen
|
65
|
-
normal_vector *= factor
|
66
|
-
return normal_vector
|
67
|
-
|
68
|
-
|
69
|
-
def is_clockwise(path, start=0):
|
70
|
-
def poly_clockwise(poly):
|
71
|
-
"""
|
72
|
-
returns True if the polygon is clockwise ordered, false if not
|
73
|
-
"""
|
74
|
-
|
75
|
-
total = (
|
76
|
-
poly[-1].x * poly[0].y - poly[0].x * poly[-1].y
|
77
|
-
) # last point to first point
|
78
|
-
for i in range(len(poly) - 1):
|
79
|
-
total += poly[i].x * poly[i + 1].y - poly[i + 1].x * poly[i].y
|
80
|
-
|
81
|
-
if total <= 0:
|
82
|
-
return True
|
83
|
-
else:
|
84
|
-
return False
|
85
|
-
|
86
|
-
poly = []
|
87
|
-
idx = start
|
88
|
-
while idx < len(path._segments):
|
89
|
-
seg = path._segments[idx]
|
90
|
-
if isinstance(seg, (Arc, Line, QuadraticBezier, CubicBezier)):
|
91
|
-
if len(poly) == 0:
|
92
|
-
poly.append(seg.start)
|
93
|
-
poly.append(seg.end)
|
94
|
-
else:
|
95
|
-
if len(poly) > 0:
|
96
|
-
break
|
97
|
-
idx += 1
|
98
|
-
if len(poly) == 0:
|
99
|
-
res = True
|
100
|
-
else:
|
101
|
-
res = poly_clockwise(poly)
|
102
|
-
return res
|
103
|
-
|
104
|
-
|
105
|
-
def linearize_segment(segment, interpolation=500, reduce=True):
|
106
|
-
slope_tolerance = 0.001
|
107
|
-
s = []
|
108
|
-
delta = 1.0 / interpolation
|
109
|
-
lastpt = None
|
110
|
-
t = 0
|
111
|
-
last_slope = None
|
112
|
-
while t <= 1:
|
113
|
-
appendit = True
|
114
|
-
np = segment.point(t)
|
115
|
-
if lastpt is not None:
|
116
|
-
dx = lastpt.x - np.x
|
117
|
-
dy = lastpt.y - np.y
|
118
|
-
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
|
119
|
-
appendit = False
|
120
|
-
# identical points!
|
121
|
-
else:
|
122
|
-
this_slope = atan2(dy, dx)
|
123
|
-
if last_slope is not None:
|
124
|
-
if abs(last_slope - this_slope) < slope_tolerance:
|
125
|
-
# Combine segments,
|
126
|
-
this_slope = last_slope
|
127
|
-
appendit = False
|
128
|
-
last_slope = this_slope
|
129
|
-
|
130
|
-
if appendit or not reduce:
|
131
|
-
s.append(np)
|
132
|
-
else:
|
133
|
-
s[-1] = np
|
134
|
-
t += delta
|
135
|
-
lastpt = np
|
136
|
-
if s[-1] != segment.end:
|
137
|
-
np = Point(segment.end)
|
138
|
-
s.append(np)
|
139
|
-
# print (f"linearize: {type(segment).__name__}")
|
140
|
-
# print (f"Start: ({segment.start.x:.0f}, {segment.start.y:.0f}) - ({s[0].x:.0f}, {s[0].y:.0f})")
|
141
|
-
# print (f"End: ({segment.end.x:.0f}, {segment.end.y:.0f}) - ({s[-1].x:.0f}, {s[-1].y:.0f})")
|
142
|
-
return s
|
143
|
-
|
144
|
-
|
145
|
-
def offset_point_array(points, offset):
|
146
|
-
result = list()
|
147
|
-
p0 = None
|
148
|
-
for idx, p1 in enumerate(points):
|
149
|
-
if idx > 0:
|
150
|
-
nv = norm_vector(p0, p1, offset)
|
151
|
-
result.append(p0 + nv)
|
152
|
-
result.append(p1 + nv)
|
153
|
-
p0 = Point(p1)
|
154
|
-
for idx in range(3, len(result)):
|
155
|
-
w = result[idx - 3]
|
156
|
-
z = result[idx - 2]
|
157
|
-
x = result[idx - 1]
|
158
|
-
y = result[idx]
|
159
|
-
p_i, s, t = intersect_line_segments(w, z, x, y)
|
160
|
-
if p_i is None:
|
161
|
-
continue
|
162
|
-
result[idx - 2] = Point(p_i)
|
163
|
-
result[idx - 1] = Point(p_i)
|
164
|
-
return result
|
165
|
-
|
166
|
-
|
167
|
-
def offset_arc(segment, offset=0, linearize=False, interpolation=500):
|
168
|
-
if not isinstance(segment, Arc):
|
169
|
-
return None
|
170
|
-
newsegments = list()
|
171
|
-
if linearize:
|
172
|
-
s = linearize_segment(segment, interpolation=interpolation, reduce=True)
|
173
|
-
s = offset_point_array(s, offset)
|
174
|
-
for idx in range(1, len(s)):
|
175
|
-
seg = Line(
|
176
|
-
start=Point(s[idx - 1][0], s[idx - 1][1]),
|
177
|
-
end=Point(s[idx][0], s[idx][1]),
|
178
|
-
)
|
179
|
-
newsegments.append(seg)
|
180
|
-
else:
|
181
|
-
centerpt = Point(segment.center)
|
182
|
-
startpt = centerpt.polar_to(
|
183
|
-
angle=centerpt.angle_to(segment.start),
|
184
|
-
distance=centerpt.distance_to(segment.start) + offset,
|
185
|
-
)
|
186
|
-
endpt = centerpt.polar_to(
|
187
|
-
angle=centerpt.angle_to(segment.end),
|
188
|
-
distance=centerpt.distance_to(segment.end) + offset,
|
189
|
-
)
|
190
|
-
newseg = Arc(
|
191
|
-
startpt,
|
192
|
-
endpt,
|
193
|
-
centerpt,
|
194
|
-
# ccw=ccw,
|
195
|
-
)
|
196
|
-
newsegments.append(newseg)
|
197
|
-
return newsegments
|
198
|
-
|
199
|
-
|
200
|
-
def offset_line(segment, offset=0):
|
201
|
-
if not isinstance(segment, Line):
|
202
|
-
return None
|
203
|
-
newseg = copy(segment)
|
204
|
-
normal_vector = norm_vector(segment.start, segment.end, offset)
|
205
|
-
newseg.start += normal_vector
|
206
|
-
newseg.end += normal_vector
|
207
|
-
# print (f"Old= ({segment.start.x:.0f}, {segment.start.y:.0f})-({segment.end.x:.0f}, {segment.end.y:.0f})")
|
208
|
-
# print (f"New= ({newsegment.start.x:.0f}, {newsegment.start.y:.0f})-({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
|
209
|
-
return [newseg]
|
210
|
-
|
211
|
-
|
212
|
-
def offset_quad(segment, offset=0, linearize=False, interpolation=500):
|
213
|
-
if not isinstance(segment, QuadraticBezier):
|
214
|
-
return None
|
215
|
-
cubic = CubicBezier(
|
216
|
-
start=segment.start,
|
217
|
-
control1=segment.start + 2 / 3 * (segment.control - segment.start),
|
218
|
-
control2=segment.end + 2 / 3 * (segment.control - segment.end),
|
219
|
-
end=segment.end,
|
220
|
-
)
|
221
|
-
newsegments = offset_cubic(cubic, offset, linearize, interpolation)
|
222
|
-
|
223
|
-
return newsegments
|
224
|
-
|
225
|
-
|
226
|
-
def offset_cubic(segment, offset=0, linearize=False, interpolation=500):
|
227
|
-
"""
|
228
|
-
To establish an offset for a quadratic or cubic bezier by another cubic bezier
|
229
|
-
is not possible so this requires approximation.
|
230
|
-
An acceptable approximation is proposed by Tiller and Hanson:
|
231
|
-
P1 start point
|
232
|
-
P2 end point
|
233
|
-
C1 control point 1
|
234
|
-
C2 control point 2
|
235
|
-
You create the offset version of these 3 lines and look for their intersections:
|
236
|
-
- offset to (P1 C1) -> helper 1
|
237
|
-
- offset to (C1 C2) -> helper 2
|
238
|
-
- offset to (P2 C2) -> helper 3
|
239
|
-
we establish P1-new
|
240
|
-
the intersections between helper 1 and helper 2 is our new control point C1-new
|
241
|
-
the intersections between helper 2 and helper 3 is our new control point C2-new
|
242
|
-
|
243
|
-
Beware, this has limitations! It's not dealing well with curves that have cusps
|
244
|
-
"""
|
245
|
-
|
246
|
-
if not isinstance(segment, CubicBezier):
|
247
|
-
return None
|
248
|
-
newsegments = list()
|
249
|
-
if linearize:
|
250
|
-
s = linearize_segment(segment, interpolation=interpolation, reduce=True)
|
251
|
-
s = offset_point_array(s, offset)
|
252
|
-
for idx in range(1, len(s)):
|
253
|
-
seg = Line(
|
254
|
-
start=Point(s[idx - 1][0], s[idx - 1][1]),
|
255
|
-
end=Point(s[idx][0], s[idx][1]),
|
256
|
-
)
|
257
|
-
newsegments.append(seg)
|
258
|
-
else:
|
259
|
-
newseg = copy(segment)
|
260
|
-
if segment.control1 == segment.start:
|
261
|
-
p1 = segment.control2
|
262
|
-
else:
|
263
|
-
p1 = segment.control1
|
264
|
-
normal_vector1 = norm_vector(segment.start, p1, offset)
|
265
|
-
if segment.control2 == segment.end:
|
266
|
-
p1 = segment.control1
|
267
|
-
else:
|
268
|
-
p1 = segment.control2
|
269
|
-
normal_vector2 = norm_vector(p1, segment.end, offset)
|
270
|
-
normal_vector3 = norm_vector(segment.control1, segment.control2, offset)
|
271
|
-
|
272
|
-
newseg.start += normal_vector1
|
273
|
-
newseg.end += normal_vector2
|
274
|
-
|
275
|
-
v = segment.start + normal_vector1
|
276
|
-
w = segment.control1 + normal_vector1
|
277
|
-
x = segment.control1 + normal_vector3
|
278
|
-
y = segment.control2 + normal_vector3
|
279
|
-
intersect, s, t = intersect_line_segments(v, w, x, y)
|
280
|
-
if intersect is None:
|
281
|
-
# Fallback
|
282
|
-
intersect = segment.control1 + 0.5 * (normal_vector1 + normal_vector3)
|
283
|
-
newseg.control1 = intersect
|
284
|
-
|
285
|
-
x = segment.control2 + normal_vector2
|
286
|
-
y = segment.end + normal_vector2
|
287
|
-
v = segment.control1 + normal_vector3
|
288
|
-
w = segment.control2 + normal_vector3
|
289
|
-
intersect, s, t = intersect_line_segments(v, w, x, y)
|
290
|
-
if intersect is None:
|
291
|
-
# Fallback
|
292
|
-
intersect = segment.control2 + 0.5 * (normal_vector2 + normal_vector3)
|
293
|
-
newseg.control2 = intersect
|
294
|
-
# print (f"Old: start=({segment.start.x:.0f}, {segment.start.y:.0f}), c1=({segment.control1.x:.0f}, {segment.control1.y:.0f}), c2=({segment.control2.x:.0f}, {segment.control2.y:.0f}), end=({segment.end.x:.0f}, {segment.end.y:.0f})")
|
295
|
-
# print (f"New: start=({newsegment.start.x:.0f}, {newsegment.start.y:.0f}), c1=({newsegment.control1.x:.0f}, {newsegment.control1.y:.0f}), c2=({newsegment.control2.x:.0f}, {newsegment.control2.y:.0f}), end=({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
|
296
|
-
newsegments.append(newseg)
|
297
|
-
return newsegments
|
298
|
-
|
299
|
-
|
300
|
-
def intersect_line_segments(w, z, x, y):
|
301
|
-
"""
|
302
|
-
We establish the intersection between two lines given by
|
303
|
-
line1 = (w, z), line2 = (x, y)
|
304
|
-
We define the first line by the equation w + s * (z - w)
|
305
|
-
We define the second line by the equation x + t * (y - x)
|
306
|
-
We give back the intersection and the values for s and t
|
307
|
-
out of these two equations at the intersection point.
|
308
|
-
Notabene: if the intersection is on the two line segments
|
309
|
-
then s and t need to be between 0 and 1.
|
310
|
-
|
311
|
-
Args:
|
312
|
-
w (Point): Start point of the first line segment
|
313
|
-
z (Point): End point of the second line segment
|
314
|
-
x (Point): Start point of the first line segment
|
315
|
-
y (Point): End point of the second line segment
|
316
|
-
Returns three values: P, s, t
|
317
|
-
P: Point of intersection, None if the two lines have no intersection
|
318
|
-
S: Value for s in P = w + s * (z - w)
|
319
|
-
T: Value for t in P = x + t * (y - x)
|
320
|
-
|
321
|
-
( w1 ) ( z1 - w1 ) ( x1 ) ( y1 - x1 )
|
322
|
-
( ) + t ( ) = ( ) + s ( )
|
323
|
-
( w2 ) ( z2 - w2 ) ( y1 ) ( y2 - x2 )
|
324
|
-
|
325
|
-
( w1 - x1 ) ( y1 - x1 ) ( z1 - w1 )
|
326
|
-
( ) = s ( ) - t ( )
|
327
|
-
( w2 - x2 ) ( y2 - x2 ) ( z2 - w2 )
|
328
|
-
|
329
|
-
( w1 - x1 ) ( y1 - x1 -z1 + w1 ) ( s )
|
330
|
-
( ) = ( ) ( )
|
331
|
-
( w2 - x2 ) ( y2 - x2 -z2 + w2 ) ( t )
|
332
|
-
|
333
|
-
"""
|
334
|
-
a = y.x - x.x
|
335
|
-
b = -z.x + w.x
|
336
|
-
c = y.y - x.y
|
337
|
-
d = -z.y + w.y
|
338
|
-
"""
|
339
|
-
The inverse matrix of
|
340
|
-
(a b) 1 (d -b)
|
341
|
-
= -------- * ( )
|
342
|
-
(c d) ad - bc (-c a)
|
343
|
-
"""
|
344
|
-
deter = a * d - b * c
|
345
|
-
if abs(deter) < 1.0e-8:
|
346
|
-
# They don't have an interference
|
347
|
-
return None, None, None
|
348
|
-
|
349
|
-
s = 1 / deter * (d * (w.x - x.x) + -b * (w.y - x.y))
|
350
|
-
t = 1 / deter * (-c * (w.x - x.x) + a * (w.y - x.y))
|
351
|
-
p1 = w + t * (z - w)
|
352
|
-
p2 = x + s * (y - x)
|
353
|
-
# print (f"p1 = ({p1.x:.3f}, {p1.y:.3f})")
|
354
|
-
# print (f"p2 = ({p2.x:.3f}, {p2.y:.3f})")
|
355
|
-
p = p1
|
356
|
-
return p, s, t
|
357
|
-
|
358
|
-
|
359
|
-
def offset_path(self, path, offset_value=0):
|
360
|
-
# As this oveloading a regular method in a class
|
361
|
-
# it needs to have the very same definition (including the class
|
362
|
-
# reference self)
|
363
|
-
p = path_offset(
|
364
|
-
path,
|
365
|
-
offset_value=-offset_value,
|
366
|
-
radial_connector=True,
|
367
|
-
linearize=True,
|
368
|
-
interpolation=500,
|
369
|
-
)
|
370
|
-
if p is None:
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
)
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
elif isinstance(
|
460
|
-
needs_connector = False
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
)
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
)
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
# print(f"{perf_counter()-t_start:.3f}
|
599
|
-
|
600
|
-
# print(f"{perf_counter()-t_start:.3f}
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
)
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
#
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
#
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
#
|
646
|
-
#
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
idx =
|
656
|
-
while idx
|
657
|
-
p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
|
658
|
-
):
|
659
|
-
idx
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
p._segments[idx1]
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
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
|
-
|
764
|
-
if
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
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
|
-
p
|
819
|
-
)
|
820
|
-
if
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
def
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
type=bool,
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
"
|
877
|
-
|
878
|
-
|
879
|
-
("
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
)
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
if
|
908
|
-
|
909
|
-
else:
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
p =
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
)
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
1
|
+
"""
|
2
|
+
This adds console commands that deal with the creation of an offset
|
3
|
+
"""
|
4
|
+
from copy import copy
|
5
|
+
from math import atan2, tau
|
6
|
+
|
7
|
+
from meerk40t.core.node.node import Linejoin
|
8
|
+
from meerk40t.core.units import UNITS_PER_PIXEL, Length
|
9
|
+
from meerk40t.svgelements import (
|
10
|
+
Arc,
|
11
|
+
Close,
|
12
|
+
CubicBezier,
|
13
|
+
Line,
|
14
|
+
Move,
|
15
|
+
Path,
|
16
|
+
Point,
|
17
|
+
QuadraticBezier,
|
18
|
+
)
|
19
|
+
from meerk40t.tools.geomstr import Geomstr
|
20
|
+
|
21
|
+
"""
|
22
|
+
The following routines deal with the offset of an SVG path at a given distance D.
|
23
|
+
An offset or parallel curve can easily be established:
|
24
|
+
- for a line segment by another line parallel and in distance D:
|
25
|
+
Establish the two normals with length D on the end points and
|
26
|
+
create the two new endpoints
|
27
|
+
- for an arc segment: elongate rx and ry by D
|
28
|
+
To establish an offset for a quadratic or cubic bezier by another cubic bezier
|
29
|
+
is not possible so this requires approximation.
|
30
|
+
An acceptable approximation is proposed by Tiller and Hanson:
|
31
|
+
P1 start point
|
32
|
+
P2 end point
|
33
|
+
C1 control point 1
|
34
|
+
C2 control point 2
|
35
|
+
You create the offset version of these 3 lines and look for their intersections:
|
36
|
+
- offset to (P1 C1) -> helper 1
|
37
|
+
- offset to (C1 C2) -> helper 2
|
38
|
+
- offset to (P2 C2) -> helper 3
|
39
|
+
we establish P1-new
|
40
|
+
the intersections between helper 1 and helper 2 is our new control point C1-new
|
41
|
+
the intersections between helper 2 and helper 3 is our new control point C2-new
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
A good visual representation can be seen here:
|
46
|
+
https://feirell.github.io/offset-bezier/
|
47
|
+
|
48
|
+
The algorithm deals with the challenge as follows:
|
49
|
+
a) It walks through the subpaths of a given path so that we have a continuous curve
|
50
|
+
b) It looks at the different segment typs and deals with them,
|
51
|
+
generating a new offseted segment
|
52
|
+
c) Finally it stitches those segments together, preparing for the simplification
|
53
|
+
"""
|
54
|
+
|
55
|
+
|
56
|
+
def norm_vector(p1, p2, target_len):
|
57
|
+
line_vector = p2 - p1
|
58
|
+
# if line_vector.x == 0 and line_vector.y == 0:
|
59
|
+
# return Point(target_len, 0)
|
60
|
+
factor = target_len
|
61
|
+
normal_vector = Point(-1 * line_vector.y, line_vector.x)
|
62
|
+
normlen = abs(normal_vector)
|
63
|
+
if normlen != 0:
|
64
|
+
factor = target_len / normlen
|
65
|
+
normal_vector *= factor
|
66
|
+
return normal_vector
|
67
|
+
|
68
|
+
|
69
|
+
def is_clockwise(path, start=0):
|
70
|
+
def poly_clockwise(poly):
|
71
|
+
"""
|
72
|
+
returns True if the polygon is clockwise ordered, false if not
|
73
|
+
"""
|
74
|
+
|
75
|
+
total = (
|
76
|
+
poly[-1].x * poly[0].y - poly[0].x * poly[-1].y
|
77
|
+
) # last point to first point
|
78
|
+
for i in range(len(poly) - 1):
|
79
|
+
total += poly[i].x * poly[i + 1].y - poly[i + 1].x * poly[i].y
|
80
|
+
|
81
|
+
if total <= 0:
|
82
|
+
return True
|
83
|
+
else:
|
84
|
+
return False
|
85
|
+
|
86
|
+
poly = []
|
87
|
+
idx = start
|
88
|
+
while idx < len(path._segments):
|
89
|
+
seg = path._segments[idx]
|
90
|
+
if isinstance(seg, (Arc, Line, QuadraticBezier, CubicBezier)):
|
91
|
+
if len(poly) == 0:
|
92
|
+
poly.append(seg.start)
|
93
|
+
poly.append(seg.end)
|
94
|
+
else:
|
95
|
+
if len(poly) > 0:
|
96
|
+
break
|
97
|
+
idx += 1
|
98
|
+
if len(poly) == 0:
|
99
|
+
res = True
|
100
|
+
else:
|
101
|
+
res = poly_clockwise(poly)
|
102
|
+
return res
|
103
|
+
|
104
|
+
|
105
|
+
def linearize_segment(segment, interpolation=500, reduce=True):
|
106
|
+
slope_tolerance = 0.001
|
107
|
+
s = []
|
108
|
+
delta = 1.0 / interpolation
|
109
|
+
lastpt = None
|
110
|
+
t = 0
|
111
|
+
last_slope = None
|
112
|
+
while t <= 1:
|
113
|
+
appendit = True
|
114
|
+
np = segment.point(t)
|
115
|
+
if lastpt is not None:
|
116
|
+
dx = lastpt.x - np.x
|
117
|
+
dy = lastpt.y - np.y
|
118
|
+
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
|
119
|
+
appendit = False
|
120
|
+
# identical points!
|
121
|
+
else:
|
122
|
+
this_slope = atan2(dy, dx)
|
123
|
+
if last_slope is not None:
|
124
|
+
if abs(last_slope - this_slope) < slope_tolerance:
|
125
|
+
# Combine segments, i.e. get rid of mid point
|
126
|
+
this_slope = last_slope
|
127
|
+
appendit = False
|
128
|
+
last_slope = this_slope
|
129
|
+
|
130
|
+
if appendit or not reduce:
|
131
|
+
s.append(np)
|
132
|
+
else:
|
133
|
+
s[-1] = np
|
134
|
+
t += delta
|
135
|
+
lastpt = np
|
136
|
+
if s[-1] != segment.end:
|
137
|
+
np = Point(segment.end)
|
138
|
+
s.append(np)
|
139
|
+
# print (f"linearize: {type(segment).__name__}")
|
140
|
+
# print (f"Start: ({segment.start.x:.0f}, {segment.start.y:.0f}) - ({s[0].x:.0f}, {s[0].y:.0f})")
|
141
|
+
# print (f"End: ({segment.end.x:.0f}, {segment.end.y:.0f}) - ({s[-1].x:.0f}, {s[-1].y:.0f})")
|
142
|
+
return s
|
143
|
+
|
144
|
+
|
145
|
+
def offset_point_array(points, offset):
|
146
|
+
result = list()
|
147
|
+
p0 = None
|
148
|
+
for idx, p1 in enumerate(points):
|
149
|
+
if idx > 0:
|
150
|
+
nv = norm_vector(p0, p1, offset)
|
151
|
+
result.append(p0 + nv)
|
152
|
+
result.append(p1 + nv)
|
153
|
+
p0 = Point(p1)
|
154
|
+
for idx in range(3, len(result)):
|
155
|
+
w = result[idx - 3]
|
156
|
+
z = result[idx - 2]
|
157
|
+
x = result[idx - 1]
|
158
|
+
y = result[idx]
|
159
|
+
p_i, s, t = intersect_line_segments(w, z, x, y)
|
160
|
+
if p_i is None:
|
161
|
+
continue
|
162
|
+
result[idx - 2] = Point(p_i)
|
163
|
+
result[idx - 1] = Point(p_i)
|
164
|
+
return result
|
165
|
+
|
166
|
+
|
167
|
+
def offset_arc(segment, offset=0, linearize=False, interpolation=500):
|
168
|
+
if not isinstance(segment, Arc):
|
169
|
+
return None
|
170
|
+
newsegments = list()
|
171
|
+
if linearize:
|
172
|
+
s = linearize_segment(segment, interpolation=interpolation, reduce=True)
|
173
|
+
s = offset_point_array(s, offset)
|
174
|
+
for idx in range(1, len(s)):
|
175
|
+
seg = Line(
|
176
|
+
start=Point(s[idx - 1][0], s[idx - 1][1]),
|
177
|
+
end=Point(s[idx][0], s[idx][1]),
|
178
|
+
)
|
179
|
+
newsegments.append(seg)
|
180
|
+
else:
|
181
|
+
centerpt = Point(segment.center)
|
182
|
+
startpt = centerpt.polar_to(
|
183
|
+
angle=centerpt.angle_to(segment.start),
|
184
|
+
distance=centerpt.distance_to(segment.start) + offset,
|
185
|
+
)
|
186
|
+
endpt = centerpt.polar_to(
|
187
|
+
angle=centerpt.angle_to(segment.end),
|
188
|
+
distance=centerpt.distance_to(segment.end) + offset,
|
189
|
+
)
|
190
|
+
newseg = Arc(
|
191
|
+
startpt,
|
192
|
+
endpt,
|
193
|
+
centerpt,
|
194
|
+
# ccw=ccw,
|
195
|
+
)
|
196
|
+
newsegments.append(newseg)
|
197
|
+
return newsegments
|
198
|
+
|
199
|
+
|
200
|
+
def offset_line(segment, offset=0):
|
201
|
+
if not isinstance(segment, Line):
|
202
|
+
return None
|
203
|
+
newseg = copy(segment)
|
204
|
+
normal_vector = norm_vector(segment.start, segment.end, offset)
|
205
|
+
newseg.start += normal_vector
|
206
|
+
newseg.end += normal_vector
|
207
|
+
# print (f"Old= ({segment.start.x:.0f}, {segment.start.y:.0f})-({segment.end.x:.0f}, {segment.end.y:.0f})")
|
208
|
+
# print (f"New= ({newsegment.start.x:.0f}, {newsegment.start.y:.0f})-({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
|
209
|
+
return [newseg]
|
210
|
+
|
211
|
+
|
212
|
+
def offset_quad(segment, offset=0, linearize=False, interpolation=500):
|
213
|
+
if not isinstance(segment, QuadraticBezier):
|
214
|
+
return None
|
215
|
+
cubic = CubicBezier(
|
216
|
+
start=segment.start,
|
217
|
+
control1=segment.start + 2 / 3 * (segment.control - segment.start),
|
218
|
+
control2=segment.end + 2 / 3 * (segment.control - segment.end),
|
219
|
+
end=segment.end,
|
220
|
+
)
|
221
|
+
newsegments = offset_cubic(cubic, offset, linearize, interpolation)
|
222
|
+
|
223
|
+
return newsegments
|
224
|
+
|
225
|
+
|
226
|
+
def offset_cubic(segment, offset=0, linearize=False, interpolation=500):
|
227
|
+
"""
|
228
|
+
To establish an offset for a quadratic or cubic bezier by another cubic bezier
|
229
|
+
is not possible so this requires approximation.
|
230
|
+
An acceptable approximation is proposed by Tiller and Hanson:
|
231
|
+
P1 start point
|
232
|
+
P2 end point
|
233
|
+
C1 control point 1
|
234
|
+
C2 control point 2
|
235
|
+
You create the offset version of these 3 lines and look for their intersections:
|
236
|
+
- offset to (P1 C1) -> helper 1
|
237
|
+
- offset to (C1 C2) -> helper 2
|
238
|
+
- offset to (P2 C2) -> helper 3
|
239
|
+
we establish P1-new
|
240
|
+
the intersections between helper 1 and helper 2 is our new control point C1-new
|
241
|
+
the intersections between helper 2 and helper 3 is our new control point C2-new
|
242
|
+
|
243
|
+
Beware, this has limitations! It's not dealing well with curves that have cusps
|
244
|
+
"""
|
245
|
+
|
246
|
+
if not isinstance(segment, CubicBezier):
|
247
|
+
return None
|
248
|
+
newsegments = list()
|
249
|
+
if linearize:
|
250
|
+
s = linearize_segment(segment, interpolation=interpolation, reduce=True)
|
251
|
+
s = offset_point_array(s, offset)
|
252
|
+
for idx in range(1, len(s)):
|
253
|
+
seg = Line(
|
254
|
+
start=Point(s[idx - 1][0], s[idx - 1][1]),
|
255
|
+
end=Point(s[idx][0], s[idx][1]),
|
256
|
+
)
|
257
|
+
newsegments.append(seg)
|
258
|
+
else:
|
259
|
+
newseg = copy(segment)
|
260
|
+
if segment.control1 == segment.start:
|
261
|
+
p1 = segment.control2
|
262
|
+
else:
|
263
|
+
p1 = segment.control1
|
264
|
+
normal_vector1 = norm_vector(segment.start, p1, offset)
|
265
|
+
if segment.control2 == segment.end:
|
266
|
+
p1 = segment.control1
|
267
|
+
else:
|
268
|
+
p1 = segment.control2
|
269
|
+
normal_vector2 = norm_vector(p1, segment.end, offset)
|
270
|
+
normal_vector3 = norm_vector(segment.control1, segment.control2, offset)
|
271
|
+
|
272
|
+
newseg.start += normal_vector1
|
273
|
+
newseg.end += normal_vector2
|
274
|
+
|
275
|
+
v = segment.start + normal_vector1
|
276
|
+
w = segment.control1 + normal_vector1
|
277
|
+
x = segment.control1 + normal_vector3
|
278
|
+
y = segment.control2 + normal_vector3
|
279
|
+
intersect, s, t = intersect_line_segments(v, w, x, y)
|
280
|
+
if intersect is None:
|
281
|
+
# Fallback
|
282
|
+
intersect = segment.control1 + 0.5 * (normal_vector1 + normal_vector3)
|
283
|
+
newseg.control1 = intersect
|
284
|
+
|
285
|
+
x = segment.control2 + normal_vector2
|
286
|
+
y = segment.end + normal_vector2
|
287
|
+
v = segment.control1 + normal_vector3
|
288
|
+
w = segment.control2 + normal_vector3
|
289
|
+
intersect, s, t = intersect_line_segments(v, w, x, y)
|
290
|
+
if intersect is None:
|
291
|
+
# Fallback
|
292
|
+
intersect = segment.control2 + 0.5 * (normal_vector2 + normal_vector3)
|
293
|
+
newseg.control2 = intersect
|
294
|
+
# print (f"Old: start=({segment.start.x:.0f}, {segment.start.y:.0f}), c1=({segment.control1.x:.0f}, {segment.control1.y:.0f}), c2=({segment.control2.x:.0f}, {segment.control2.y:.0f}), end=({segment.end.x:.0f}, {segment.end.y:.0f})")
|
295
|
+
# print (f"New: start=({newsegment.start.x:.0f}, {newsegment.start.y:.0f}), c1=({newsegment.control1.x:.0f}, {newsegment.control1.y:.0f}), c2=({newsegment.control2.x:.0f}, {newsegment.control2.y:.0f}), end=({newsegment.end.x:.0f}, {newsegment.end.y:.0f})")
|
296
|
+
newsegments.append(newseg)
|
297
|
+
return newsegments
|
298
|
+
|
299
|
+
|
300
|
+
def intersect_line_segments(w, z, x, y):
|
301
|
+
"""
|
302
|
+
We establish the intersection between two lines given by
|
303
|
+
line1 = (w, z), line2 = (x, y)
|
304
|
+
We define the first line by the equation w + s * (z - w)
|
305
|
+
We define the second line by the equation x + t * (y - x)
|
306
|
+
We give back the intersection and the values for s and t
|
307
|
+
out of these two equations at the intersection point.
|
308
|
+
Notabene: if the intersection is on the two line segments
|
309
|
+
then s and t need to be between 0 and 1.
|
310
|
+
|
311
|
+
Args:
|
312
|
+
w (Point): Start point of the first line segment
|
313
|
+
z (Point): End point of the second line segment
|
314
|
+
x (Point): Start point of the first line segment
|
315
|
+
y (Point): End point of the second line segment
|
316
|
+
Returns three values: P, s, t
|
317
|
+
P: Point of intersection, None if the two lines have no intersection
|
318
|
+
S: Value for s in P = w + s * (z - w)
|
319
|
+
T: Value for t in P = x + t * (y - x)
|
320
|
+
|
321
|
+
( w1 ) ( z1 - w1 ) ( x1 ) ( y1 - x1 )
|
322
|
+
( ) + t ( ) = ( ) + s ( )
|
323
|
+
( w2 ) ( z2 - w2 ) ( y1 ) ( y2 - x2 )
|
324
|
+
|
325
|
+
( w1 - x1 ) ( y1 - x1 ) ( z1 - w1 )
|
326
|
+
( ) = s ( ) - t ( )
|
327
|
+
( w2 - x2 ) ( y2 - x2 ) ( z2 - w2 )
|
328
|
+
|
329
|
+
( w1 - x1 ) ( y1 - x1 -z1 + w1 ) ( s )
|
330
|
+
( ) = ( ) ( )
|
331
|
+
( w2 - x2 ) ( y2 - x2 -z2 + w2 ) ( t )
|
332
|
+
|
333
|
+
"""
|
334
|
+
a = y.x - x.x
|
335
|
+
b = -z.x + w.x
|
336
|
+
c = y.y - x.y
|
337
|
+
d = -z.y + w.y
|
338
|
+
"""
|
339
|
+
The inverse matrix of
|
340
|
+
(a b) 1 (d -b)
|
341
|
+
= -------- * ( )
|
342
|
+
(c d) ad - bc (-c a)
|
343
|
+
"""
|
344
|
+
deter = a * d - b * c
|
345
|
+
if abs(deter) < 1.0e-8:
|
346
|
+
# They don't have an interference
|
347
|
+
return None, None, None
|
348
|
+
|
349
|
+
s = 1 / deter * (d * (w.x - x.x) + -b * (w.y - x.y))
|
350
|
+
t = 1 / deter * (-c * (w.x - x.x) + a * (w.y - x.y))
|
351
|
+
p1 = w + t * (z - w)
|
352
|
+
# p2 = x + s * (y - x)
|
353
|
+
# print (f"p1 = ({p1.x:.3f}, {p1.y:.3f})")
|
354
|
+
# print (f"p2 = ({p2.x:.3f}, {p2.y:.3f})")
|
355
|
+
p = p1
|
356
|
+
return p, s, t
|
357
|
+
|
358
|
+
|
359
|
+
def offset_path(self, path, offset_value=0):
|
360
|
+
# As this oveloading a regular method in a class
|
361
|
+
# it needs to have the very same definition (including the class
|
362
|
+
# reference self)
|
363
|
+
p = path_offset(
|
364
|
+
path,
|
365
|
+
offset_value=-offset_value,
|
366
|
+
radial_connector=True,
|
367
|
+
linearize=True,
|
368
|
+
interpolation=500,
|
369
|
+
)
|
370
|
+
if p is None:
|
371
|
+
return path
|
372
|
+
g = Geomstr.svg(p)
|
373
|
+
if g.index:
|
374
|
+
# We are already at device resolution, so we need to reduce tolerance a lot
|
375
|
+
# Standard is 25 tats, so about 1/3 of a mil
|
376
|
+
p = g.simplify(tolerance=0.1).as_path()
|
377
|
+
p.stroke = path.stroke
|
378
|
+
p.fill = path.fill
|
379
|
+
return p
|
380
|
+
|
381
|
+
|
382
|
+
def path_offset(
|
383
|
+
path, offset_value=0, radial_connector=False, linearize=True, interpolation=500
|
384
|
+
):
|
385
|
+
MINIMAL_LEN = 5
|
386
|
+
|
387
|
+
def stitch_segments_at_index(
|
388
|
+
offset, stitchpath, seg1_end, orgintersect, radial=False, closed=False
|
389
|
+
):
|
390
|
+
point_added = 0
|
391
|
+
left_end = seg1_end
|
392
|
+
lp = len(stitchpath)
|
393
|
+
right_start = left_end + 1
|
394
|
+
if right_start >= lp:
|
395
|
+
if not closed:
|
396
|
+
return point_added
|
397
|
+
# Look for the first segment
|
398
|
+
right_start = right_start % lp
|
399
|
+
while not isinstance(
|
400
|
+
stitchpath._segments[right_start],
|
401
|
+
(Arc, Line, QuadraticBezier, CubicBezier),
|
402
|
+
):
|
403
|
+
right_start += 1
|
404
|
+
seg1 = stitchpath._segments[left_end]
|
405
|
+
seg2 = stitchpath._segments[right_start]
|
406
|
+
|
407
|
+
# print (f"Stitch {left_end}: {type(seg1).__name__}, {right_start}: {type(seg2).__name__} - max={len(stitchpath._segments)}")
|
408
|
+
if isinstance(seg1, Close):
|
409
|
+
# Close will be dealt with differently...
|
410
|
+
return point_added
|
411
|
+
if isinstance(seg1, Move):
|
412
|
+
seg1.end = Point(seg2.start)
|
413
|
+
return point_added
|
414
|
+
|
415
|
+
if isinstance(seg1, Line):
|
416
|
+
needs_connector = True
|
417
|
+
if isinstance(seg2, Line):
|
418
|
+
p, s, t = intersect_line_segments(
|
419
|
+
Point(seg1.start),
|
420
|
+
Point(seg1.end),
|
421
|
+
Point(seg2.start),
|
422
|
+
Point(seg2.end),
|
423
|
+
)
|
424
|
+
if p is not None:
|
425
|
+
# We have an intersection
|
426
|
+
if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
|
427
|
+
# We shorten the segments accordingly.
|
428
|
+
seg1.end = Point(p)
|
429
|
+
seg2.start = Point(p)
|
430
|
+
if right_start > 0 and isinstance(
|
431
|
+
stitchpath._segments[right_start - 1], Move
|
432
|
+
):
|
433
|
+
stitchpath._segments[right_start - 1].end = Point(p)
|
434
|
+
needs_connector = False
|
435
|
+
# print ("Used internal intersect")
|
436
|
+
elif not radial:
|
437
|
+
# is the intersection too far away for our purposes?
|
438
|
+
odist = orgintersect.distance_to(p)
|
439
|
+
if odist > abs(offset):
|
440
|
+
angle = orgintersect.angle_to(p)
|
441
|
+
p = orgintersect.polar_to(angle, abs(offset))
|
442
|
+
|
443
|
+
newseg1 = Line(seg1.end, p)
|
444
|
+
newseg2 = Line(p, seg2.start)
|
445
|
+
stitchpath._segments.insert(left_end + 1, newseg2)
|
446
|
+
stitchpath._segments.insert(left_end + 1, newseg1)
|
447
|
+
point_added = 2
|
448
|
+
needs_connector = False
|
449
|
+
# print ("Used shortened external intersect")
|
450
|
+
else:
|
451
|
+
seg1.end = Point(p)
|
452
|
+
seg2.start = Point(p)
|
453
|
+
if right_start > 0 and isinstance(
|
454
|
+
stitchpath._segments[right_start - 1], Move
|
455
|
+
):
|
456
|
+
stitchpath._segments[right_start - 1].end = Point(p)
|
457
|
+
needs_connector = False
|
458
|
+
# print ("Used external intersect")
|
459
|
+
elif isinstance(seg1, Move):
|
460
|
+
needs_connector = False
|
461
|
+
else: # Arc, Quad and Cubic Bezier
|
462
|
+
needs_connector = True
|
463
|
+
if isinstance(seg2, Line):
|
464
|
+
needs_connector = True
|
465
|
+
elif isinstance(seg2, Move):
|
466
|
+
needs_connector = False
|
467
|
+
|
468
|
+
if needs_connector and seg1.end != seg2.start:
|
469
|
+
"""
|
470
|
+
There is a fundamental challenge to this naiive implementation:
|
471
|
+
if the offset gets bigger you will get intersections of previous segments
|
472
|
+
which will effectively defeat it. You will end up with connection lines
|
473
|
+
reaching back creating a loop. Right now there's no real good way
|
474
|
+
to deal with it:
|
475
|
+
a) if it would be just the effort to create an offset of your path you
|
476
|
+
can apply an intersection algorithm like Bentley-Ottman to identify
|
477
|
+
intersections and remove them (or even simpler just use the
|
478
|
+
Point.convex_hull method in svgelements).
|
479
|
+
*BUT*
|
480
|
+
b) this might defeat the initial purpose of the routine to get some kerf
|
481
|
+
compensation. So you are effectively eliminating cutlines from your design
|
482
|
+
which may not be what you want.
|
483
|
+
|
484
|
+
So we try to avoid that by just looking at two consecutive path segments
|
485
|
+
as these were by definition continuous.
|
486
|
+
"""
|
487
|
+
|
488
|
+
if radial:
|
489
|
+
# print ("Inserted an arc")
|
490
|
+
# Let's check whether the distance of these points is smaller
|
491
|
+
# than the radius
|
492
|
+
|
493
|
+
angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(seg2.start)
|
494
|
+
while angle < 0:
|
495
|
+
angle += tau
|
496
|
+
while angle > tau:
|
497
|
+
angle -= tau
|
498
|
+
# print (f"Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
|
499
|
+
startpt = Point(seg1.end)
|
500
|
+
endpt = Point(seg2.start)
|
501
|
+
|
502
|
+
if angle >= tau / 2:
|
503
|
+
ccw = True
|
504
|
+
else:
|
505
|
+
ccw = False
|
506
|
+
# print ("Generate connect-arc")
|
507
|
+
connect_seg = Arc(
|
508
|
+
start=startpt, end=endpt, center=Point(orgintersect), ccw=ccw
|
509
|
+
)
|
510
|
+
clen = connect_seg.length(error=1e-2)
|
511
|
+
# print (f"Ratio: {clen / abs(tau * offset):.2f}")
|
512
|
+
if clen > abs(tau * offset / 2):
|
513
|
+
# That seems strange...
|
514
|
+
connect_seg = Line(startpt, endpt)
|
515
|
+
else:
|
516
|
+
# print ("Inserted a Line")
|
517
|
+
connect_seg = Line(Point(seg1.end), Point(seg2.start))
|
518
|
+
stitchpath._segments.insert(left_end + 1, connect_seg)
|
519
|
+
point_added = 1
|
520
|
+
elif needs_connector:
|
521
|
+
# print ("Need connector but end points were identical")
|
522
|
+
pass
|
523
|
+
else:
|
524
|
+
# print ("No connector needed")
|
525
|
+
pass
|
526
|
+
return point_added
|
527
|
+
|
528
|
+
def close_subpath(radial, sub_path, firstidx, lastidx, offset, orgintersect):
|
529
|
+
# from time import perf_counter
|
530
|
+
seg1 = None
|
531
|
+
seg2 = None
|
532
|
+
very_first = None
|
533
|
+
very_last = None
|
534
|
+
# t_start = perf_counter()
|
535
|
+
idx = firstidx
|
536
|
+
while idx < len(sub_path._segments) and very_first is None:
|
537
|
+
seg = sub_path._segments[idx]
|
538
|
+
if seg.start is not None:
|
539
|
+
very_first = Point(seg.start)
|
540
|
+
seg1 = seg
|
541
|
+
break
|
542
|
+
idx += 1
|
543
|
+
idx = lastidx
|
544
|
+
while idx >= 0 and very_last is None:
|
545
|
+
seg = sub_path._segments[idx]
|
546
|
+
if seg.end is not None:
|
547
|
+
seg2 = seg
|
548
|
+
very_last = Point(seg.end)
|
549
|
+
break
|
550
|
+
idx -= 1
|
551
|
+
if very_first is None or very_last is None:
|
552
|
+
return
|
553
|
+
# print (f"{perf_counter()-t_start:.3f} Found first and last")
|
554
|
+
seglen = very_first.distance_to(very_last)
|
555
|
+
if seglen > MINIMAL_LEN:
|
556
|
+
p, s, t = intersect_line_segments(
|
557
|
+
Point(seg1.start),
|
558
|
+
Point(seg1.end),
|
559
|
+
Point(seg2.start),
|
560
|
+
Point(seg2.end),
|
561
|
+
)
|
562
|
+
if p is not None:
|
563
|
+
# We have an intersection and shorten the segments accordingly.
|
564
|
+
d = orgintersect.distance_to(p)
|
565
|
+
if 0 <= abs(s) <= 1 and 0 <= abs(t) <= 1:
|
566
|
+
seg1.start = Point(p)
|
567
|
+
seg2.end = Point(p)
|
568
|
+
# print (f"{perf_counter()-t_start:.3f} Close subpath by adjusting inner lines, d={d:.2f} vs. offs={offset:.2f}")
|
569
|
+
elif d >= abs(offset):
|
570
|
+
if radial:
|
571
|
+
# print (f"{perf_counter()-t_start:.3f} Insert an arc")
|
572
|
+
# Let's check whether the distance of these points is smaller
|
573
|
+
# than the radius
|
574
|
+
|
575
|
+
angle = seg1.end.angle_to(seg1.start) - seg1.end.angle_to(
|
576
|
+
seg2.start
|
577
|
+
)
|
578
|
+
while angle < 0:
|
579
|
+
angle += tau
|
580
|
+
while angle > tau:
|
581
|
+
angle -= tau
|
582
|
+
# print (f"{perf_counter()-t_start:.3f} Angle: {angle:.2f} ({angle / tau * 360.0:.1f})")
|
583
|
+
startpt = Point(seg2.end)
|
584
|
+
endpt = Point(seg1.start)
|
585
|
+
|
586
|
+
if angle >= tau / 2:
|
587
|
+
ccw = True
|
588
|
+
else:
|
589
|
+
ccw = False
|
590
|
+
# print (f"{perf_counter()-t_start:.3f} Generate connect-arc")
|
591
|
+
# print (f"{perf_counter()-t_start:.3f} s={startpt}, e={endpt}, c={orgintersect}, ccw={ccw}")
|
592
|
+
segment = Arc(
|
593
|
+
start=startpt,
|
594
|
+
end=endpt,
|
595
|
+
center=Point(orgintersect),
|
596
|
+
ccw=ccw,
|
597
|
+
)
|
598
|
+
# print (f"{perf_counter()-t_start:.3f} Now calculating length")
|
599
|
+
clen = segment.length(error=1e-2)
|
600
|
+
# print (f"{perf_counter()-t_start:.3f} Ratio: {clen / abs(tau * offset):.2f}")
|
601
|
+
if clen > abs(tau * offset / 2):
|
602
|
+
# That seems strange...
|
603
|
+
segment = Line(startpt, endpt)
|
604
|
+
# print(f"{perf_counter()-t_start:.3f} Inserting segment at {lastidx + 1}...")
|
605
|
+
sub_path._segments.insert(lastidx + 1, segment)
|
606
|
+
# print(f"{perf_counter()-t_start:.3f} Done.")
|
607
|
+
|
608
|
+
else:
|
609
|
+
p = orgintersect.polar_to(
|
610
|
+
angle=orgintersect.angle_to(p),
|
611
|
+
distance=abs(offset),
|
612
|
+
)
|
613
|
+
segment = Line(p, seg1.start)
|
614
|
+
sub_path._segments.insert(lastidx + 1, segment)
|
615
|
+
segment = Line(seg2.end, p)
|
616
|
+
sub_path._segments.insert(lastidx + 1, segment)
|
617
|
+
# sub_path._segments.insert(firstidx, segment)
|
618
|
+
# print (f"Close subpath with interim pt, d={d:.2f} vs. offs={offset:.2f}")
|
619
|
+
else:
|
620
|
+
seg1.start = Point(p)
|
621
|
+
seg2.end = Point(p)
|
622
|
+
# print (f"Close subpath by adjusting lines, d={d:.2f} vs. offs={offset:.2f}")
|
623
|
+
else:
|
624
|
+
segment = Line(very_last, very_first)
|
625
|
+
sub_path._segments.insert(lastidx + 1, segment)
|
626
|
+
# print ("Fallback case, just create line")
|
627
|
+
|
628
|
+
# def dis(pt):
|
629
|
+
# if pt is None:
|
630
|
+
# return "None"
|
631
|
+
# else:
|
632
|
+
# return f"({pt.x:.0f}, {pt.y:.0f})"
|
633
|
+
|
634
|
+
results = []
|
635
|
+
# This needs to be a continuous path
|
636
|
+
spct = 0
|
637
|
+
for subpath in path.as_subpaths():
|
638
|
+
spct += 1
|
639
|
+
# print (f"Subpath {spct}")
|
640
|
+
p = Path(subpath)
|
641
|
+
if not linearize:
|
642
|
+
# p.approximate_arcs_with_cubics()
|
643
|
+
pass
|
644
|
+
offset = offset_value
|
645
|
+
# # No offset bigger than half the path size, otherwise stuff will get crazy
|
646
|
+
# if offset > 0:
|
647
|
+
# bb = p.bbox()
|
648
|
+
# offset = min(offset, bb[2] - bb[0])
|
649
|
+
# offset = min(offset, bb[3] - bb[1])
|
650
|
+
is_closed = False
|
651
|
+
# Let's check the first and last valid point. If they are identical
|
652
|
+
# we consider this to be a closed path even if it has no closed indicator.
|
653
|
+
# firstp_start = None
|
654
|
+
# lastp = None
|
655
|
+
idx = 0
|
656
|
+
while (idx < len(p)) and not isinstance(
|
657
|
+
p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
|
658
|
+
):
|
659
|
+
idx += 1
|
660
|
+
firstp_start = Point(p._segments[idx].start)
|
661
|
+
idx = len(p._segments) - 1
|
662
|
+
while idx >= 0 and not isinstance(
|
663
|
+
p._segments[idx], (Arc, Line, QuadraticBezier, CubicBezier)
|
664
|
+
):
|
665
|
+
idx -= 1
|
666
|
+
lastp = Point(p._segments[idx].end)
|
667
|
+
if firstp_start.distance_to(lastp) < 1e-3:
|
668
|
+
is_closed = True
|
669
|
+
# print ("Seems to be closed!")
|
670
|
+
# We need to establish if this is a closed path and if the first segment goes counterclockwise
|
671
|
+
cw = False
|
672
|
+
if not is_closed:
|
673
|
+
for idx in range(len(p._segments) - 1, -1, -1):
|
674
|
+
if isinstance(p._segments[idx], Close):
|
675
|
+
is_closed = True
|
676
|
+
break
|
677
|
+
if is_closed:
|
678
|
+
cw = is_clockwise(p, 0)
|
679
|
+
if cw:
|
680
|
+
offset = -1 * offset_value
|
681
|
+
# print (f"Subpath: closed={is_closed}, clockwise={cw}")
|
682
|
+
# Remember the complete subshape (could be multiple segements due to linearization)
|
683
|
+
last_point = None
|
684
|
+
first_point = None
|
685
|
+
is_closed = False
|
686
|
+
helper1 = None
|
687
|
+
helper2 = None
|
688
|
+
for idx in range(len(p._segments) - 1, -1, -1):
|
689
|
+
segment = p._segments[idx]
|
690
|
+
# print (f"Deal with seg {idx}: {type(segment).__name__} - {first_point}, {last_point}, {is_closed}")
|
691
|
+
if isinstance(segment, Close):
|
692
|
+
# Let's add a line and replace the closed segment by this new segment
|
693
|
+
# Look for the last two valid segments
|
694
|
+
last_point = None
|
695
|
+
first_point = None
|
696
|
+
pt_last = None
|
697
|
+
pt_first = None
|
698
|
+
idx1 = idx - 1
|
699
|
+
while idx1 >= 0:
|
700
|
+
if isinstance(
|
701
|
+
p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
|
702
|
+
):
|
703
|
+
pt_last = Point(p._segments[idx1].end)
|
704
|
+
break
|
705
|
+
idx1 -= 1
|
706
|
+
idx1 -= 1
|
707
|
+
while idx1 >= 0:
|
708
|
+
if isinstance(
|
709
|
+
p._segments[idx1], (Arc, Line, QuadraticBezier, CubicBezier)
|
710
|
+
):
|
711
|
+
pt_first = Point(p._segments[idx1].start)
|
712
|
+
else:
|
713
|
+
break
|
714
|
+
idx1 -= 1
|
715
|
+
if pt_last is not None and pt_first is not None:
|
716
|
+
segment = Line(pt_last, pt_first)
|
717
|
+
p._segments[idx] = segment
|
718
|
+
last_point = idx
|
719
|
+
is_closed = True
|
720
|
+
cw = is_clockwise(p, max(0, idx1))
|
721
|
+
if cw:
|
722
|
+
offset = -1 * offset_value
|
723
|
+
else:
|
724
|
+
# Invalid close?! Remove it
|
725
|
+
p._segments.pop(idx)
|
726
|
+
if last_point is not None:
|
727
|
+
last_point -= 1
|
728
|
+
continue
|
729
|
+
elif isinstance(segment, Move):
|
730
|
+
if last_point is not None and first_point is not None and is_closed:
|
731
|
+
seglen = p._segments[first_point].start.distance_to(
|
732
|
+
p._segments[last_point].end
|
733
|
+
)
|
734
|
+
if seglen > MINIMAL_LEN:
|
735
|
+
close_subpath(
|
736
|
+
radial_connector,
|
737
|
+
p,
|
738
|
+
first_point,
|
739
|
+
last_point,
|
740
|
+
offset,
|
741
|
+
helper2,
|
742
|
+
)
|
743
|
+
last_point = None
|
744
|
+
first_point = None
|
745
|
+
if segment.start is not None and segment.end is not None:
|
746
|
+
seglen = segment.start.distance_to(segment.end)
|
747
|
+
if seglen < MINIMAL_LEN:
|
748
|
+
# print (f"Skipped: {seglen}")
|
749
|
+
p._segments.pop(idx)
|
750
|
+
if last_point is not None:
|
751
|
+
last_point -= 1
|
752
|
+
continue
|
753
|
+
first_point = idx
|
754
|
+
if last_point is None:
|
755
|
+
last_point = idx
|
756
|
+
is_closed = False
|
757
|
+
offset = offset_value
|
758
|
+
# We need to establish if this is a closed path and if it goes counterclockwise
|
759
|
+
# Let establish the range and check whether this is closed
|
760
|
+
idx1 = last_point - 1
|
761
|
+
fpt = None
|
762
|
+
while idx1 >= 0:
|
763
|
+
seg = p._segments[idx1]
|
764
|
+
if isinstance(seg, (Line, Arc, QuadraticBezier, CubicBezier)):
|
765
|
+
fpt = seg.start
|
766
|
+
idx1 -= 1
|
767
|
+
if fpt is not None and segment.end.distance_to(fpt) < MINIMAL_LEN:
|
768
|
+
is_closed = True
|
769
|
+
cw = is_clockwise(p, max(0, idx1))
|
770
|
+
if cw:
|
771
|
+
offset = -1 * offset_value
|
772
|
+
# print ("Seems to be closed!")
|
773
|
+
# print (f"Regular point: {idx}, {type(segment).__name__}, {first_point}, {last_point}, {is_closed}")
|
774
|
+
helper1 = Point(p._segments[idx].end)
|
775
|
+
helper2 = Point(p._segments[idx].start)
|
776
|
+
left_end = idx
|
777
|
+
# print (f"Segment to deal with: {type(segment).__name__}")
|
778
|
+
if isinstance(segment, Arc):
|
779
|
+
arclinearize = linearize
|
780
|
+
# Arc is not working, so we always linearize
|
781
|
+
arclinearize = True
|
782
|
+
newsegment = offset_arc(segment, offset, arclinearize, interpolation)
|
783
|
+
if newsegment is None or len(newsegment) == 0:
|
784
|
+
continue
|
785
|
+
left_end = idx - 1 + len(newsegment)
|
786
|
+
last_point += len(newsegment) - 1
|
787
|
+
p._segments[idx] = newsegment[0]
|
788
|
+
for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
|
789
|
+
p._segments.insert(idx + 1, newsegment[nidx])
|
790
|
+
elif isinstance(segment, QuadraticBezier):
|
791
|
+
newsegment = offset_quad(segment, offset, linearize, interpolation)
|
792
|
+
if newsegment is None or len(newsegment) == 0:
|
793
|
+
continue
|
794
|
+
left_end = idx - 1 + len(newsegment)
|
795
|
+
last_point += len(newsegment) - 1
|
796
|
+
p._segments[idx] = newsegment[0]
|
797
|
+
for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
|
798
|
+
p._segments.insert(idx + 1, newsegment[nidx])
|
799
|
+
elif isinstance(segment, CubicBezier):
|
800
|
+
newsegment = offset_cubic(segment, offset, linearize, interpolation)
|
801
|
+
if newsegment is None or len(newsegment) == 0:
|
802
|
+
continue
|
803
|
+
left_end = idx - 1 + len(newsegment)
|
804
|
+
last_point += len(newsegment) - 1
|
805
|
+
p._segments[idx] = newsegment[0]
|
806
|
+
for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
|
807
|
+
p._segments.insert(idx + 1, newsegment[nidx])
|
808
|
+
elif isinstance(segment, Line):
|
809
|
+
newsegment = offset_line(segment, offset)
|
810
|
+
if newsegment is None or len(newsegment) == 0:
|
811
|
+
continue
|
812
|
+
left_end = idx - 1 + len(newsegment)
|
813
|
+
last_point += len(newsegment) - 1
|
814
|
+
p._segments[idx] = newsegment[0]
|
815
|
+
for nidx in range(len(newsegment) - 1, 0, -1): # All but the first
|
816
|
+
p._segments.insert(idx + 1, newsegment[nidx])
|
817
|
+
stitched = stitch_segments_at_index(
|
818
|
+
offset, p, left_end, helper1, radial=radial_connector
|
819
|
+
)
|
820
|
+
if last_point is not None:
|
821
|
+
last_point += stitched
|
822
|
+
if last_point is not None and first_point is not None and is_closed:
|
823
|
+
seglen = p._segments[first_point].start.distance_to(
|
824
|
+
p._segments[last_point].end
|
825
|
+
)
|
826
|
+
if seglen > MINIMAL_LEN:
|
827
|
+
close_subpath(
|
828
|
+
radial_connector, p, first_point, last_point, offset, helper2
|
829
|
+
)
|
830
|
+
|
831
|
+
results.append(p)
|
832
|
+
|
833
|
+
if len(results) == 0:
|
834
|
+
# Strange, should never happen
|
835
|
+
return path
|
836
|
+
result = results[0]
|
837
|
+
for idx in range(1, len(results)):
|
838
|
+
result += results[idx]
|
839
|
+
# result.approximate_arcs_with_cubics()
|
840
|
+
return result
|
841
|
+
|
842
|
+
|
843
|
+
def plugin(kernel, lifecycle=None):
|
844
|
+
_ = kernel.translation
|
845
|
+
if lifecycle == "postboot":
|
846
|
+
init_commands(kernel)
|
847
|
+
|
848
|
+
|
849
|
+
def init_commands(kernel):
|
850
|
+
self = kernel.elements
|
851
|
+
|
852
|
+
_ = kernel.translation
|
853
|
+
|
854
|
+
classify_new = self.post_classify
|
855
|
+
# We are patching the class responsible for Cut nodes in general,
|
856
|
+
# so that any new instance of this class will be able to use the
|
857
|
+
# new functionality.
|
858
|
+
# Notabene: this may be overloaded by another routine (like from pyclipr)
|
859
|
+
# at a later time.
|
860
|
+
from meerk40t.core.node.op_cut import CutOpNode
|
861
|
+
|
862
|
+
CutOpNode.offset_routine = offset_path
|
863
|
+
|
864
|
+
@self.console_argument(
|
865
|
+
"offset",
|
866
|
+
type=str,
|
867
|
+
help=_(
|
868
|
+
"offset to line mm (positive values to left/outside, negative values to right/inside)"
|
869
|
+
),
|
870
|
+
)
|
871
|
+
@self.console_option(
|
872
|
+
"radial", "r", action="store_true", type=bool, help=_("radial connector")
|
873
|
+
)
|
874
|
+
@self.console_option(
|
875
|
+
"native",
|
876
|
+
"n",
|
877
|
+
action="store_true",
|
878
|
+
type=bool,
|
879
|
+
help=_("native path offset (use at you own risk)"),
|
880
|
+
)
|
881
|
+
@self.console_option(
|
882
|
+
"interpolation", "i", type=int, help=_("interpolation points per segment")
|
883
|
+
)
|
884
|
+
@self.console_command(
|
885
|
+
("offset2", "offset"),
|
886
|
+
help=_("create an offset path for any of the given elements, old algorithm"),
|
887
|
+
input_type=(None, "elements"),
|
888
|
+
output_type="elements",
|
889
|
+
)
|
890
|
+
def element_offset_path(
|
891
|
+
command,
|
892
|
+
channel,
|
893
|
+
_,
|
894
|
+
offset=None,
|
895
|
+
radial=None,
|
896
|
+
native=False,
|
897
|
+
interpolation=None,
|
898
|
+
data=None,
|
899
|
+
post=None,
|
900
|
+
**kwargs,
|
901
|
+
):
|
902
|
+
if data is None:
|
903
|
+
data = list(self.elems(emphasized=True))
|
904
|
+
if len(data) == 0:
|
905
|
+
channel(_("No elements selected"))
|
906
|
+
return "elements", data
|
907
|
+
if native:
|
908
|
+
linearize = False
|
909
|
+
else:
|
910
|
+
linearize = True
|
911
|
+
if interpolation is None:
|
912
|
+
interpolation = 500
|
913
|
+
if offset is None:
|
914
|
+
offset = 0
|
915
|
+
else:
|
916
|
+
try:
|
917
|
+
ll = Length(offset)
|
918
|
+
# Invert for right behaviour
|
919
|
+
offset = -1.0 * float(ll)
|
920
|
+
except ValueError:
|
921
|
+
offset = 0
|
922
|
+
if radial is None:
|
923
|
+
radial = False
|
924
|
+
data_out = list()
|
925
|
+
for node in data:
|
926
|
+
if hasattr(node, "as_path"):
|
927
|
+
p = abs(node.as_path())
|
928
|
+
else:
|
929
|
+
bb = node.bounds
|
930
|
+
if bb is None:
|
931
|
+
# Node has no bounds or space, therefore no offset outline.
|
932
|
+
return "elements", data_out
|
933
|
+
p = Geomstr.rect(
|
934
|
+
x=bb[0], y=bb[1], width=bb[2] - bb[0], height=bb[3] - bb[1]
|
935
|
+
).as_path()
|
936
|
+
|
937
|
+
node_path = path_offset(
|
938
|
+
p,
|
939
|
+
offset,
|
940
|
+
radial_connector=radial,
|
941
|
+
linearize=linearize,
|
942
|
+
interpolation=interpolation,
|
943
|
+
)
|
944
|
+
if node_path is None or len(node_path) == 0:
|
945
|
+
continue
|
946
|
+
node_path.validate_connections()
|
947
|
+
newnode = self.elem_branch.add(
|
948
|
+
path=node_path, type="elem path", stroke=node.stroke
|
949
|
+
)
|
950
|
+
newnode.stroke_width = UNITS_PER_PIXEL
|
951
|
+
newnode.linejoin = Linejoin.JOIN_ROUND
|
952
|
+
newnode.label = (
|
953
|
+
f"Offset of {node.id if node.label is None else node.display_label()}"
|
954
|
+
)
|
955
|
+
data_out.append(newnode)
|
956
|
+
|
957
|
+
# Newly created! Classification needed?
|
958
|
+
if len(data_out) > 0:
|
959
|
+
post.append(classify_new(data_out))
|
960
|
+
self.signal("refresh_scene", "Scene")
|
961
|
+
return "elements", data_out
|
962
|
+
|
963
|
+
# --------------------------- END COMMANDS ------------------------------
|