meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meerk40t/__init__.py +1 -1
- meerk40t/balormk/balor_params.py +167 -167
- meerk40t/balormk/clone_loader.py +457 -457
- meerk40t/balormk/controller.py +1566 -1512
- meerk40t/balormk/cylindermod.py +64 -0
- meerk40t/balormk/device.py +966 -1959
- meerk40t/balormk/driver.py +778 -591
- meerk40t/balormk/galvo_commands.py +1194 -0
- meerk40t/balormk/gui/balorconfig.py +237 -111
- meerk40t/balormk/gui/balorcontroller.py +191 -184
- meerk40t/balormk/gui/baloroperationproperties.py +116 -115
- meerk40t/balormk/gui/corscene.py +845 -0
- meerk40t/balormk/gui/gui.py +179 -147
- meerk40t/balormk/livelightjob.py +466 -382
- meerk40t/balormk/mock_connection.py +131 -109
- meerk40t/balormk/plugin.py +133 -135
- meerk40t/balormk/usb_connection.py +306 -301
- meerk40t/camera/__init__.py +1 -1
- meerk40t/camera/camera.py +514 -397
- meerk40t/camera/gui/camerapanel.py +1241 -1095
- meerk40t/camera/gui/gui.py +58 -58
- meerk40t/camera/plugin.py +441 -399
- meerk40t/ch341/__init__.py +27 -27
- meerk40t/ch341/ch341device.py +628 -628
- meerk40t/ch341/libusb.py +595 -589
- meerk40t/ch341/mock.py +171 -171
- meerk40t/ch341/windriver.py +157 -157
- meerk40t/constants.py +13 -0
- meerk40t/core/__init__.py +1 -1
- meerk40t/core/bindalias.py +550 -539
- meerk40t/core/core.py +47 -47
- meerk40t/core/cutcode/cubiccut.py +73 -73
- meerk40t/core/cutcode/cutcode.py +315 -312
- meerk40t/core/cutcode/cutgroup.py +141 -137
- meerk40t/core/cutcode/cutobject.py +192 -185
- meerk40t/core/cutcode/dwellcut.py +37 -37
- meerk40t/core/cutcode/gotocut.py +29 -29
- meerk40t/core/cutcode/homecut.py +29 -29
- meerk40t/core/cutcode/inputcut.py +34 -34
- meerk40t/core/cutcode/linecut.py +33 -33
- meerk40t/core/cutcode/outputcut.py +34 -34
- meerk40t/core/cutcode/plotcut.py +335 -335
- meerk40t/core/cutcode/quadcut.py +61 -61
- meerk40t/core/cutcode/rastercut.py +168 -148
- meerk40t/core/cutcode/waitcut.py +34 -34
- meerk40t/core/cutplan.py +1843 -1316
- meerk40t/core/drivers.py +330 -329
- meerk40t/core/elements/align.py +801 -669
- meerk40t/core/elements/branches.py +1858 -1507
- meerk40t/core/elements/clipboard.py +229 -219
- meerk40t/core/elements/element_treeops.py +4595 -2837
- meerk40t/core/elements/element_types.py +125 -105
- meerk40t/core/elements/elements.py +4315 -3617
- meerk40t/core/elements/files.py +117 -64
- meerk40t/core/elements/geometry.py +473 -224
- meerk40t/core/elements/grid.py +467 -316
- meerk40t/core/elements/materials.py +158 -94
- meerk40t/core/elements/notes.py +50 -38
- meerk40t/core/elements/offset_clpr.py +934 -912
- meerk40t/core/elements/offset_mk.py +963 -955
- meerk40t/core/elements/penbox.py +339 -267
- meerk40t/core/elements/placements.py +300 -83
- meerk40t/core/elements/render.py +785 -687
- meerk40t/core/elements/shapes.py +2618 -2092
- meerk40t/core/elements/testcases.py +105 -0
- meerk40t/core/elements/trace.py +651 -563
- meerk40t/core/elements/tree_commands.py +415 -409
- meerk40t/core/elements/undo_redo.py +116 -58
- meerk40t/core/elements/wordlist.py +319 -200
- meerk40t/core/exceptions.py +9 -9
- meerk40t/core/laserjob.py +220 -220
- meerk40t/core/logging.py +63 -63
- meerk40t/core/node/blobnode.py +83 -86
- meerk40t/core/node/bootstrap.py +105 -103
- meerk40t/core/node/branch_elems.py +40 -31
- meerk40t/core/node/branch_ops.py +45 -38
- meerk40t/core/node/branch_regmark.py +48 -41
- meerk40t/core/node/cutnode.py +29 -32
- meerk40t/core/node/effect_hatch.py +375 -257
- meerk40t/core/node/effect_warp.py +398 -0
- meerk40t/core/node/effect_wobble.py +441 -309
- meerk40t/core/node/elem_ellipse.py +404 -309
- meerk40t/core/node/elem_image.py +1082 -801
- meerk40t/core/node/elem_line.py +358 -292
- meerk40t/core/node/elem_path.py +259 -201
- meerk40t/core/node/elem_point.py +129 -102
- meerk40t/core/node/elem_polyline.py +310 -246
- meerk40t/core/node/elem_rect.py +376 -286
- meerk40t/core/node/elem_text.py +445 -418
- meerk40t/core/node/filenode.py +59 -40
- meerk40t/core/node/groupnode.py +138 -74
- meerk40t/core/node/image_processed.py +777 -766
- meerk40t/core/node/image_raster.py +156 -113
- meerk40t/core/node/layernode.py +31 -31
- meerk40t/core/node/mixins.py +135 -107
- meerk40t/core/node/node.py +1427 -1304
- meerk40t/core/node/nutils.py +117 -114
- meerk40t/core/node/op_cut.py +463 -335
- meerk40t/core/node/op_dots.py +296 -251
- meerk40t/core/node/op_engrave.py +414 -311
- meerk40t/core/node/op_image.py +755 -369
- meerk40t/core/node/op_raster.py +787 -522
- meerk40t/core/node/place_current.py +37 -40
- meerk40t/core/node/place_point.py +329 -126
- meerk40t/core/node/refnode.py +58 -47
- meerk40t/core/node/rootnode.py +225 -219
- meerk40t/core/node/util_console.py +48 -48
- meerk40t/core/node/util_goto.py +84 -65
- meerk40t/core/node/util_home.py +61 -61
- meerk40t/core/node/util_input.py +102 -102
- meerk40t/core/node/util_output.py +102 -102
- meerk40t/core/node/util_wait.py +65 -65
- meerk40t/core/parameters.py +709 -707
- meerk40t/core/planner.py +875 -785
- meerk40t/core/plotplanner.py +656 -652
- meerk40t/core/space.py +120 -113
- meerk40t/core/spoolers.py +706 -705
- meerk40t/core/svg_io.py +1836 -1549
- meerk40t/core/treeop.py +534 -445
- meerk40t/core/undos.py +278 -124
- meerk40t/core/units.py +784 -680
- meerk40t/core/view.py +393 -322
- meerk40t/core/webhelp.py +62 -62
- meerk40t/core/wordlist.py +513 -504
- meerk40t/cylinder/cylinder.py +247 -0
- meerk40t/cylinder/gui/cylindersettings.py +41 -0
- meerk40t/cylinder/gui/gui.py +24 -0
- meerk40t/device/__init__.py +1 -1
- meerk40t/device/basedevice.py +322 -123
- meerk40t/device/devicechoices.py +50 -0
- meerk40t/device/dummydevice.py +163 -128
- meerk40t/device/gui/defaultactions.py +618 -602
- meerk40t/device/gui/effectspanel.py +114 -0
- meerk40t/device/gui/formatterpanel.py +253 -290
- meerk40t/device/gui/warningpanel.py +337 -260
- meerk40t/device/mixins.py +13 -13
- meerk40t/dxf/__init__.py +1 -1
- meerk40t/dxf/dxf_io.py +766 -554
- meerk40t/dxf/plugin.py +47 -35
- meerk40t/external_plugins.py +79 -79
- meerk40t/external_plugins_build.py +28 -28
- meerk40t/extra/cag.py +112 -116
- meerk40t/extra/coolant.py +403 -0
- meerk40t/extra/encode_detect.py +204 -0
- meerk40t/extra/ezd.py +1165 -1165
- meerk40t/extra/hershey.py +834 -340
- meerk40t/extra/imageactions.py +322 -316
- meerk40t/extra/inkscape.py +628 -622
- meerk40t/extra/lbrn.py +424 -424
- meerk40t/extra/outerworld.py +283 -0
- meerk40t/extra/param_functions.py +1542 -1556
- meerk40t/extra/potrace.py +257 -253
- meerk40t/extra/serial_exchange.py +118 -0
- meerk40t/extra/updater.py +602 -453
- meerk40t/extra/vectrace.py +147 -146
- meerk40t/extra/winsleep.py +83 -83
- meerk40t/extra/xcs_reader.py +597 -0
- meerk40t/fill/fills.py +781 -335
- meerk40t/fill/patternfill.py +1061 -1061
- meerk40t/fill/patterns.py +614 -567
- meerk40t/grbl/control.py +87 -87
- meerk40t/grbl/controller.py +990 -903
- meerk40t/grbl/device.py +1084 -768
- meerk40t/grbl/driver.py +989 -771
- meerk40t/grbl/emulator.py +532 -497
- meerk40t/grbl/gcodejob.py +783 -767
- meerk40t/grbl/gui/grblconfiguration.py +373 -298
- meerk40t/grbl/gui/grblcontroller.py +485 -271
- meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
- meerk40t/grbl/gui/grbloperationconfig.py +105 -0
- meerk40t/grbl/gui/gui.py +147 -116
- meerk40t/grbl/interpreter.py +44 -44
- meerk40t/grbl/loader.py +22 -22
- meerk40t/grbl/mock_connection.py +56 -56
- meerk40t/grbl/plugin.py +294 -264
- meerk40t/grbl/serial_connection.py +93 -88
- meerk40t/grbl/tcp_connection.py +81 -79
- meerk40t/grbl/ws_connection.py +112 -0
- meerk40t/gui/__init__.py +1 -1
- meerk40t/gui/about.py +2042 -296
- meerk40t/gui/alignment.py +1644 -1608
- meerk40t/gui/autoexec.py +199 -0
- meerk40t/gui/basicops.py +791 -670
- meerk40t/gui/bufferview.py +77 -71
- meerk40t/gui/busy.py +232 -133
- meerk40t/gui/choicepropertypanel.py +1662 -1469
- meerk40t/gui/consolepanel.py +706 -542
- meerk40t/gui/devicepanel.py +687 -581
- meerk40t/gui/dialogoptions.py +110 -107
- meerk40t/gui/executejob.py +316 -306
- meerk40t/gui/fonts.py +90 -90
- meerk40t/gui/functionwrapper.py +252 -0
- meerk40t/gui/gui_mixins.py +729 -0
- meerk40t/gui/guicolors.py +205 -182
- meerk40t/gui/help_assets/help_assets.py +218 -201
- meerk40t/gui/helper.py +154 -0
- meerk40t/gui/hersheymanager.py +1440 -846
- meerk40t/gui/icons.py +3422 -2747
- meerk40t/gui/imagesplitter.py +555 -508
- meerk40t/gui/keymap.py +354 -344
- meerk40t/gui/laserpanel.py +897 -806
- meerk40t/gui/laserrender.py +1470 -1232
- meerk40t/gui/lasertoolpanel.py +805 -793
- meerk40t/gui/magnetoptions.py +436 -0
- meerk40t/gui/materialmanager.py +2944 -0
- meerk40t/gui/materialtest.py +1722 -1694
- meerk40t/gui/mkdebug.py +646 -359
- meerk40t/gui/mwindow.py +163 -140
- meerk40t/gui/navigationpanels.py +2605 -2467
- meerk40t/gui/notes.py +143 -142
- meerk40t/gui/opassignment.py +414 -410
- meerk40t/gui/operation_info.py +310 -299
- meerk40t/gui/plugin.py +500 -328
- meerk40t/gui/position.py +714 -669
- meerk40t/gui/preferences.py +901 -650
- meerk40t/gui/propertypanels/attributes.py +1461 -1131
- meerk40t/gui/propertypanels/blobproperty.py +117 -114
- meerk40t/gui/propertypanels/consoleproperty.py +83 -80
- meerk40t/gui/propertypanels/gotoproperty.py +77 -0
- meerk40t/gui/propertypanels/groupproperties.py +223 -217
- meerk40t/gui/propertypanels/hatchproperty.py +489 -469
- meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
- meerk40t/gui/propertypanels/inputproperty.py +59 -58
- meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
- meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
- meerk40t/gui/propertypanels/outputproperty.py +59 -58
- meerk40t/gui/propertypanels/pathproperty.py +389 -380
- meerk40t/gui/propertypanels/placementproperty.py +1214 -383
- meerk40t/gui/propertypanels/pointproperty.py +140 -136
- meerk40t/gui/propertypanels/propertywindow.py +313 -181
- meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
- meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
- meerk40t/gui/propertypanels/textproperty.py +770 -755
- meerk40t/gui/propertypanels/waitproperty.py +56 -55
- meerk40t/gui/propertypanels/warpproperty.py +121 -0
- meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
- meerk40t/gui/ribbon.py +2471 -2210
- meerk40t/gui/scene/scene.py +1100 -1051
- meerk40t/gui/scene/sceneconst.py +22 -22
- meerk40t/gui/scene/scenepanel.py +439 -349
- meerk40t/gui/scene/scenespacewidget.py +365 -365
- meerk40t/gui/scene/widget.py +518 -505
- meerk40t/gui/scenewidgets/affinemover.py +215 -215
- meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
- meerk40t/gui/scenewidgets/bedwidget.py +120 -97
- meerk40t/gui/scenewidgets/elementswidget.py +137 -107
- meerk40t/gui/scenewidgets/gridwidget.py +785 -745
- meerk40t/gui/scenewidgets/guidewidget.py +765 -765
- meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
- meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
- meerk40t/gui/scenewidgets/nodeselector.py +28 -28
- meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
- meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
- meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
- meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
- meerk40t/gui/simpleui.py +362 -333
- meerk40t/gui/simulation.py +2451 -2094
- meerk40t/gui/snapoptions.py +208 -203
- meerk40t/gui/spoolerpanel.py +1227 -1180
- meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
- meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
- meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
- meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
- meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
- meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
- meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
- meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
- meerk40t/gui/themes.py +200 -78
- meerk40t/gui/tips.py +590 -0
- meerk40t/gui/toolwidgets/circlebrush.py +35 -35
- meerk40t/gui/toolwidgets/toolcircle.py +248 -242
- meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
- meerk40t/gui/toolwidgets/tooldraw.py +97 -90
- meerk40t/gui/toolwidgets/toolellipse.py +219 -212
- meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
- meerk40t/gui/toolwidgets/toolline.py +39 -144
- meerk40t/gui/toolwidgets/toollinetext.py +79 -236
- meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
- meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
- meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
- meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
- meerk40t/gui/toolwidgets/toolparameter.py +754 -668
- meerk40t/gui/toolwidgets/toolplacement.py +108 -108
- meerk40t/gui/toolwidgets/toolpoint.py +68 -59
- meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
- meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
- meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
- meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
- meerk40t/gui/toolwidgets/toolrect.py +211 -207
- meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
- meerk40t/gui/toolwidgets/toolribbon.py +598 -113
- meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
- meerk40t/gui/toolwidgets/tooltext.py +98 -89
- meerk40t/gui/toolwidgets/toolvector.py +213 -204
- meerk40t/gui/toolwidgets/toolwidget.py +39 -39
- meerk40t/gui/usbconnect.py +98 -91
- meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
- meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
- meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
- meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
- meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
- meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
- meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
- meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
- meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
- meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
- meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
- meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
- meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
- meerk40t/gui/wordlisteditor.py +985 -931
- meerk40t/gui/wxmeerk40t.py +1447 -1169
- meerk40t/gui/wxmmain.py +5644 -4112
- meerk40t/gui/wxmribbon.py +1591 -1076
- meerk40t/gui/wxmscene.py +1631 -1453
- meerk40t/gui/wxmtree.py +2416 -2089
- meerk40t/gui/wxutils.py +1769 -1099
- meerk40t/gui/zmatrix.py +102 -102
- meerk40t/image/__init__.py +1 -1
- meerk40t/image/dither.py +429 -0
- meerk40t/image/imagetools.py +2793 -2269
- meerk40t/internal_plugins.py +150 -130
- meerk40t/kernel/__init__.py +63 -12
- meerk40t/kernel/channel.py +259 -212
- meerk40t/kernel/context.py +538 -538
- meerk40t/kernel/exceptions.py +41 -41
- meerk40t/kernel/functions.py +463 -414
- meerk40t/kernel/jobs.py +100 -100
- meerk40t/kernel/kernel.py +3828 -3571
- meerk40t/kernel/lifecycles.py +71 -71
- meerk40t/kernel/module.py +49 -49
- meerk40t/kernel/service.py +147 -147
- meerk40t/kernel/settings.py +383 -343
- meerk40t/lihuiyu/controller.py +883 -876
- meerk40t/lihuiyu/device.py +1181 -1069
- meerk40t/lihuiyu/driver.py +1466 -1372
- meerk40t/lihuiyu/gui/gui.py +127 -106
- meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
- meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
- meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
- meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
- meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
- meerk40t/lihuiyu/interpreter.py +53 -53
- meerk40t/lihuiyu/laserspeed.py +450 -450
- meerk40t/lihuiyu/loader.py +90 -90
- meerk40t/lihuiyu/parser.py +404 -404
- meerk40t/lihuiyu/plugin.py +101 -102
- meerk40t/lihuiyu/tcp_connection.py +111 -109
- meerk40t/main.py +231 -165
- meerk40t/moshi/builder.py +788 -781
- meerk40t/moshi/controller.py +505 -499
- meerk40t/moshi/device.py +495 -442
- meerk40t/moshi/driver.py +862 -696
- meerk40t/moshi/gui/gui.py +78 -76
- meerk40t/moshi/gui/moshicontrollergui.py +538 -522
- meerk40t/moshi/gui/moshidrivergui.py +87 -75
- meerk40t/moshi/plugin.py +43 -43
- meerk40t/network/console_server.py +140 -57
- meerk40t/network/kernelserver.py +10 -9
- meerk40t/network/tcp_server.py +142 -140
- meerk40t/network/udp_server.py +103 -77
- meerk40t/network/web_server.py +404 -0
- meerk40t/newly/controller.py +1158 -1144
- meerk40t/newly/device.py +874 -732
- meerk40t/newly/driver.py +540 -412
- meerk40t/newly/gui/gui.py +219 -188
- meerk40t/newly/gui/newlyconfig.py +116 -101
- meerk40t/newly/gui/newlycontroller.py +193 -186
- meerk40t/newly/gui/operationproperties.py +51 -51
- meerk40t/newly/mock_connection.py +82 -82
- meerk40t/newly/newly_params.py +56 -56
- meerk40t/newly/plugin.py +1214 -1246
- meerk40t/newly/usb_connection.py +322 -322
- meerk40t/rotary/gui/gui.py +52 -46
- meerk40t/rotary/gui/rotarysettings.py +240 -232
- meerk40t/rotary/rotary.py +202 -98
- meerk40t/ruida/control.py +291 -91
- meerk40t/ruida/controller.py +138 -1088
- meerk40t/ruida/device.py +676 -231
- meerk40t/ruida/driver.py +534 -472
- meerk40t/ruida/emulator.py +1494 -1491
- meerk40t/ruida/exceptions.py +4 -4
- meerk40t/ruida/gui/gui.py +71 -76
- meerk40t/ruida/gui/ruidaconfig.py +239 -72
- meerk40t/ruida/gui/ruidacontroller.py +187 -184
- meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
- meerk40t/ruida/loader.py +54 -52
- meerk40t/ruida/mock_connection.py +57 -109
- meerk40t/ruida/plugin.py +124 -87
- meerk40t/ruida/rdjob.py +2084 -945
- meerk40t/ruida/serial_connection.py +116 -0
- meerk40t/ruida/tcp_connection.py +146 -0
- meerk40t/ruida/udp_connection.py +73 -0
- meerk40t/svgelements.py +9671 -9669
- meerk40t/tools/driver_to_path.py +584 -579
- meerk40t/tools/geomstr.py +5583 -4680
- meerk40t/tools/jhfparser.py +357 -292
- meerk40t/tools/kerftest.py +904 -890
- meerk40t/tools/livinghinges.py +1168 -1033
- meerk40t/tools/pathtools.py +987 -949
- meerk40t/tools/pmatrix.py +234 -0
- meerk40t/tools/pointfinder.py +942 -942
- meerk40t/tools/polybool.py +941 -940
- meerk40t/tools/rasterplotter.py +1660 -547
- meerk40t/tools/shxparser.py +1047 -901
- meerk40t/tools/ttfparser.py +726 -446
- meerk40t/tools/zinglplotter.py +595 -593
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
- meerk40t-0.9.7020.dist-info/RECORD +446 -0
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
- meerk40t/balormk/elementlightjob.py +0 -159
- meerk40t-0.9.3001.dist-info/RECORD +0 -437
- test/bootstrap.py +0 -63
- test/test_cli.py +0 -12
- test/test_core_cutcode.py +0 -418
- test/test_core_elements.py +0 -144
- test/test_core_plotplanner.py +0 -397
- test/test_core_viewports.py +0 -312
- test/test_drivers_grbl.py +0 -108
- test/test_drivers_lihuiyu.py +0 -443
- test/test_drivers_newly.py +0 -113
- test/test_element_degenerate_points.py +0 -43
- test/test_elements_classify.py +0 -97
- test/test_elements_penbox.py +0 -22
- test/test_file_svg.py +0 -176
- test/test_fill.py +0 -155
- test/test_geomstr.py +0 -1523
- test/test_geomstr_nodes.py +0 -18
- test/test_imagetools_actualize.py +0 -306
- test/test_imagetools_wizard.py +0 -258
- test/test_kernel.py +0 -200
- test/test_laser_speeds.py +0 -3303
- test/test_length.py +0 -57
- test/test_lifecycle.py +0 -66
- test/test_operations.py +0 -251
- test/test_operations_hatch.py +0 -57
- test/test_ruida.py +0 -19
- test/test_spooler.py +0 -22
- test/test_tools_rasterplotter.py +0 -29
- test/test_wobble.py +0 -133
- test/test_zingl.py +0 -124
- {test → meerk40t/cylinder}/__init__.py +0 -0
- /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
- {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
meerk40t/tools/rasterplotter.py
CHANGED
@@ -1,547 +1,1660 @@
|
|
1
|
-
"""
|
2
|
-
The RasterPlotter is a plotter that maps particular raster pixels to directional and raster
|
3
|
-
methods. This class should be expanded to cover most raster situations.
|
4
|
-
|
5
|
-
The X_AXIS / Y_AXIS flag determines whether we raster across the X_AXIS or Y_AXIS. Standard
|
6
|
-
right-to-left rastering starting at the top edge on the left is the default. This would be
|
7
|
-
in the upper left hand corner proceeding right, and stepping towards bottom each scanline.
|
8
|
-
|
9
|
-
If the X_AXIS is set, the edge being used can be either TOP or BOTTOM. That flag means edge
|
10
|
-
with X_AXIS rastering. However, in Y_AXIS rastering the start edge can either be right-edge
|
11
|
-
or left-edge, for this the RIGHT and LEFT flags are used. However, the start point on either
|
12
|
-
edge can be TOP or BOTTOM if we're starting on a RIGHT or LEFT edge. So those flags mark
|
13
|
-
that. The same is true for RIGHT or LEFT on a TOP or BOTTOM edge.
|
14
|
-
|
15
|
-
The TOP, RIGHT, LEFT, BOTTOM combined give you the starting corner.
|
16
|
-
|
17
|
-
The rasters can either be BIDIRECTIONAL or UNIDIRECTIONAL meaning they raster on both swings
|
18
|
-
or only on forward swing.
|
19
|
-
"""
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
if
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
if self.
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
if
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
def
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
return
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
return
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
def
|
346
|
-
"""
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
460
|
-
|
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
|
-
|
1
|
+
"""
|
2
|
+
The RasterPlotter is a plotter that maps particular raster pixels to directional and raster
|
3
|
+
methods. This class should be expanded to cover most raster situations.
|
4
|
+
|
5
|
+
The X_AXIS / Y_AXIS flag determines whether we raster across the X_AXIS or Y_AXIS. Standard
|
6
|
+
right-to-left rastering starting at the top edge on the left is the default. This would be
|
7
|
+
in the upper left hand corner proceeding right, and stepping towards bottom each scanline.
|
8
|
+
|
9
|
+
If the X_AXIS is set, the edge being used can be either TOP or BOTTOM. That flag means edge
|
10
|
+
with X_AXIS rastering. However, in Y_AXIS rastering the start edge can either be right-edge
|
11
|
+
or left-edge, for this the RIGHT and LEFT flags are used. However, the start point on either
|
12
|
+
edge can be TOP or BOTTOM if we're starting on a RIGHT or LEFT edge. So those flags mark
|
13
|
+
that. The same is true for RIGHT or LEFT on a TOP or BOTTOM edge.
|
14
|
+
|
15
|
+
The TOP, RIGHT, LEFT, BOTTOM combined give you the starting corner.
|
16
|
+
|
17
|
+
The rasters can either be BIDIRECTIONAL or UNIDIRECTIONAL meaning they raster on both swings
|
18
|
+
or only on forward swing.
|
19
|
+
"""
|
20
|
+
|
21
|
+
import numpy as np
|
22
|
+
from math import sqrt
|
23
|
+
from time import perf_counter, sleep
|
24
|
+
from meerk40t.constants import (
|
25
|
+
RASTER_T2B,
|
26
|
+
RASTER_B2T,
|
27
|
+
RASTER_R2L,
|
28
|
+
RASTER_L2R,
|
29
|
+
RASTER_HATCH,
|
30
|
+
RASTER_GREEDY_H,
|
31
|
+
RASTER_GREEDY_V,
|
32
|
+
RASTER_CROSSOVER,
|
33
|
+
RASTER_SPIRAL,
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
class RasterPlotter:
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
data,
|
41
|
+
width,
|
42
|
+
height,
|
43
|
+
direction=0,
|
44
|
+
horizontal=True,
|
45
|
+
start_minimum_y=True,
|
46
|
+
start_minimum_x=True,
|
47
|
+
bidirectional=True,
|
48
|
+
use_integers=True,
|
49
|
+
skip_pixel=0,
|
50
|
+
overscan=0,
|
51
|
+
offset_x=0,
|
52
|
+
offset_y=0,
|
53
|
+
step_x=1,
|
54
|
+
step_y=1,
|
55
|
+
filter=None,
|
56
|
+
laserspot=0,
|
57
|
+
special=None,
|
58
|
+
):
|
59
|
+
"""
|
60
|
+
Initialization for the Raster Plotter function. This should set all the needed parameters for plotting.
|
61
|
+
|
62
|
+
@param data: pixel data accessed through data[x,y] parameters
|
63
|
+
@param width: Width of the given data.
|
64
|
+
@param height: Height of the given data.
|
65
|
+
@param horizontal: Flags for how the pixel traversal should be conducted.
|
66
|
+
@param start_minimum_y: Flags for how the pixel traversal should be conducted.
|
67
|
+
@param start_minimum_x: Flags for how the pixel traversal should be conducted.
|
68
|
+
@param bidirectional: Flags for how the pixel traversal should be conducted.
|
69
|
+
@param skip_pixel: Skip pixel. If this value is the pixel value, we skip travel in that direction.
|
70
|
+
@param use_integers: return integer values rather than floating point values.
|
71
|
+
@param overscan: The extra amount of padding to add to the end scanline.
|
72
|
+
@param offset_x: The offset in x of the rastering location. This will be added to x values returned in plot.
|
73
|
+
@param offset_y: The offset in y of the rastering location. This will be added to y values returned in plot.
|
74
|
+
@param step_x: The amount units per pixel.
|
75
|
+
@param step_y: The amount scanline gap.
|
76
|
+
@param filter: Pixel filter is called for each pixel to transform or alter it as needed. The actual
|
77
|
+
implementation is agnostic regarding what data is provided. The filter is expected
|
78
|
+
to convert the data[x,y] into some form which will be expressed by plot. Unless skipped as
|
79
|
+
part of the skip pixel.
|
80
|
+
@param direction: 0 top 2 bottom, 1 bottom 2 top, 2 right 2 left, 3 left 2 right, 4 greedy path optimisation, 5 crossover
|
81
|
+
a) greedy will keep the main direction, a bit time intensive for larger images
|
82
|
+
b) crossover draws first all majority lines, then all majority columns
|
83
|
+
(to be decided by looping over the matrix and looking at the amount
|
84
|
+
of black pixels on the same line / on the same column), timewise okayish
|
85
|
+
@param laserspot: the laserbeam diameter in pixels (low dpi = irrelevant, high dpi very relevant)
|
86
|
+
@param special: a dict of special treatment instructions for the different algorithms
|
87
|
+
"""
|
88
|
+
# Don't try to plot from two different sources at the same time...
|
89
|
+
self._locked = False
|
90
|
+
|
91
|
+
if special is None:
|
92
|
+
special = {}
|
93
|
+
self.debug_level = 0 # 0 Nothing, 1 file creation, 2 file + summary, 3 file + summary + details
|
94
|
+
self.data = data
|
95
|
+
self.width = width
|
96
|
+
self.height = height
|
97
|
+
self.direction = direction
|
98
|
+
self._cache = None
|
99
|
+
parameters = {
|
100
|
+
# Provide an override for the minimumx / minimumy / horizontal / bidirectional
|
101
|
+
RASTER_T2B: (None, True, True, None), # top to bottom
|
102
|
+
RASTER_B2T: (None, False, True, None), # bottom to top
|
103
|
+
RASTER_R2L: (False, None, False, None), # right to left
|
104
|
+
RASTER_L2R: (True, None, False, None), # left to right
|
105
|
+
RASTER_HATCH: (None, None, None, None), # crossraster (one of the two)
|
106
|
+
RASTER_GREEDY_H: (None, None, None, True), # greedy neighbour horizontal
|
107
|
+
RASTER_GREEDY_V: (None, None, None, True), # greedy neighbour
|
108
|
+
RASTER_CROSSOVER: (None, None, None, True), # true crossover
|
109
|
+
}
|
110
|
+
def_x, def_y, def_hor, def_bidir = parameters.get(direction, (None, None, None, None))
|
111
|
+
self.start_minimum_x = start_minimum_x if def_x is None else def_x
|
112
|
+
self.start_minimum_y = start_minimum_y if def_y is None else def_y
|
113
|
+
self.horizontal = horizontal if def_hor is None else def_hor
|
114
|
+
self.bidirectional = bidirectional if def_bidir is None else def_bidir
|
115
|
+
|
116
|
+
self.use_integers = use_integers
|
117
|
+
self.skip_pixel = skip_pixel
|
118
|
+
# laserspot = width in pixels, so the surrounding logic needs to calculate it
|
119
|
+
# We consider an overlap only in the enclosed square of the circle
|
120
|
+
# and calculate the overlap in pixels to the left / to the right
|
121
|
+
self.overlap = int(laserspot / sqrt(2) / 2)
|
122
|
+
# self.overlap = 1
|
123
|
+
self.special = dict(special) # Copy it so it wont be changed
|
124
|
+
if horizontal:
|
125
|
+
self.overscan = round(overscan / float(step_x))
|
126
|
+
else:
|
127
|
+
self.overscan = round(overscan / float(step_y))
|
128
|
+
self.offset_x = offset_x
|
129
|
+
self.offset_y = offset_y
|
130
|
+
self.step_x = step_x
|
131
|
+
self.step_y = step_y
|
132
|
+
self.filter = filter
|
133
|
+
self.initial_x, self.initial_y = self.calculate_first_pixel()
|
134
|
+
self.final_x, self.final_y = self.calculate_last_pixel()
|
135
|
+
self._distance_travel = 0
|
136
|
+
self._distance_burn = 0
|
137
|
+
|
138
|
+
def __repr__(self):
|
139
|
+
methods = (
|
140
|
+
"Top2Bottom",
|
141
|
+
"Bottom2Top",
|
142
|
+
"Right2Left",
|
143
|
+
"Left2Right",
|
144
|
+
"Hatch",
|
145
|
+
"Greedy Neighbor Hor",
|
146
|
+
"Greedy Neighbor Ver",
|
147
|
+
"Crossover",
|
148
|
+
"Spiral",
|
149
|
+
)
|
150
|
+
if 0 <= self.direction < len(methods):
|
151
|
+
s_meth = f"Rasterplotter ({self.width}x{self.height}): {methods[self.direction]} ({self.direction})"
|
152
|
+
else:
|
153
|
+
s_meth = f"Rasterplotter ({self.width}x{self.height}): Unknown {self.direction}"
|
154
|
+
s_direc = 'Bidirectional' if self.bidirectional else 'Unidirectional'
|
155
|
+
s_axis = 'horizontal' if self.horizontal else 'vertical'
|
156
|
+
s_ystart = 'top' if self.start_minimum_y else 'bottom'
|
157
|
+
s_xstart = 'left' if self.start_minimum_x else 'right'
|
158
|
+
return f"{s_meth}, {s_direc} {s_axis} plot starting at {s_ystart}-{s_xstart}"
|
159
|
+
|
160
|
+
def reset(self):
|
161
|
+
self._cache = None
|
162
|
+
|
163
|
+
def px(self, x, y):
|
164
|
+
"""
|
165
|
+
Returns the filtered pixel
|
166
|
+
|
167
|
+
@param x:
|
168
|
+
@param y:
|
169
|
+
@return: Filtered Pixel
|
170
|
+
"""
|
171
|
+
if 0 <= y < self.height and 0 <= x < self.width:
|
172
|
+
if self.filter is None:
|
173
|
+
return self.data[x, y]
|
174
|
+
return self.filter(self.data[x, y])
|
175
|
+
raise IndexError
|
176
|
+
|
177
|
+
def leftmost_not_equal(self, y):
|
178
|
+
"""
|
179
|
+
Determine the leftmost pixel that is not equal to the skip_pixel value.
|
180
|
+
|
181
|
+
if all pixels skipped returns None
|
182
|
+
"""
|
183
|
+
for x in range(0, self.width):
|
184
|
+
pixel = self.px(x, y)
|
185
|
+
if pixel != self.skip_pixel:
|
186
|
+
return x
|
187
|
+
return None
|
188
|
+
|
189
|
+
def topmost_not_equal(self, x):
|
190
|
+
"""
|
191
|
+
Determine the topmost pixel that is not equal to the skip_pixel value
|
192
|
+
|
193
|
+
if all pixels skipped returns None
|
194
|
+
"""
|
195
|
+
for y in range(0, self.height):
|
196
|
+
pixel = self.px(x, y)
|
197
|
+
if pixel != self.skip_pixel:
|
198
|
+
return y
|
199
|
+
return None
|
200
|
+
|
201
|
+
def rightmost_not_equal(self, y):
|
202
|
+
"""
|
203
|
+
Determine the rightmost pixel that is not equal to the skip_pixel value
|
204
|
+
|
205
|
+
if all pixels skipped returns None
|
206
|
+
"""
|
207
|
+
for x in range(self.width - 1, -1, -1):
|
208
|
+
pixel = self.px(x, y)
|
209
|
+
if pixel != self.skip_pixel:
|
210
|
+
return x
|
211
|
+
return None
|
212
|
+
|
213
|
+
def bottommost_not_equal(self, x):
|
214
|
+
"""
|
215
|
+
Determine the bottommost pixel that is not equal to the skip_pixel value
|
216
|
+
|
217
|
+
if all pixels skipped returns None
|
218
|
+
"""
|
219
|
+
for y in range(self.height - 1, -1, -1):
|
220
|
+
pixel = self.px(x, y)
|
221
|
+
if pixel != self.skip_pixel:
|
222
|
+
return y
|
223
|
+
return None
|
224
|
+
|
225
|
+
@property
|
226
|
+
def distance_travel(self):
|
227
|
+
return self._distance_travel
|
228
|
+
|
229
|
+
@property
|
230
|
+
def distance_burn(self):
|
231
|
+
return self._distance_burn
|
232
|
+
|
233
|
+
def nextcolor_left(self, x, y, default=None):
|
234
|
+
"""
|
235
|
+
Determine the next pixel change going left from the (x,y) point.
|
236
|
+
If no next pixel is found default is returned.
|
237
|
+
"""
|
238
|
+
if x <= -1:
|
239
|
+
return default
|
240
|
+
if x == 0:
|
241
|
+
return -1
|
242
|
+
if x == self.width:
|
243
|
+
return self.width - 1
|
244
|
+
if self.width < x:
|
245
|
+
return self.width
|
246
|
+
|
247
|
+
v = self.px(x, y)
|
248
|
+
for ix in range(x, -1, -1):
|
249
|
+
pixel = self.px(ix, y)
|
250
|
+
if pixel != v:
|
251
|
+
return ix
|
252
|
+
return 0
|
253
|
+
|
254
|
+
def nextcolor_top(self, x, y, default=None):
|
255
|
+
"""
|
256
|
+
Determine the next pixel change going top from the (x,y) point.
|
257
|
+
If no next pixel is found default is returned.
|
258
|
+
"""
|
259
|
+
if y <= -1:
|
260
|
+
return default
|
261
|
+
if y == 0:
|
262
|
+
return -1
|
263
|
+
if y == self.height:
|
264
|
+
return self.height - 1
|
265
|
+
if self.height < y:
|
266
|
+
return self.height
|
267
|
+
|
268
|
+
v = self.px(x, y)
|
269
|
+
for iy in range(y, -1, -1):
|
270
|
+
pixel = self.px(x, iy)
|
271
|
+
if pixel != v:
|
272
|
+
return iy
|
273
|
+
return 0
|
274
|
+
|
275
|
+
def nextcolor_right(self, x, y, default=None):
|
276
|
+
"""
|
277
|
+
Determine the next pixel change going right from the (x,y) point.
|
278
|
+
If no next pixel is found default is returned.
|
279
|
+
"""
|
280
|
+
if x < -1:
|
281
|
+
return -1
|
282
|
+
if x == -1:
|
283
|
+
return 0
|
284
|
+
if x == self.width - 1:
|
285
|
+
return self.width
|
286
|
+
if self.width <= x:
|
287
|
+
return default
|
288
|
+
|
289
|
+
v = self.px(x, y)
|
290
|
+
for ix in range(x, self.width):
|
291
|
+
pixel = self.px(ix, y)
|
292
|
+
if pixel != v:
|
293
|
+
return ix
|
294
|
+
return self.width - 1
|
295
|
+
|
296
|
+
def nextcolor_bottom(self, x, y, default=None):
|
297
|
+
"""
|
298
|
+
Determine the next pixel change going bottom from the (x,y) point.
|
299
|
+
If no next pixel is found default is returned.
|
300
|
+
"""
|
301
|
+
if y < -1:
|
302
|
+
return -1
|
303
|
+
if y == -1:
|
304
|
+
return 0
|
305
|
+
if y == self.height - 1:
|
306
|
+
return self.height
|
307
|
+
if self.height <= y:
|
308
|
+
return default
|
309
|
+
|
310
|
+
v = self.px(x, y)
|
311
|
+
for iy in range(y, self.height):
|
312
|
+
pixel = self.px(x, iy)
|
313
|
+
if pixel != v:
|
314
|
+
return iy
|
315
|
+
return self.height - 1
|
316
|
+
|
317
|
+
def calculate_next_horizontal_pixel(self, y, dy=1, leftmost_pixel=True):
|
318
|
+
"""
|
319
|
+
Find the horizontal extreme at the given y-scanline, stepping by dy in the target image.
|
320
|
+
This can be done on either the leftmost (True) or rightmost (False).
|
321
|
+
|
322
|
+
@param y: y-scanline
|
323
|
+
@param dy: dy-step amount (usually should be -1 or 1)
|
324
|
+
@param leftmost_pixel: find pixel from the left
|
325
|
+
@return:
|
326
|
+
"""
|
327
|
+
try:
|
328
|
+
if leftmost_pixel:
|
329
|
+
while True:
|
330
|
+
x = self.leftmost_not_equal(y)
|
331
|
+
if x is not None:
|
332
|
+
break
|
333
|
+
y += dy
|
334
|
+
else:
|
335
|
+
while True:
|
336
|
+
x = self.rightmost_not_equal(y)
|
337
|
+
if x is not None:
|
338
|
+
break
|
339
|
+
y += dy
|
340
|
+
except IndexError:
|
341
|
+
# Remaining image is blank
|
342
|
+
return None, None
|
343
|
+
return x, y
|
344
|
+
|
345
|
+
def calculate_next_vertical_pixel(self, x, dx=1, topmost_pixel=True):
|
346
|
+
"""
|
347
|
+
Find the vertical extreme at the given x-scanline, stepping by dx in the target image.
|
348
|
+
This can be done on either the topmost (True) or bottommost (False).
|
349
|
+
|
350
|
+
@param x: x-scanline
|
351
|
+
@param dx: dx-step amount (usually should be -1 or 1)
|
352
|
+
@param topmost_pixel: find the pixel from the top
|
353
|
+
@return:
|
354
|
+
"""
|
355
|
+
try:
|
356
|
+
if topmost_pixel:
|
357
|
+
while True:
|
358
|
+
y = self.topmost_not_equal(x)
|
359
|
+
if y is not None:
|
360
|
+
break
|
361
|
+
x += dx
|
362
|
+
else:
|
363
|
+
while True:
|
364
|
+
# find that the bottommost pixel in that row.
|
365
|
+
y = self.bottommost_not_equal(x)
|
366
|
+
|
367
|
+
if y is not None:
|
368
|
+
# This is a valid pixel.
|
369
|
+
break
|
370
|
+
# No pixel in that row was valid. Move to the next row.
|
371
|
+
x += dx
|
372
|
+
except IndexError:
|
373
|
+
# Remaining image was blank, there are no more relevant pixels.
|
374
|
+
return None, None
|
375
|
+
return x, y
|
376
|
+
|
377
|
+
def calculate_first_pixel(self):
|
378
|
+
"""
|
379
|
+
Find the first non-skipped pixel in the rastering.
|
380
|
+
|
381
|
+
This takes into account the traversal values of X_AXIS or Y_AXIS and BOTTOM and RIGHT
|
382
|
+
The start edge and the start point.
|
383
|
+
|
384
|
+
@return: x,y coordinates of first pixel.
|
385
|
+
"""
|
386
|
+
if self.horizontal:
|
387
|
+
y = 0 if self.start_minimum_y else self.height - 1
|
388
|
+
dy = 1 if self.start_minimum_y else -1
|
389
|
+
x, y = self.calculate_next_horizontal_pixel(y, dy, self.start_minimum_x)
|
390
|
+
return x, y
|
391
|
+
else:
|
392
|
+
x = 0 if self.start_minimum_x else self.width - 1
|
393
|
+
dx = 1 if self.start_minimum_x else -1
|
394
|
+
x, y = self.calculate_next_vertical_pixel(x, dx, self.start_minimum_y)
|
395
|
+
return x, y
|
396
|
+
|
397
|
+
def calculate_last_pixel(self):
|
398
|
+
"""
|
399
|
+
Find the last non-skipped pixel in the rastering.
|
400
|
+
|
401
|
+
First and last scanlines start from the same side when scanline count is odd
|
402
|
+
|
403
|
+
@return: x,y coordinates of last pixel.
|
404
|
+
"""
|
405
|
+
if self.horizontal:
|
406
|
+
y = self.height - 1 if self.start_minimum_y else 0
|
407
|
+
dy = -1 if self.start_minimum_y else 1
|
408
|
+
start_on_left = (
|
409
|
+
self.start_minimum_x if self.width & 1 else not self.start_minimum_x
|
410
|
+
)
|
411
|
+
x, y = self.calculate_next_horizontal_pixel(y, dy, start_on_left)
|
412
|
+
return x, y
|
413
|
+
else:
|
414
|
+
x = self.width - 1 if self.start_minimum_x else 0
|
415
|
+
dx = -1 if self.start_minimum_x else 1
|
416
|
+
start_on_top = (
|
417
|
+
self.start_minimum_y if self.height & 1 else not self.start_minimum_y
|
418
|
+
)
|
419
|
+
x, y = self.calculate_next_vertical_pixel(x, dx, start_on_top)
|
420
|
+
return x, y
|
421
|
+
|
422
|
+
def initial_position(self):
|
423
|
+
"""
|
424
|
+
Returns raw initial position for the relevant pixel within the data.
|
425
|
+
@return: initial position within the data.
|
426
|
+
"""
|
427
|
+
if self.use_integers:
|
428
|
+
return int(round(self.initial_x)), int(round(self.initial_y))
|
429
|
+
else:
|
430
|
+
return self.initial_x, self.initial_y
|
431
|
+
|
432
|
+
def initial_position_in_scene(self):
|
433
|
+
"""
|
434
|
+
Returns the initial position for this within the scene. Taking into account start corner, and step size.
|
435
|
+
@return: initial position within scene. The first plot location.
|
436
|
+
"""
|
437
|
+
if self.initial_x is None: # image is blank.
|
438
|
+
if self.use_integers:
|
439
|
+
return int(round(self.offset_x)), int(round(self.offset_y))
|
440
|
+
else:
|
441
|
+
return self.offset_x, self.offset_y
|
442
|
+
if self.use_integers:
|
443
|
+
return (
|
444
|
+
int(round(self.offset_x + self.initial_x * self.step_x)),
|
445
|
+
int(round(self.offset_y + self.initial_y * self.step_y)),
|
446
|
+
)
|
447
|
+
else:
|
448
|
+
return (
|
449
|
+
self.offset_x + self.initial_x * self.step_x,
|
450
|
+
self.offset_y + self.initial_y * self.step_y,
|
451
|
+
)
|
452
|
+
|
453
|
+
def final_position_in_scene(self):
|
454
|
+
"""
|
455
|
+
Returns best guess of final position relative to the scene offset. Taking into account start corner, and parity
|
456
|
+
of the width and height.
|
457
|
+
@return:
|
458
|
+
"""
|
459
|
+
if self.final_x is None: # image is blank.
|
460
|
+
if self.use_integers:
|
461
|
+
return int(round(self.offset_x)), int(round(self.offset_y))
|
462
|
+
else:
|
463
|
+
return self.offset_x, self.offset_y
|
464
|
+
if self.use_integers:
|
465
|
+
return (
|
466
|
+
int(round(self.offset_x + self.final_x * self.step_x)),
|
467
|
+
int(round(self.offset_y + self.final_y * self.step_y)),
|
468
|
+
)
|
469
|
+
else:
|
470
|
+
return (
|
471
|
+
self.offset_x + self.final_x * self.step_x,
|
472
|
+
self.offset_y + self.final_y * self.step_y,
|
473
|
+
)
|
474
|
+
|
475
|
+
def plot(self):
|
476
|
+
"""
|
477
|
+
Plot the values yielded by following the given raster plotter in the traversal defined.
|
478
|
+
"""
|
479
|
+
while self._locked:
|
480
|
+
sleep(0.1)
|
481
|
+
self._locked = True
|
482
|
+
offset_x = self.offset_x
|
483
|
+
offset_y = self.offset_y
|
484
|
+
step_x = self.step_x
|
485
|
+
step_y = self.step_y
|
486
|
+
self._distance_travel = 0
|
487
|
+
self._distance_burn = 0
|
488
|
+
|
489
|
+
if self.initial_x is None:
|
490
|
+
# There is no image.
|
491
|
+
return
|
492
|
+
# Debug code....
|
493
|
+
methods = (
|
494
|
+
"Top2Bottom",
|
495
|
+
"Bottom2Top",
|
496
|
+
"Right2Left",
|
497
|
+
"Left2Right",
|
498
|
+
"Hatch",
|
499
|
+
"Greedy Neighbor Hor",
|
500
|
+
"Greedy Neighbor Ver",
|
501
|
+
"Crossover",
|
502
|
+
"Spiral",
|
503
|
+
)
|
504
|
+
testmethods = (
|
505
|
+
"Test: Horizontal Rectangle",
|
506
|
+
"Test: Vertical Rectangle",
|
507
|
+
"Test: Horizontal Snake",
|
508
|
+
"Test: Vertical Snake",
|
509
|
+
"Test: Spiral",
|
510
|
+
)
|
511
|
+
if self._cache is None:
|
512
|
+
if self.debug_level > 0:
|
513
|
+
try:
|
514
|
+
if self.direction >= 0:
|
515
|
+
m = methods[self.direction]
|
516
|
+
else:
|
517
|
+
m = testmethods[abs(self.direction) - 1]
|
518
|
+
s_meth = f"Method: {m} ({self.direction})"
|
519
|
+
except IndexError:
|
520
|
+
s_meth = f"Method: Unknown {self.direction}"
|
521
|
+
print (s_meth)
|
522
|
+
data = list(self._plot_pixels())
|
523
|
+
from platform import system
|
524
|
+
defaultdir = "c:\\temp\\" if system() == "Windows" else ""
|
525
|
+
has_duplicates = 0
|
526
|
+
tstamp = int(perf_counter() * 100)
|
527
|
+
with open(f"{defaultdir}plot_{tstamp}.txt", mode="w") as f:
|
528
|
+
f.write(f"0.9.7\n{s_meth}\n{'Bidirectional' if self.bidirectional else 'Unidirectional'} {'horizontal' if self.horizontal else 'vertical'} plot starting at {'top' if self.start_minimum_y else 'bottom'}-{'left' if self.start_minimum_x else 'right'}\n")
|
529
|
+
f.write(f"Overscan: {self.overscan:.2f}, Stepx={step_x:.2f}, Stepy={step_y:.2f}\n")
|
530
|
+
f.write(f"Image dimensions: {self.width}x{self.height}\n")
|
531
|
+
f.write(f"Startpoint: {self.initial_x}, {self.initial_y}\n")
|
532
|
+
f.write(f"Overlapping pixels to any side: {self.overlap}\n")
|
533
|
+
if self.special:
|
534
|
+
f.write(f"Special instructions:\n")
|
535
|
+
for key, value in self.special.items():
|
536
|
+
f.write(f" {key} = {value}\n")
|
537
|
+
f.write("----------------------------------------------------------------------\n")
|
538
|
+
test_dict = {}
|
539
|
+
lastx = self.initial_x
|
540
|
+
lasty = self.initial_y
|
541
|
+
failed = False
|
542
|
+
for lineno, (x, y, on) in enumerate(data, start=1):
|
543
|
+
if lastx is not None:
|
544
|
+
dx = x - lastx
|
545
|
+
dy = y - lasty
|
546
|
+
if dx != 0 and dy != 0: # and abs(dx) != abs(dy):
|
547
|
+
f.write (f"You f**ed up! No zigzag movement from line {lineno - 1} to {lineno}: {lastx}, {lasty} -> {x}, {y}\n")
|
548
|
+
print (f"You f**ed up! No zigzag movement from line {lineno - 1} to {lineno}: {lastx}, {lasty} -> {x}, {y}")
|
549
|
+
failed = True
|
550
|
+
lastx = x
|
551
|
+
lasty = y
|
552
|
+
if not failed:
|
553
|
+
f.write("Good news, no zig-zag movements identified!\n")
|
554
|
+
f.write("----------------------------------------------------------------------\n")
|
555
|
+
for lineno, (x, y, on) in enumerate(data, start=1):
|
556
|
+
if x is None or y is None:
|
557
|
+
continue
|
558
|
+
key = f"{x} - {y}"
|
559
|
+
if key in test_dict:
|
560
|
+
f.write (f"Duplicate coordinates in list at ({x}, {y})! 1st: #{test_dict[key][0]}, on={test_dict[key][1]}, 2nd: #{lineno}, on={on}\n")
|
561
|
+
has_duplicates += 1
|
562
|
+
else:
|
563
|
+
test_dict[key] = (lineno, on)
|
564
|
+
if has_duplicates:
|
565
|
+
f.write("----------------------------------------------------------------------\n")
|
566
|
+
for lineno, (x, y, on) in enumerate(data, start=1):
|
567
|
+
f.write(f"{lineno}: {x}, {y}, {on}\n")
|
568
|
+
if has_duplicates:
|
569
|
+
print(f"Attention: the generated plot has {has_duplicates} duplicate coordinate values!")
|
570
|
+
print(f"{'Bidirectional' if self.bidirectional else 'Unidirectional'} {'horizontal' if self.horizontal else 'vertical'} plot starting at {'top' if self.start_minimum_y else 'bottom'}-{'left' if self.start_minimum_x else 'right'}")
|
571
|
+
print(f"Overscan: {self.overscan:.2f}, Stepx={step_x:.2f}, Stepy={step_y:.2f}")
|
572
|
+
print(f"Image dimensions: {self.width}x{self.height}")
|
573
|
+
else:
|
574
|
+
data = list(self._plot_pixels())
|
575
|
+
self._cache = data
|
576
|
+
else:
|
577
|
+
data = self._cache
|
578
|
+
last_x = offset_x
|
579
|
+
last_y = offset_y
|
580
|
+
if self.use_integers:
|
581
|
+
for x, y, on in data:
|
582
|
+
if x is None or y is None:
|
583
|
+
# Passthrough
|
584
|
+
yield x, y, on
|
585
|
+
else:
|
586
|
+
nx = int(round(offset_x + step_x * x))
|
587
|
+
ny = int(round(offset_y + y * step_y))
|
588
|
+
self._distance_burn += 0 if on == 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
|
589
|
+
self._distance_travel += 0 if on != 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
|
590
|
+
yield nx, ny, on
|
591
|
+
last_x = nx
|
592
|
+
last_y = ny
|
593
|
+
else:
|
594
|
+
for x, y, on in data:
|
595
|
+
if x is None or y is None:
|
596
|
+
# Passthrough
|
597
|
+
yield x, y, on
|
598
|
+
else:
|
599
|
+
nx = round(offset_x + step_x * x)
|
600
|
+
ny = round(offset_y + y * step_y)
|
601
|
+
self._distance_burn += 0 if on == 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
|
602
|
+
self._distance_travel += 0 if on != 0 else sqrt( (nx - last_x) * (nx-last_x) + (ny - last_y) * (ny - last_y) )
|
603
|
+
yield offset_x + step_x * x, offset_y + y * step_y, on
|
604
|
+
last_x = nx
|
605
|
+
last_y = ny
|
606
|
+
self._locked = False
|
607
|
+
|
608
|
+
def _plot_pixels(self):
|
609
|
+
legacy = self.special.get("legacy", False)
|
610
|
+
if self.direction in (RASTER_GREEDY_H, RASTER_GREEDY_V):
|
611
|
+
yield from self._plot_greedy_neighbour(horizontal=self.horizontal)
|
612
|
+
elif self.direction == RASTER_CROSSOVER:
|
613
|
+
yield from self._plot_crossover()
|
614
|
+
elif self.direction == RASTER_SPIRAL:
|
615
|
+
yield from self._plot_spiral()
|
616
|
+
# elif self.direction < 0:
|
617
|
+
# yield from self.testpattern_generator()
|
618
|
+
elif self.horizontal:
|
619
|
+
if legacy:
|
620
|
+
yield from self._legacy_plot_horizontal()
|
621
|
+
else:
|
622
|
+
yield from self._plot_horizontal()
|
623
|
+
else:
|
624
|
+
if legacy:
|
625
|
+
yield from self._legacy_plot_vertical()
|
626
|
+
else:
|
627
|
+
yield from self._plot_vertical()
|
628
|
+
# yield from self._plot_vertical()
|
629
|
+
|
630
|
+
def _debug_data(self, force=False):
|
631
|
+
if self.debug_level < 3 and not force:
|
632
|
+
return
|
633
|
+
BLANK = 255
|
634
|
+
for y in range(self.height):
|
635
|
+
msg:str = f"{y:3d}: "
|
636
|
+
for x in range(self.width):
|
637
|
+
msg += "." if self.data[x, y] == BLANK else "X"
|
638
|
+
print (msg)
|
639
|
+
|
640
|
+
def _get_pixel_chains(self, xy:int, is_x : bool) -> list:
|
641
|
+
last_pixel = None
|
642
|
+
segments = []
|
643
|
+
upper = self.width if is_x else self.height
|
644
|
+
for idx in range(upper):
|
645
|
+
pixel = self.px(idx, xy) if is_x else self.px(xy, idx)
|
646
|
+
on = 0 if pixel == self.skip_pixel else pixel
|
647
|
+
if on:
|
648
|
+
if on == last_pixel:
|
649
|
+
segments[-1][1] = idx
|
650
|
+
else:
|
651
|
+
segments.append ([idx, idx, on])
|
652
|
+
last_pixel = on
|
653
|
+
return segments
|
654
|
+
|
655
|
+
def _consume_pixel_chains(self, segments:list, xy:int, is_x : bool):
|
656
|
+
BLANK = 255
|
657
|
+
# for x in range(5):
|
658
|
+
# msg1 = f"{x}: "
|
659
|
+
# msg2 = ""
|
660
|
+
# for y in range(5):
|
661
|
+
# msg1 += "." if self.data[x, y] == BLANK else "X"
|
662
|
+
# msg2 += f" {self.data[x, y]}"
|
663
|
+
# print (msg1, msg2)
|
664
|
+
|
665
|
+
for seg in segments:
|
666
|
+
c_start = seg[0]
|
667
|
+
c_end = seg[1]
|
668
|
+
for idx in range(c_start, c_end + 1):
|
669
|
+
if is_x:
|
670
|
+
px = idx
|
671
|
+
py = xy
|
672
|
+
else:
|
673
|
+
px = xy
|
674
|
+
py = idx
|
675
|
+
for x_idx in range(-self.overlap, self.overlap + 1):
|
676
|
+
for y_idx in range(-self.overlap, self.overlap + 1):
|
677
|
+
nx = px + x_idx
|
678
|
+
ny = py + y_idx
|
679
|
+
if nx < 0 or nx >= self.width or ny < 0 or ny >= self.height:
|
680
|
+
continue
|
681
|
+
self.data[nx, ny] = BLANK
|
682
|
+
self._debug_data()
|
683
|
+
|
684
|
+
def _plot_vertical(self):
|
685
|
+
"""
|
686
|
+
This code is for vertical rastering.
|
687
|
+
We are looking first for all consecutive pixel chains with the same pixel value
|
688
|
+
Then we loop through the segments and yield the 'end edge' of the 'last' pixel
|
689
|
+
'end edge' and 'last' are dependent on the sweep direction.
|
690
|
+
There is one peculiarity though that is required for K40 lasers:
|
691
|
+
a) We may only move from one yielded (x,y,on) tuple in a pure horizontal or pure
|
692
|
+
vertical fashion (we could as well go perfectly diagonal but we are not using
|
693
|
+
this feature). So at the end of one sweepline we need to change to the
|
694
|
+
next scanline by going directly up/down/left/right and then move to the first
|
695
|
+
relevant pixel.
|
696
|
+
b) we need to take care that we are not landing on the same pixel twice. So we move
|
697
|
+
outside the new chain to avoid this
|
698
|
+
"""
|
699
|
+
unidirectional = not self.bidirectional
|
700
|
+
|
701
|
+
dx = 1 if self.start_minimum_x else -1
|
702
|
+
dy = 1 if self.start_minimum_y else -1
|
703
|
+
lower = min(self.initial_x, self.final_x)
|
704
|
+
upper = max(self.initial_x, self.final_x)
|
705
|
+
last_x = self.initial_x
|
706
|
+
last_y = self.initial_y
|
707
|
+
x = lower if self.start_minimum_x else upper
|
708
|
+
first = True
|
709
|
+
while lower <= x <= upper:
|
710
|
+
segments = self._get_pixel_chains(x, False)
|
711
|
+
self._consume_pixel_chains(segments, x, False)
|
712
|
+
if segments:
|
713
|
+
if dy > 0:
|
714
|
+
# from top to bottom
|
715
|
+
idx = 0
|
716
|
+
start = 0
|
717
|
+
end = 1
|
718
|
+
edge_start = -0.5
|
719
|
+
edge_end = 0.5
|
720
|
+
else:
|
721
|
+
idx = len(segments) - 1
|
722
|
+
end = 0
|
723
|
+
start = 1
|
724
|
+
edge_start = 0.5
|
725
|
+
edge_end = -0.5
|
726
|
+
# Goto next column, but make sure we end up outside our chain
|
727
|
+
# We consider as well the overscan value
|
728
|
+
overscan_top = 0 if dy >= 0 else self.overscan
|
729
|
+
overscan_bottom = 0 if dy <= 0 else self.overscan
|
730
|
+
if not first and (segments[0][0] - overscan_top <= last_y <= segments[-1][1] + overscan_bottom):
|
731
|
+
# inside the chain!
|
732
|
+
# So lets move a bit to the side
|
733
|
+
if dy > 0:
|
734
|
+
if self.bidirectional:
|
735
|
+
# Previous was sweep from right to left, so we go beyond first point
|
736
|
+
last_y = segments[0][0] - overscan_top - 1
|
737
|
+
else:
|
738
|
+
# We go beyond last point
|
739
|
+
last_y = segments[-1][1] + overscan_bottom + 1
|
740
|
+
else:
|
741
|
+
# Previous was sweep from left to right, so we go beyond last point
|
742
|
+
last_y = segments[-1][1] + overscan_bottom + 1
|
743
|
+
last_x = x - dx
|
744
|
+
yield last_x, last_y, 0
|
745
|
+
last_x = x
|
746
|
+
yield last_x, last_y, 0
|
747
|
+
while 0 <= idx < len(segments):
|
748
|
+
sy = segments[idx][start] + edge_start
|
749
|
+
ey = segments[idx][end] + edge_end
|
750
|
+
on = segments[idx][2]
|
751
|
+
if last_y != sy:
|
752
|
+
yield last_x, sy, 0
|
753
|
+
last_y = ey
|
754
|
+
yield last_x, last_y, on
|
755
|
+
idx += dy
|
756
|
+
if self.overscan:
|
757
|
+
last_y += dy * self.overscan
|
758
|
+
yield last_x, last_y, 0
|
759
|
+
if not unidirectional:
|
760
|
+
dy = -dy
|
761
|
+
first = False
|
762
|
+
else:
|
763
|
+
# Just climb the line, and don't change directions
|
764
|
+
last_x = x
|
765
|
+
yield last_x, last_y, 0
|
766
|
+
|
767
|
+
x += dx
|
768
|
+
|
769
|
+
def _plot_horizontal(self):
|
770
|
+
"""
|
771
|
+
This code is horizontal rastering.
|
772
|
+
We are looking first for all consecutive pixel chains with the same pixel value
|
773
|
+
Then we loop through the segments and yield the 'end edge' of the 'last' pixel
|
774
|
+
'end edge' and 'last' are dependent on the sweep direction.
|
775
|
+
There is one peculiarity though that is required for K40 lasers:
|
776
|
+
a) We may only move from one yielded (x,y,on) tuple in a pure horizontal or pure
|
777
|
+
vertical fashion (we could as well go perfectly diagonal but we are not using
|
778
|
+
this feature). So at the end of one sweepline we need to change to the
|
779
|
+
next scanline by going directly up/down/left/right and then move to the first
|
780
|
+
relevant pixel.
|
781
|
+
b) we need to take care that we are not landing on the same pixel twice. So we move
|
782
|
+
outside the new chain to avoid this
|
783
|
+
"""
|
784
|
+
unidirectional = not self.bidirectional
|
785
|
+
|
786
|
+
dx = 1 if self.start_minimum_x else -1
|
787
|
+
dy = 1 if self.start_minimum_y else -1
|
788
|
+
last_x = self.initial_x
|
789
|
+
last_y = self.initial_y
|
790
|
+
lower = min(self.initial_y, self.final_y)
|
791
|
+
upper = max(self.initial_y, self.final_y)
|
792
|
+
y = lower if self.start_minimum_y else upper
|
793
|
+
first = True
|
794
|
+
while lower <= y <= upper:
|
795
|
+
segments = self._get_pixel_chains(y, True)
|
796
|
+
self._consume_pixel_chains(segments, y, True)
|
797
|
+
if segments:
|
798
|
+
if dx > 0:
|
799
|
+
# from left to right
|
800
|
+
idx = 0
|
801
|
+
start = 0
|
802
|
+
end = 1
|
803
|
+
edge_start = -0.5
|
804
|
+
edge_end = 0.5
|
805
|
+
else:
|
806
|
+
idx = len(segments) - 1
|
807
|
+
end = 0
|
808
|
+
start = 1
|
809
|
+
edge_start = 0.5
|
810
|
+
edge_end = -0.5
|
811
|
+
if last_x is None:
|
812
|
+
last_x = segments[idx][start] + edge_start
|
813
|
+
# Goto next line, but make sure we end up outside our chain
|
814
|
+
# We consider as well the overscan value
|
815
|
+
overscan_left = 0 if dx >= 0 else self.overscan
|
816
|
+
overscan_right = 0 if dx <= 0 else self.overscan
|
817
|
+
if not first and (segments[0][0] - overscan_left <= last_x <= segments[-1][1] + overscan_right):
|
818
|
+
# inside the chain!
|
819
|
+
# So lets move a bit to the side
|
820
|
+
if dx > 0:
|
821
|
+
if self.bidirectional:
|
822
|
+
# Previous was sweep from right to left, so we go beyond first point
|
823
|
+
last_x = segments[0][0] - overscan_left - 1
|
824
|
+
else:
|
825
|
+
# We go beyond last point
|
826
|
+
last_x = segments[-1][1] + overscan_right + 1
|
827
|
+
else:
|
828
|
+
# Previous was sweep from left to right, so we go beyond last point
|
829
|
+
last_x = segments[-1][1] + overscan_right + 1
|
830
|
+
yield last_x, y - dy, 0
|
831
|
+
last_y = y
|
832
|
+
yield last_x, last_y, 0
|
833
|
+
while 0 <= idx < len(segments):
|
834
|
+
sx = segments[idx][start] + edge_start
|
835
|
+
ex = segments[idx][end] + edge_end
|
836
|
+
on = segments[idx][2]
|
837
|
+
if last_x != sx:
|
838
|
+
yield sx, last_y, 0
|
839
|
+
last_x = ex
|
840
|
+
yield last_x, last_y, on
|
841
|
+
idx += dx
|
842
|
+
if self.overscan:
|
843
|
+
last_x += dx * self.overscan
|
844
|
+
yield last_x, last_y, 0
|
845
|
+
if not unidirectional:
|
846
|
+
dx = -dx
|
847
|
+
first = False
|
848
|
+
else:
|
849
|
+
# Just climb the line, and don't change directions
|
850
|
+
last_y = y
|
851
|
+
yield last_x, last_y, 0
|
852
|
+
y += dy
|
853
|
+
|
854
|
+
# Legacy code for the m2nano - yes this has deficits for low dpi but it seems
|
855
|
+
# to be finetuned to the needs of the m2nano controller.
|
856
|
+
# This will be called if the appropriate device setting is in place
|
857
|
+
def _legacy_plot_vertical(self):
|
858
|
+
"""
|
859
|
+
This code is for vertical rastering.
|
860
|
+
|
861
|
+
@return:
|
862
|
+
"""
|
863
|
+
width = self.width
|
864
|
+
unidirectional = not self.bidirectional
|
865
|
+
skip_pixel = self.skip_pixel
|
866
|
+
|
867
|
+
x, y = self.initial_position()
|
868
|
+
dx = 1 if self.start_minimum_x else -1
|
869
|
+
dy = 1 if self.start_minimum_y else -1
|
870
|
+
|
871
|
+
yield x, y, 0
|
872
|
+
while 0 <= x < width:
|
873
|
+
lower_bound = self.topmost_not_equal(x)
|
874
|
+
if lower_bound is None:
|
875
|
+
x += dx
|
876
|
+
yield x, y, 0
|
877
|
+
continue
|
878
|
+
upper_bound = self.bottommost_not_equal(x)
|
879
|
+
traveling_bottom = self.start_minimum_y if unidirectional else dy >= 0
|
880
|
+
next_traveling_bottom = self.start_minimum_y if unidirectional else dy <= 0
|
881
|
+
|
882
|
+
next_x, next_y = self.calculate_next_vertical_pixel(
|
883
|
+
x + dx, dx, topmost_pixel=next_traveling_bottom
|
884
|
+
)
|
885
|
+
if next_y is not None:
|
886
|
+
# If we have a next scanline, we must end after the last pixel of that scanline too.
|
887
|
+
upper_bound = max(next_y, upper_bound) + self.overscan
|
888
|
+
lower_bound = min(next_y, lower_bound) - self.overscan
|
889
|
+
pixel_chain = []
|
890
|
+
last_y = lower_bound if traveling_bottom else upper_bound
|
891
|
+
if traveling_bottom:
|
892
|
+
while y < upper_bound:
|
893
|
+
try:
|
894
|
+
pixel = self.px(x, y)
|
895
|
+
except IndexError:
|
896
|
+
pixel = 0
|
897
|
+
y = self.nextcolor_bottom(x, y, upper_bound)
|
898
|
+
y = min(y, upper_bound)
|
899
|
+
if pixel == skip_pixel:
|
900
|
+
yield x, y, 0
|
901
|
+
else:
|
902
|
+
yield x, y, pixel
|
903
|
+
pixel_chain.append([last_y, y, pixel])
|
904
|
+
last_y = y + 1
|
905
|
+
else:
|
906
|
+
while lower_bound < y:
|
907
|
+
try:
|
908
|
+
pixel = self.px(x, y)
|
909
|
+
except IndexError:
|
910
|
+
pixel = 0
|
911
|
+
y = self.nextcolor_top(x, y, lower_bound)
|
912
|
+
y = max(y, lower_bound)
|
913
|
+
if pixel == skip_pixel:
|
914
|
+
yield x, y, 0
|
915
|
+
else:
|
916
|
+
yield x, y, pixel
|
917
|
+
pixel_chain.append([y, last_y, pixel])
|
918
|
+
last_y = y - 1
|
919
|
+
if pixel_chain:
|
920
|
+
self._consume_pixel_chains(pixel_chain, x, False)
|
921
|
+
if next_y is None:
|
922
|
+
# remaining image is blank, we stop right here.
|
923
|
+
return
|
924
|
+
yield next_x, y, 0
|
925
|
+
if y != next_y:
|
926
|
+
yield next_x, next_y, 0
|
927
|
+
x = next_x
|
928
|
+
y = next_y
|
929
|
+
dy = -dy
|
930
|
+
|
931
|
+
def _legacy_plot_horizontal(self):
|
932
|
+
"""
|
933
|
+
This code is horizontal rastering.
|
934
|
+
|
935
|
+
@return:
|
936
|
+
"""
|
937
|
+
height = self.height
|
938
|
+
unidirectional = not self.bidirectional
|
939
|
+
skip_pixel = self.skip_pixel
|
940
|
+
|
941
|
+
x, y = self.initial_position()
|
942
|
+
dx = 1 if self.start_minimum_x else -1
|
943
|
+
dy = 1 if self.start_minimum_y else -1
|
944
|
+
yield x, y, 0
|
945
|
+
while 0 <= y < height:
|
946
|
+
lower_bound = self.leftmost_not_equal(y)
|
947
|
+
if lower_bound is None:
|
948
|
+
y += dy
|
949
|
+
yield x, y, 0
|
950
|
+
continue
|
951
|
+
upper_bound = self.rightmost_not_equal(y)
|
952
|
+
traveling_right = self.start_minimum_x if unidirectional else dx >= 0
|
953
|
+
next_traveling_right = self.start_minimum_x if unidirectional else dx <= 0
|
954
|
+
|
955
|
+
next_x, next_y = self.calculate_next_horizontal_pixel(
|
956
|
+
y + dy, dy, leftmost_pixel=next_traveling_right
|
957
|
+
)
|
958
|
+
if next_x is not None:
|
959
|
+
# If we have a next scanline, we must end after the last pixel of that scanline too.
|
960
|
+
upper_bound = max(next_x, upper_bound) + self.overscan
|
961
|
+
lower_bound = min(next_x, lower_bound) - self.overscan
|
962
|
+
pixel_chain = []
|
963
|
+
last_x = lower_bound if traveling_right else upper_bound
|
964
|
+
if traveling_right:
|
965
|
+
while x < upper_bound:
|
966
|
+
try:
|
967
|
+
pixel = self.px(x, y)
|
968
|
+
except IndexError:
|
969
|
+
pixel = 0
|
970
|
+
x = self.nextcolor_right(x, y, upper_bound)
|
971
|
+
x = min(x, upper_bound)
|
972
|
+
if pixel == skip_pixel:
|
973
|
+
yield x, y, 0
|
974
|
+
else:
|
975
|
+
yield x, y, pixel
|
976
|
+
pixel_chain.append([last_x, x, pixel])
|
977
|
+
last_x = x + 1
|
978
|
+
else:
|
979
|
+
while lower_bound < x:
|
980
|
+
try:
|
981
|
+
pixel = self.px(x, y)
|
982
|
+
except IndexError:
|
983
|
+
pixel = 0
|
984
|
+
x = self.nextcolor_left(x, y, lower_bound)
|
985
|
+
x = max(x, lower_bound)
|
986
|
+
if pixel == skip_pixel:
|
987
|
+
yield x, y, 0
|
988
|
+
else:
|
989
|
+
yield x, y, pixel
|
990
|
+
pixel_chain.append([x, last_x, pixel])
|
991
|
+
last_x = x - 1
|
992
|
+
if pixel_chain:
|
993
|
+
self._consume_pixel_chains(pixel_chain, y, True)
|
994
|
+
if next_y is None:
|
995
|
+
# remaining image is blank, we stop right here.
|
996
|
+
return
|
997
|
+
yield x, next_y, 0
|
998
|
+
if x != next_x:
|
999
|
+
yield next_x, next_y, 0
|
1000
|
+
x = next_x
|
1001
|
+
y = next_y
|
1002
|
+
dx = -dx
|
1003
|
+
|
1004
|
+
def _plot_greedy_neighbour(self, horizontal: bool = True):
|
1005
|
+
"""
|
1006
|
+
Distance Matrix Function: The distance_matrix function calculates the squared distances from the
|
1007
|
+
current point to all other points, applying a penalty to the y-distances to prefer movements in the x-direction.
|
1008
|
+
|
1009
|
+
Initialization: We initialize the visited array to keep track of visited segments, the path list
|
1010
|
+
to store the order of segments along with the relevant point (start or end),
|
1011
|
+
and the current_point as the starting point.
|
1012
|
+
|
1013
|
+
Sliding Window: We use a sliding window to limit the number of segments we need to consider at each step.
|
1014
|
+
The window size is controlled by the window_size parameter and is applied to both x and y coordinates.
|
1015
|
+
|
1016
|
+
Main Loop: In the main loop, we calculate the distances from the current point to all unvisited points
|
1017
|
+
within the sliding window using the distance_matrix function.
|
1018
|
+
We then use np.argmin to find the index of the smallest distance.
|
1019
|
+
We update the current_point to the next point and mark the segment as visited.
|
1020
|
+
The path list is updated with the segment index and whether the start or end point is relevant.
|
1021
|
+
"""
|
1022
|
+
|
1023
|
+
def walk_segments(segments, horizontal=True, xy_penalty=1, width=1, height=1):
|
1024
|
+
n:int = len(segments)
|
1025
|
+
visited = np.zeros(n, dtype=bool)
|
1026
|
+
path = []
|
1027
|
+
window_size = 10
|
1028
|
+
current_point = np.array(segments[0][0], dtype=float)
|
1029
|
+
segment_points = np.array([point for segment in segments for point in segment], dtype=float)
|
1030
|
+
mask = ~visited.repeat(2)
|
1031
|
+
while len(path) < n:
|
1032
|
+
# Find the range of segments within the x- and y-window
|
1033
|
+
x_min = current_point[0] - window_size
|
1034
|
+
x_max = current_point[0] + window_size
|
1035
|
+
y_min = current_point[1] - window_size
|
1036
|
+
y_max = current_point[1] + window_size
|
1037
|
+
unvisited_indices = np.where(
|
1038
|
+
(segment_points[:, 0] >= x_min) &
|
1039
|
+
(segment_points[:, 0] <= x_max) &
|
1040
|
+
(segment_points[:, 1] >= y_min) &
|
1041
|
+
(segment_points[:, 1] <= y_max) &
|
1042
|
+
mask
|
1043
|
+
)[0]
|
1044
|
+
if len(unvisited_indices) == 0:
|
1045
|
+
# If no segments are within the window, expand the window
|
1046
|
+
window_size *= 2
|
1047
|
+
# print (f"Did not find points: now window: {window_size}")
|
1048
|
+
if window_size <= 2* height or window_size <= 2 * width: # Safety belt
|
1049
|
+
continue
|
1050
|
+
|
1051
|
+
unvisited_points = segment_points[unvisited_indices]
|
1052
|
+
|
1053
|
+
# distances = distance_matrix(unvisited_points, current_point, y_penalty)
|
1054
|
+
diff = unvisited_points - current_point
|
1055
|
+
if horizontal:
|
1056
|
+
diff[:, 1] *= xy_penalty # Apply penalty to y-distances
|
1057
|
+
else:
|
1058
|
+
diff[:, 0] *= xy_penalty # Apply penalty to x-distances
|
1059
|
+
|
1060
|
+
distances = np.sum(diff ** 2, axis=1) # Return squared distances
|
1061
|
+
|
1062
|
+
min_distance_idx = np.argmin(distances)
|
1063
|
+
next_segment = unvisited_indices[min_distance_idx] // 2
|
1064
|
+
|
1065
|
+
if not visited[next_segment]:
|
1066
|
+
visited[next_segment] = True
|
1067
|
+
# mask = ~visited.repeat(2)
|
1068
|
+
mask[2 * next_segment] = False
|
1069
|
+
mask[2 * next_segment + 1] = False
|
1070
|
+
if min_distance_idx % 2 == 0:
|
1071
|
+
path.append((next_segment, 'end'))
|
1072
|
+
current_point = segment_points[next_segment * 2 + 1] # Move to the other endpoint
|
1073
|
+
else:
|
1074
|
+
path.append((next_segment, 'start'))
|
1075
|
+
current_point = segment_points[next_segment * 2] # Move to the other endpoint
|
1076
|
+
window_size = 10 # Reset window size
|
1077
|
+
|
1078
|
+
return path
|
1079
|
+
|
1080
|
+
t0 = perf_counter()
|
1081
|
+
# An experimental routine
|
1082
|
+
if horizontal:
|
1083
|
+
dy = 1 if self.start_minimum_y else -1
|
1084
|
+
lower = min(self.initial_y, self.final_y)
|
1085
|
+
upper = max(self.initial_y, self.final_y)
|
1086
|
+
y = lower if self.start_minimum_y else upper
|
1087
|
+
else:
|
1088
|
+
dx = 1 if self.start_minimum_x else -1
|
1089
|
+
lower = min(self.initial_x, self.final_x)
|
1090
|
+
upper = max(self.initial_x, self.final_x)
|
1091
|
+
x = lower if self.start_minimum_x else upper
|
1092
|
+
|
1093
|
+
line_parts = []
|
1094
|
+
on_parts = []
|
1095
|
+
if self.debug_level > 2:
|
1096
|
+
print (f"{'horizontal' if horizontal else 'Vertical'} for {self.width}x{self.height} image. {'y' if horizontal else 'x'} from {lower} to {upper}")
|
1097
|
+
if horizontal:
|
1098
|
+
while lower <= y <= upper:
|
1099
|
+
segments = self._get_pixel_chains(y, True)
|
1100
|
+
self._consume_pixel_chains(segments, y, True)
|
1101
|
+
for seg in segments:
|
1102
|
+
# Append (xstart, y), (xend, y), on
|
1103
|
+
line_parts.append( ( (seg[0], y), (seg[1], y) ) )
|
1104
|
+
on_parts.append(seg[2])
|
1105
|
+
y += dy
|
1106
|
+
else:
|
1107
|
+
while lower <= x <= upper:
|
1108
|
+
segments = self._get_pixel_chains(x, False)
|
1109
|
+
self._consume_pixel_chains(segments, x, False)
|
1110
|
+
for seg in segments:
|
1111
|
+
# Append (xstart, y), (xend, y), on
|
1112
|
+
line_parts.append( ( (x, seg[0]), (x, seg[1]) ) )
|
1113
|
+
on_parts.append(seg[2])
|
1114
|
+
x += dx
|
1115
|
+
if self.debug_level > 2:
|
1116
|
+
print (f"Created {len(line_parts)} segments")
|
1117
|
+
t1 = perf_counter()
|
1118
|
+
penalty = 3 if self.special.get("gantry", False) else 1
|
1119
|
+
path = walk_segments(line_parts, horizontal=horizontal, xy_penalty=penalty, width=self.width, height=self.height)
|
1120
|
+
# print("Order of segments:", path)
|
1121
|
+
t2 = perf_counter()
|
1122
|
+
if horizontal:
|
1123
|
+
last_x = self.initial_x
|
1124
|
+
last_y = lower
|
1125
|
+
else:
|
1126
|
+
last_x = lower
|
1127
|
+
last_y = self.initial_y
|
1128
|
+
for idx, mode in path:
|
1129
|
+
if mode == "end":
|
1130
|
+
# end was closer
|
1131
|
+
(ex, ey), (sx, sy) = line_parts[idx]
|
1132
|
+
else:
|
1133
|
+
(sx, sy), (ex, ey) = line_parts[idx]
|
1134
|
+
on = on_parts[idx]
|
1135
|
+
if horizontal:
|
1136
|
+
dx = ex - sx
|
1137
|
+
if dx >= 0:
|
1138
|
+
# from left to right
|
1139
|
+
edge_start = -0.5
|
1140
|
+
edge_end = 0.5
|
1141
|
+
else:
|
1142
|
+
edge_start = 0.5
|
1143
|
+
edge_end = -0.5
|
1144
|
+
sx += edge_start
|
1145
|
+
ex += edge_end
|
1146
|
+
if sy != last_y:
|
1147
|
+
last_y = sy
|
1148
|
+
yield last_x, last_y, 0
|
1149
|
+
if last_x != sx:
|
1150
|
+
yield sx, last_y, 0
|
1151
|
+
else:
|
1152
|
+
dy = ey - sy
|
1153
|
+
if dy >= 0:
|
1154
|
+
# from left to right
|
1155
|
+
edge_start = -0.5
|
1156
|
+
edge_end = 0.5
|
1157
|
+
else:
|
1158
|
+
edge_start = 0.5
|
1159
|
+
edge_end = -0.5
|
1160
|
+
sy += edge_start
|
1161
|
+
ey += edge_end
|
1162
|
+
if sx != last_x:
|
1163
|
+
last_x = sx
|
1164
|
+
yield last_x, last_y, 0
|
1165
|
+
if last_y != sy:
|
1166
|
+
last_y = sy
|
1167
|
+
yield sx, sy, 0
|
1168
|
+
|
1169
|
+
yield ex, ey, on
|
1170
|
+
last_x = ex
|
1171
|
+
last_y = ey
|
1172
|
+
t3 = perf_counter()
|
1173
|
+
if self.debug_level > 1:
|
1174
|
+
print (f"Overall time for {'horizontal' if horizontal else 'vertical'} consumption: {t3-t0:.2f}s - created: {len(line_parts)} segments")
|
1175
|
+
print (f"Computation: {t2-t0:.2f}s - Chain creation:{t1 - t0:.2f}s, Walk: {t2 - t1:.2f}s")
|
1176
|
+
self.final_x = last_x
|
1177
|
+
self.final_y = last_y
|
1178
|
+
|
1179
|
+
def _plot_spiral(self):
|
1180
|
+
|
1181
|
+
|
1182
|
+
rows = self.height
|
1183
|
+
cols = self.width
|
1184
|
+
center_row, center_col = rows // 2, cols // 2
|
1185
|
+
self.initial_x = center_col
|
1186
|
+
self.initial_y = center_row
|
1187
|
+
|
1188
|
+
directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] # Right, Down, Left, Up
|
1189
|
+
edges = [(0.5, 0), (0, 0.5), (-0.5, 0), (0, -0.5)]
|
1190
|
+
direction_index = 0
|
1191
|
+
steps = 1
|
1192
|
+
|
1193
|
+
row, col = center_row, center_col
|
1194
|
+
# is the very first pixel an on?
|
1195
|
+
last_x = col
|
1196
|
+
last_y = row
|
1197
|
+
count = 1
|
1198
|
+
pixel = self.px(col, row)
|
1199
|
+
if pixel == self.skip_pixel:
|
1200
|
+
pixel = 0
|
1201
|
+
last_pixel = pixel
|
1202
|
+
if pixel:
|
1203
|
+
yield col - 0.5, row, 0
|
1204
|
+
last_x = col + 0.5
|
1205
|
+
yield last_x, row, pixel
|
1206
|
+
while count < rows * cols:
|
1207
|
+
for _ in range(2):
|
1208
|
+
segments = []
|
1209
|
+
dx, dy = edges[direction_index]
|
1210
|
+
edge_start_x = -dx
|
1211
|
+
edge_start_y = -dy
|
1212
|
+
edge_end_x = dx
|
1213
|
+
edge_end_y = dy
|
1214
|
+
# msg = f"[({col}, {row}) - {steps}] "
|
1215
|
+
for _ in range(steps):
|
1216
|
+
row += directions[direction_index][1]
|
1217
|
+
col += directions[direction_index][0]
|
1218
|
+
if 0 <= row < rows and 0 <= col < cols:
|
1219
|
+
pixel = self.px(col, row)
|
1220
|
+
on = 0 if pixel == self.skip_pixel else pixel
|
1221
|
+
# msg = f"{msg} {'X' if on else '.'}"
|
1222
|
+
if on:
|
1223
|
+
if on == last_pixel and len(segments):
|
1224
|
+
segments[-1][1] = (col, row)
|
1225
|
+
else:
|
1226
|
+
segments.append ([(col, row), (col, row), on])
|
1227
|
+
|
1228
|
+
last_pixel = on
|
1229
|
+
count += 1
|
1230
|
+
if count == rows * cols:
|
1231
|
+
break
|
1232
|
+
# Deal with segments
|
1233
|
+
# print (msg)
|
1234
|
+
# print (segments)
|
1235
|
+
for (sx, sy), (ex, ey), pixel in segments:
|
1236
|
+
sx += edge_start_x
|
1237
|
+
sy += edge_start_y
|
1238
|
+
ex += edge_end_x
|
1239
|
+
ey += edge_end_y
|
1240
|
+
if last_y != sy:
|
1241
|
+
yield last_x, sy, 0
|
1242
|
+
last_y = sy
|
1243
|
+
if last_x != sx:
|
1244
|
+
yield sx, last_y, 0
|
1245
|
+
last_x = ex
|
1246
|
+
last_y = ey
|
1247
|
+
yield last_x, last_y, pixel
|
1248
|
+
self.final_x, self.final_y = last_x, last_y
|
1249
|
+
# Now we need to empty overlapping pixels...
|
1250
|
+
if self.overlap > 0:
|
1251
|
+
BLANK = 255
|
1252
|
+
for (start_x, start_y), (end_x, end_y), on in segments:
|
1253
|
+
sx = min(start_x, end_x)
|
1254
|
+
ex = max(start_x, end_x)
|
1255
|
+
sy = min(start_y, end_y)
|
1256
|
+
ey = max(start_y, end_y)
|
1257
|
+
|
1258
|
+
if direction_index in (0, 2): # horizontal
|
1259
|
+
for y_idx in range(-self.overlap, self.overlap + 1):
|
1260
|
+
ny = sy + y_idx
|
1261
|
+
for nx in range(sx, ex + 1):
|
1262
|
+
if 0 <= nx < self.width and 0 <= ny < self.height:
|
1263
|
+
self.data[nx, ny] = BLANK
|
1264
|
+
else:
|
1265
|
+
for x_idx in range(-self.overlap, self.overlap + 1):
|
1266
|
+
nx = sx + x_idx
|
1267
|
+
for ny in range(sy, ey + 1):
|
1268
|
+
if 0 <= nx < self.width and 0 <= ny < self.height:
|
1269
|
+
self.data[nx, ny] = BLANK
|
1270
|
+
|
1271
|
+
|
1272
|
+
direction_index = (direction_index + 1) % 4
|
1273
|
+
steps += 1
|
1274
|
+
|
1275
|
+
|
1276
|
+
def _plot_crossover(self):
|
1277
|
+
"""
|
1278
|
+
This algorithm scans through the image looking for the row or the column with the most pixels.
|
1279
|
+
It will hand back this information together with a precompiled list of
|
1280
|
+
state changes (start, end, pixel), ie a non-blank pixel with a different on value
|
1281
|
+
than the previous pixel
|
1282
|
+
It will then clean the row / column and start again.
|
1283
|
+
This happens until no more non-empty rows / cols can be found
|
1284
|
+
|
1285
|
+
The receiving routine takes these values, orders them according to
|
1286
|
+
type (row/col) and row/col number and generates plot instructions
|
1287
|
+
that will be yielded.
|
1288
|
+
|
1289
|
+
Yields:
|
1290
|
+
list of tuples with (x, y, on)
|
1291
|
+
"""
|
1292
|
+
ROW=0
|
1293
|
+
COL=1
|
1294
|
+
|
1295
|
+
def process_image(image):
|
1296
|
+
# We will modify the image to keep track of deleted rows and columns
|
1297
|
+
# Get the dimensions of the image
|
1298
|
+
rows, cols = image.shape
|
1299
|
+
# We prefer cols (which is the x-axis and that is normally
|
1300
|
+
# slightly advantageous for gantry lasers)
|
1301
|
+
if self.special.get("gantry", False):
|
1302
|
+
colfactor = 1.0
|
1303
|
+
rowfactor = 0.8
|
1304
|
+
else:
|
1305
|
+
colfactor = 1.0
|
1306
|
+
rowfactor = 1.0
|
1307
|
+
|
1308
|
+
# Initialize a list to store the results
|
1309
|
+
results = []
|
1310
|
+
|
1311
|
+
# Iterate through the matrix, we cover all rows and cols
|
1312
|
+
colidx = 0
|
1313
|
+
rowidx = 0
|
1314
|
+
covered_row = [None] * rows
|
1315
|
+
covered_col = [None] * cols
|
1316
|
+
stored_row = np.zeros(rows)
|
1317
|
+
stored_col = np.zeros(cols)
|
1318
|
+
stored_row_len = np.zeros(rows)
|
1319
|
+
stored_col_len = np.zeros(cols)
|
1320
|
+
recalc_row = True
|
1321
|
+
recalc_col = True
|
1322
|
+
while True:
|
1323
|
+
if recalc_col:
|
1324
|
+
for i in range(cols):
|
1325
|
+
col_len = cols
|
1326
|
+
if covered_col[i] is None:
|
1327
|
+
nonzero_indices = np.nonzero(image[:, i])[0]
|
1328
|
+
count = len(nonzero_indices)
|
1329
|
+
if count == 0:
|
1330
|
+
covered_col[i] = True
|
1331
|
+
else:
|
1332
|
+
col_len = nonzero_indices[-1] - nonzero_indices[0] + 1
|
1333
|
+
covered_col[i] = False
|
1334
|
+
stored_col[i] = count
|
1335
|
+
stored_col_len[i] = col_len
|
1336
|
+
if recalc_row:
|
1337
|
+
for i in range(rows):
|
1338
|
+
row_len = rows
|
1339
|
+
if covered_row[i] is None:
|
1340
|
+
nonzero_indices = np.nonzero(image[i, :])[0]
|
1341
|
+
count = len(nonzero_indices)
|
1342
|
+
if count == 0:
|
1343
|
+
covered_row[i] = True
|
1344
|
+
else:
|
1345
|
+
row_len = nonzero_indices[-1] - nonzero_indices[0] + 1
|
1346
|
+
covered_row[i] = False
|
1347
|
+
stored_row[i] = count
|
1348
|
+
stored_row_len[i] = row_len
|
1349
|
+
|
1350
|
+
colidx = np.argmax(stored_col)
|
1351
|
+
rowidx = np.argmax(stored_row)
|
1352
|
+
|
1353
|
+
col_count = stored_col[colidx]
|
1354
|
+
col_len = stored_col_len[colidx]
|
1355
|
+
row_count = stored_row[rowidx]
|
1356
|
+
row_len = stored_row_len[rowidx]
|
1357
|
+
|
1358
|
+
if row_count == col_count == 0:
|
1359
|
+
break
|
1360
|
+
# Determine whether there are more pixels in the row or column
|
1361
|
+
|
1362
|
+
row_ratio = row_count * row_count / row_len * rowfactor
|
1363
|
+
col_ratio = col_count * col_count / col_len * colfactor
|
1364
|
+
# print (f"Col #{rowidx}: {int(row_count):3d} pixel over {int(row_len):3d} length, ratio: {row_ratio:.3f} {'winner' if row_ratio >= col_ratio else 'loser'}")
|
1365
|
+
# print (f"Row #{colidx}: {int(col_count):3d} pixel over {int(col_len):3d} length, ratio: {col_ratio:.3f} {'winner' if row_ratio < col_ratio else 'loser'}")
|
1366
|
+
# if row_count >= col_count:
|
1367
|
+
if row_ratio >= col_ratio:
|
1368
|
+
last_pixel = None
|
1369
|
+
segments = []
|
1370
|
+
# msg = ""
|
1371
|
+
for idx in range(cols):
|
1372
|
+
on = image[rowidx, idx]
|
1373
|
+
# msg = f"{msg}{'X' if on else '.'}"
|
1374
|
+
if on:
|
1375
|
+
if not covered_col[idx]:
|
1376
|
+
covered_col[idx] = None # needs recalc
|
1377
|
+
if on == last_pixel:
|
1378
|
+
segments[-1][1] = idx
|
1379
|
+
else:
|
1380
|
+
segments.append ([idx, idx, on])
|
1381
|
+
last_pixel = on
|
1382
|
+
results.append((COL, rowidx, segments)) # Intentionally so, as the numpy array has x and y exchanged
|
1383
|
+
# print (f"Col #{rowidx}: {msg} -> {segments}")
|
1384
|
+
|
1385
|
+
# Clear the column
|
1386
|
+
image[rowidx,:] = 0
|
1387
|
+
covered_row[rowidx] = True
|
1388
|
+
stored_row[rowidx] = 0
|
1389
|
+
for rc in range(self.overlap):
|
1390
|
+
r = rowidx - rc
|
1391
|
+
if 0 <= r < rows:
|
1392
|
+
image[r,:] = 0
|
1393
|
+
covered_row[r] = True
|
1394
|
+
stored_row[r] = 0
|
1395
|
+
r = rowidx + rc
|
1396
|
+
if 0 <= r < rows:
|
1397
|
+
image[r,:] = 0
|
1398
|
+
covered_row[r] = True
|
1399
|
+
stored_row[r] = 0
|
1400
|
+
recalc_col = True
|
1401
|
+
else:
|
1402
|
+
last_pixel = None
|
1403
|
+
segments = []
|
1404
|
+
# msg = ""
|
1405
|
+
for idx in range(rows):
|
1406
|
+
on = image[idx, colidx]
|
1407
|
+
# msg = f"{msg}{'X' if on else '.'}"
|
1408
|
+
if on:
|
1409
|
+
if not covered_row[idx]:
|
1410
|
+
covered_row[idx] = None # needs recalc
|
1411
|
+
if on == last_pixel:
|
1412
|
+
segments[-1][1] = idx
|
1413
|
+
else:
|
1414
|
+
segments.append ([idx, idx, on])
|
1415
|
+
last_pixel = on
|
1416
|
+
results.append((ROW, colidx, segments))
|
1417
|
+
# print (f"Row #{colidx}: {msg} -> {segments}")
|
1418
|
+
# Clear the row
|
1419
|
+
image[:, colidx] = 0
|
1420
|
+
covered_col[colidx] = True
|
1421
|
+
stored_col[colidx] = 0
|
1422
|
+
for rc in range(self.overlap):
|
1423
|
+
r = colidx - rc
|
1424
|
+
if 0 <= r < cols:
|
1425
|
+
image[:, r] = 0
|
1426
|
+
covered_col[r] = True
|
1427
|
+
stored_col[r] = 0
|
1428
|
+
r = rowidx + rc
|
1429
|
+
if 0 <= r < cols:
|
1430
|
+
image[:, r] = 0
|
1431
|
+
covered_col[r] = True
|
1432
|
+
stored_col[r] = 0
|
1433
|
+
recalc_row = True
|
1434
|
+
if self.debug_level > 1:
|
1435
|
+
for cidx in range(cols):
|
1436
|
+
msg = ""
|
1437
|
+
for ridx in range(rows):
|
1438
|
+
on = image[ridx, cidx]
|
1439
|
+
msg = f"{msg}{'X' if on else '.'}"
|
1440
|
+
print (f"{cidx:3d}: {msg}")
|
1441
|
+
|
1442
|
+
return results
|
1443
|
+
|
1444
|
+
t0 = perf_counter()
|
1445
|
+
# initialize the matrix
|
1446
|
+
image = np.empty((self.width, self.height))
|
1447
|
+
# Apply filter and eliminate skip_pixel
|
1448
|
+
for x in range(self.width):
|
1449
|
+
for y in range(self.height):
|
1450
|
+
px = self.px(x, y)
|
1451
|
+
if px==self.skip_pixel:
|
1452
|
+
px = 0
|
1453
|
+
image[x, y] = px
|
1454
|
+
t1 = perf_counter()
|
1455
|
+
results = process_image(image)
|
1456
|
+
results.sort()
|
1457
|
+
t2 = perf_counter()
|
1458
|
+
dx = +1
|
1459
|
+
dy = +1
|
1460
|
+
first_x = 0
|
1461
|
+
first_y = 0
|
1462
|
+
last_x = self.initial_x
|
1463
|
+
last_y = self.initial_y
|
1464
|
+
yield last_x, last_y, 0
|
1465
|
+
for mode, xy, segments in results:
|
1466
|
+
# eliminate data and swap direction
|
1467
|
+
if self.special.get("mode_filter", "") == "ROW" and mode != ROW:
|
1468
|
+
continue
|
1469
|
+
if self.special.get("mode_filter", "") == "COL" and mode != COL:
|
1470
|
+
continue
|
1471
|
+
|
1472
|
+
if not segments:
|
1473
|
+
continue
|
1474
|
+
# NB: Axis change indication is no longer required,
|
1475
|
+
# the m2nano does not support it, the other devices do not need it...
|
1476
|
+
if mode == ROW:
|
1477
|
+
if xy != last_y:
|
1478
|
+
last_y = xy
|
1479
|
+
yield last_x, last_y, 0
|
1480
|
+
if dx > 0:
|
1481
|
+
# from left to right
|
1482
|
+
idx = 0
|
1483
|
+
start = 0
|
1484
|
+
end = 1
|
1485
|
+
edge_start = -0.5
|
1486
|
+
edge_end = 0.5
|
1487
|
+
else:
|
1488
|
+
idx = len(segments) - 1
|
1489
|
+
end = 0
|
1490
|
+
start = 1
|
1491
|
+
edge_start = 0.5
|
1492
|
+
edge_end = -0.5
|
1493
|
+
while 0 <= idx < len(segments):
|
1494
|
+
sx = segments[idx][start] + edge_start
|
1495
|
+
ex = segments[idx][end] + edge_end
|
1496
|
+
on = segments[idx][2]
|
1497
|
+
if last_x != sx:
|
1498
|
+
yield sx, last_y, 0
|
1499
|
+
last_x = ex
|
1500
|
+
yield last_x, last_y, on
|
1501
|
+
idx += dx
|
1502
|
+
else:
|
1503
|
+
if xy != last_x:
|
1504
|
+
last_x = xy
|
1505
|
+
yield last_x, last_y, 0
|
1506
|
+
if dy > 0:
|
1507
|
+
# from top to bottom
|
1508
|
+
idx = 0
|
1509
|
+
start = 0
|
1510
|
+
end = 1
|
1511
|
+
edge_start = -0.5
|
1512
|
+
edge_end = 0.5
|
1513
|
+
else:
|
1514
|
+
idx = len(segments) - 1
|
1515
|
+
end = 0
|
1516
|
+
start = 1
|
1517
|
+
edge_start = 0.5
|
1518
|
+
edge_end = -0.5
|
1519
|
+
while 0 <= idx < len(segments):
|
1520
|
+
sy = segments[idx][start] + edge_start
|
1521
|
+
ey = segments[idx][end] + edge_end
|
1522
|
+
on = segments[idx][2]
|
1523
|
+
if last_y != sy:
|
1524
|
+
yield last_x, sy, 0
|
1525
|
+
last_y = ey
|
1526
|
+
yield last_x, last_y, on
|
1527
|
+
idx += dy
|
1528
|
+
if self.bidirectional:
|
1529
|
+
if mode == ROW:
|
1530
|
+
dx = -dx
|
1531
|
+
else: # column
|
1532
|
+
dy = -dy
|
1533
|
+
|
1534
|
+
# We need to set the final values so that the rastercut is able to carry on
|
1535
|
+
self.final_x = last_x
|
1536
|
+
self.final_y = last_y
|
1537
|
+
t3 = perf_counter()
|
1538
|
+
if self.debug_level > 1:
|
1539
|
+
print (f"Overall time for crossover consumption: {t3-t0:.2f}s")
|
1540
|
+
print (f"Computation: {t2 - t0:.2f}s - Array creation:{t1 - t0:.2f}s, Algorithm: {t2 - t1:.2f}s")
|
1541
|
+
"""
|
1542
|
+
# Testpattern generation
|
1543
|
+
def testpattern_generator(self):
|
1544
|
+
def rectangle_h():
|
1545
|
+
# simple rectangle
|
1546
|
+
self.initial_x = 0
|
1547
|
+
self.initial_y = 0
|
1548
|
+
self.final_x = 0
|
1549
|
+
self.final_y = 0
|
1550
|
+
self.horizontal = True
|
1551
|
+
|
1552
|
+
yield 0, 0, off
|
1553
|
+
yield self.width - 1, 0, on
|
1554
|
+
|
1555
|
+
yield self.width - 1, self.height - 1, on
|
1556
|
+
|
1557
|
+
yield 0, self.height - 1, on
|
1558
|
+
|
1559
|
+
yield 0, 0, on
|
1560
|
+
|
1561
|
+
def rectangle_v():
|
1562
|
+
# simple rectangle but start with y movements first
|
1563
|
+
self.initial_x = 0
|
1564
|
+
self.initial_y = 0
|
1565
|
+
self.final_x = 0
|
1566
|
+
self.final_y = 0
|
1567
|
+
self.horizontal = True
|
1568
|
+
|
1569
|
+
yield 0, 0, off
|
1570
|
+
yield 0, self.height - 1, on
|
1571
|
+
|
1572
|
+
yield self.width - 1, self.height - 1, on
|
1573
|
+
|
1574
|
+
yield self.width - 1, 0, on
|
1575
|
+
|
1576
|
+
yield 0, 0, on
|
1577
|
+
|
1578
|
+
|
1579
|
+
def snake_h():
|
1580
|
+
# horizontal snake
|
1581
|
+
self.initial_x = 0
|
1582
|
+
self.initial_y = 0
|
1583
|
+
x = 0
|
1584
|
+
y = 0
|
1585
|
+
self.horizontal = True
|
1586
|
+
yield 0, 0, off
|
1587
|
+
wd = self.width - 1
|
1588
|
+
left = True
|
1589
|
+
while y < self.height - 2:
|
1590
|
+
x = wd if left else 0
|
1591
|
+
self.horizontal = True
|
1592
|
+
yield x, y, on
|
1593
|
+
self.horizontal = False
|
1594
|
+
yield x, y + 2, on
|
1595
|
+
left = not left
|
1596
|
+
y += 2
|
1597
|
+
self.final_x = x
|
1598
|
+
self.final_y = y
|
1599
|
+
|
1600
|
+
def snake_v():
|
1601
|
+
# vertical snake
|
1602
|
+
self.initial_x = 0
|
1603
|
+
self.initial_y = 0
|
1604
|
+
x = 0
|
1605
|
+
yield 0, 0, off
|
1606
|
+
ht = self.height - 1
|
1607
|
+
top = True
|
1608
|
+
while x < self.width - 2:
|
1609
|
+
y = ht if top else 0
|
1610
|
+
self.horizontal = False
|
1611
|
+
yield x, y, on
|
1612
|
+
self.horizontal = True
|
1613
|
+
yield x + 2, y, on
|
1614
|
+
top = not top
|
1615
|
+
x += 2
|
1616
|
+
self.final_x = x
|
1617
|
+
self.final_y = y
|
1618
|
+
|
1619
|
+
def spiral():
|
1620
|
+
# Spiral to inside
|
1621
|
+
self.initial_x = 0
|
1622
|
+
self.initial_y = 0
|
1623
|
+
yield 0, 0, off
|
1624
|
+
|
1625
|
+
x = -2 # start
|
1626
|
+
y = 0
|
1627
|
+
width = self.width + 1
|
1628
|
+
height = self.height - 1
|
1629
|
+
while width > 0 and height > 0:
|
1630
|
+
x += width
|
1631
|
+
y += 0
|
1632
|
+
self.horizontal = True
|
1633
|
+
yield x, y, on
|
1634
|
+
x += 0
|
1635
|
+
y += height
|
1636
|
+
self.horizontal = False
|
1637
|
+
yield x, y, on
|
1638
|
+
x -=(width - 2)
|
1639
|
+
y += 0
|
1640
|
+
self.horizontal = True
|
1641
|
+
yield x, y, on
|
1642
|
+
x += 0
|
1643
|
+
y -= (height - 2)
|
1644
|
+
self.horizontal = False
|
1645
|
+
yield x, y, on
|
1646
|
+
width -= 4
|
1647
|
+
height -= 4
|
1648
|
+
self.final_x = x
|
1649
|
+
self.final_y = y
|
1650
|
+
|
1651
|
+
on = self.filter(0)
|
1652
|
+
off = 0
|
1653
|
+
# print (f"on={on}, off={off}")
|
1654
|
+
method = abs(self.direction) - 1
|
1655
|
+
methods = (rectangle_h, rectangle_v, snake_h, snake_v, spiral)
|
1656
|
+
try:
|
1657
|
+
yield from methods[method]()
|
1658
|
+
except IndexError:
|
1659
|
+
print (f"Unknown testgenerator for {self.direction}")
|
1660
|
+
"""
|