From 8b8e587611611fedb6ba6f2c4affa0c6c8d1ff09 Mon Sep 17 00:00:00 2001 From: Petr Kratina Date: Wed, 21 Jun 2023 21:53:51 +0200 Subject: [PATCH] initial commit --- .gitignore | 2 + Change_Log.txt | 319 +++ LaserSpeed.py | 253 ++ README_Linux.txt | 50 + README_MacOS.md | 47 + bezmisc.py | 288 +++ build_exe.bat | 20 + convex_hull.py | 70 + cspsubdiv.py | 38 + cubicsuperpath.py | 171 ++ dxf.py | 1405 ++++++++++ ecoords.py | 131 + egv.py | 727 ++++++ embedded_images.py | 634 +++++ emblem | Bin 0 -> 151878 bytes ffgeom.py | 141 + g_code_library.py | 2071 +++++++++++++++ gpl-3.0.txt | 674 +++++ inkex.py | 403 +++ interface.html | 77 + interpolate.py | 1 + k40_whisperer.py | 5990 +++++++++++++++++++++++++++++++++++++++++++ nano_library.py | 457 ++++ py2exe_setup.py | 24 + requirements.txt | 4 + scorchworks.ico | Bin 0 -> 151878 bytes simplepath.py | 211 ++ simplestyle.py | 245 ++ simpletransform.py | 261 ++ svg_reader.py | 920 +++++++ windowsinhibitor.py | 58 + 31 files changed, 15692 insertions(+) create mode 100644 .gitignore create mode 100644 Change_Log.txt create mode 100644 LaserSpeed.py create mode 100644 README_Linux.txt create mode 100644 README_MacOS.md create mode 100644 bezmisc.py create mode 100644 build_exe.bat create mode 100644 convex_hull.py create mode 100644 cspsubdiv.py create mode 100644 cubicsuperpath.py create mode 100644 dxf.py create mode 100644 ecoords.py create mode 100644 egv.py create mode 100644 embedded_images.py create mode 100644 emblem create mode 100644 ffgeom.py create mode 100644 g_code_library.py create mode 100644 gpl-3.0.txt create mode 100644 inkex.py create mode 100644 interface.html create mode 100644 interpolate.py create mode 100644 k40_whisperer.py create mode 100644 nano_library.py create mode 100644 py2exe_setup.py create mode 100644 requirements.txt create mode 100644 scorchworks.ico create mode 100644 simplepath.py create mode 100644 simplestyle.py create mode 100644 simpletransform.py create mode 100644 svg_reader.py create mode 100644 windowsinhibitor.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97b45a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dev_venv/ +__pycache__ diff --git a/Change_Log.txt b/Change_Log.txt new file mode 100644 index 0000000..233e284 --- /dev/null +++ b/Change_Log.txt @@ -0,0 +1,319 @@ +Version 0.01: +- Initial Release + +Version 0.02: +- Added Min/Max darkness cutoff settings for halftone raster images. +- Fixed bug that could cause raster engraving to start engraving the wrong direction. +- Fixed error messages and added better error messages. +- Removed dependency on Numpy. + +Version 0.03: +- Added more descriptive test to "No Backend Error" to indicate libUSB is not installed +- Removed debugging code that could have resulted in the head moving 1" away from raster image. +- Updated Raster test pattern SVG + +Version 0.04: +- Improvements to DXF import to prevent import failures +- DXF import now detects blue lines and designates them as engraving lines. +- DXF import looks for layers that have the text "engrave" in the name and designates items on the layer as engraving lines. +- DXF import now detects the units in the file. If no units are specified the user is prompted to select the correct units. +- Stop button now works during all phases of engraving preparation +- Status updates more during engraving preparation and engraving process +- Stop button now pauses job with the option to resume or terminate the job. +- Added error on detection of color coded text in SVG files. +- Deleted Min/Max darkness cutoff settings for halftone raster images. +- Added Levels settings utilizing a Bezier curve to control the levels of gray-scale images +- Changed scan-line step input to always be in inches. (Raster settings are in inches/dpi now) + +Version 0.05: +- Added option to support homing in the upper right corner (instead of the upper left corner) for some 50W machines +- Changed the final move after vector engrave and vector cutting to be a rapid move to the starting position (was at working speed move). +- Updated code to support more easily running on Mac computers. Instructions added in the src zip file for getting started on Mac. +- Added code to eliminate the requirement for the PIL/Pillow _imaging C module. +- Started updating for compatibility with Python 3 (this is a low priority work in progress) + +Version 0.06: +- Added code to DXF class to fix bad knot data in splines. +- Added better status messages during image processing +- Changed half toning to PIL/PILLOW half toning (faster) +- Added better error message for Memory Error which previously resulted in a blank message box. +- Added a 64bit executable to the list of downloads to address memory issued with 32bi application + +Version 0.07: +- Fixed "maximum recursion depth exceeded in cmp" error when engraving/cutting. +- Adjusted code for better memory management when generating data for the laser cutter. + +Version 0.08: +- Fixed X-axis rapid motion bug that resulted in a engraving/cutting offset after some .003 inch rapid motions. +- Instructions added in the src zip file for getting setup on Linux (README_Linux.txt). +- Added setting for B1 boards Not enabled yet still need to be tested + +Version 0.09: +- Adjusted DXF import to avoid DXF loading fail due to rounding error during calculations. + +Version 0.10: +- Added support for SVG "polyline" and "polygon" objects +- Added code to automatically convert text to paths when red/blue vector text outlines are found. +- Hidden layers now remain hidden. + +Version 0.11: +- Added support for reading g-code. +- Enabled option for controller board version B1 +- Fixed a bug that resulted in bad design scaling if one of the feed rates were not entered correctly when changing units. +- Added support for SVG "Line" objects + +Version 0.12: +- Added advanced options pane +- Added multi-pass engraving and cutting +- Added Mirror option +- Added "Use Input CSYS" option to force K40 Whisperer to use the coordinate system from the input design (the default is to use the upper left corner of the design). +- Added option to disable homing the laser cutter upon initialization. +- Added option to disable preprocessing of CRC data. Un-selecting the "Preprocess CRC Data" option results in the CRC data being generated on the fly while data is being sent to the laser. +- Added option in Rester Settings to force engraving from the bottom of the raster image to the top. Sometimes this is useful to prevent smoke from spoiling the engraved image. +- Changed the communication with the laser cutter to include error detection/correction during transmission of data. +- Fixed bug that resulted in the laser area value being removed during a change in units + +Version 0.13: +- Fixed g-code processing so that it does not ignore spindle inputs. Now works with raster input from LaserWeb4 +- Fixed bug that could result in "'update_gui' is not defined" error. + +Version 0.14: +- Fixed bug that resulted in two passes on some vector cut and vector engrave paths +- Changed some error messages to be less confusing. + +Version 0.15: +- Fixed DXF import so it recognizes colors assigned at the layer level +- Improved DXF warning messages. Identical messages are now counted and to displayed in one line. +- Added support for the remaining board type supported by LaserDRW + +Version 0.16: +- Added support for CSS style data in SVG files (some version of CorelDraw use this formating for SVG files) +- Fixed divide by zero error for some DXF files +- Fixed "zip" error for broken SVG files (now ignores the error) +- Added rough time estimates for engraving/cutting times +- Can now move design around while keeping the laser head at any corner or at the center of the design. (It will move back to upper left when laser starts.) +- Added display of the current design file name in the main window title bar. +- Now accepts SVG 'style' data that is not in the style attribute. + +Version 0.17: +- Added ability to set scale factor values for the X and Y axes to fine tune output. +- Added rotate option in the advanced settings to rotate designs 90 degrees +- Fixed SVG input for rectangles that had radii specified that were larger than possible given the rectangle size. +- Fixed SVG import of files with mixed CSS data and style data +- Fixed error that prevented G-Code from running in Version 0.16 +- Fixed bug that could cause program to enter infinite loop after lost connection with laser +- Made a slightly better error message for when an operation is attempted and the laser has not been initialized. + +Version 0.18: +- Fixed import of polygons and polylines items in SVG files without the customary commas +- Fixed bug that caused moving to the corners to not work for SVG files if the "Use Input CSYS" was selected for DXF files. + +Version 0.19: +- Fixed SVG import error for SVG files with extra spaces in point definitions + +Version 0.20: +- Fixed bug that resulted in 45 degree angles (and other shapes not straight lines) being cut faster than horizontal and vertical lines. +- Added keyboard shortcuts for some main window functions +- Fixed divide by zero error when running g-code with very slow feed rates +- Added support for hidden layers in DXF files +- Improved interpretation of some DXF files +- Added ability to save and run EGV files. Can run EGV files made by LaserDRW and generate files that can be run from LaserDRW. +- Changed behavior after number of timeouts is exceeded during a design run. K40 Whisperer will continue to try running the job until the user stops it. Previously and error window would pop up and the job would be terminated without user intervention. During pre-run moves the error window will still pop up. +- Added logic to wait for the laser to finish running a job before the interface becomes active again. + +Version 0.21: +- Updated build scripts to eliminate errors on some Windows computers. +- Changed default setting so that halftone/dithering is turned on. +- Fixed bug that generated error sometimes when opening a design is canceled from the file select dialog. + +Version 0.22: +- Fixed DXF import so that working with DXF files will be faster. +- Added link to the new manual web page in the Help menu +- Updated code to work with Python 3 (Still works with Python 2.7) +- Number of timeouts setting not controls how many timeouts trigger a laser disconnected message at the end of engraving. +- Added Python version information to the Help info window + +Version 0.23: +- Fixed DXF import for a specific subset of DXF files that use multiple layers designations for a single feature (array commands?). + +Version 0.24: +- Fixed handling of large laser area files. Previous version would fail to generate raster data for large areas. +- Fixed compatibility with Python 3.7 +- Added better error reporting when raster data fails to be produced by Inkscape + +Version 0.25: +- Fixed a variety of minor issues with SVG import (especially files generated with Adobe Illustrator) +- Fixed a couple of issues with importing DXF files. +- Added option to invert colors for raster engraving. + +Version 0.26: +- Fixed option to invert colors for raster engraving. +- Fixed more compatibility problems with Python 3 +- Added zoom to design size option +- Added options to combine engrave operations, vector operations or all operations. + +Version 0.27: +- Fixed speeds for M2 controller boards. Higher speeds were inaccurate. + +Version 0.28: +- Added ability to move the laser head to any position on the design. Use the right mouse button to move the laser head independent of the design location. Previously could only move to corners and center.) +- Reduced the number of times the display updates during calculation. +- Fixed speeds for controller boards (except M2). Higher speeds were inaccurate. + +Version 0.29: +- Slowed down the movement to return to the zero position after engraving. At high speeds the laser head would sometimes over-run and slam into the hard-stop if the job was started from the home position. +- Added command to stop windows computers from going to sleep while the laser is running. +- Changed the SVG reader so it ignores CSS entry without data instead of throwing an error. +- Added key binding for alt-control-(arrow keys) for moving the laser head independent of the design. + +Version 0.30: +- Added check box to enable/disable the settings typically used for a rotary fixture. +- Added option to set rapid speeds in the general settings. (zero uses the default controller rapid speed.) +- Fixed SVG reader so hidden layers are hidden. (In some cases they were being shown.) +- Fixed problem that caused excessive memory use when dithering was disabled. + +Changes in Version 0.31 +- Improved speed codes (again) now uses library from the K40Nano project on Github +- Added workaround for non UTF-8 characters in SVG files (Files from Adobe Illustrator sometimes have these). +- Fixed SVG reader so cloned objects work now. +- Added dialog box to manually set scale for SVG files that do not have physical scale fully defined. You should be able to bring in files without setting the units and view box in Inkscape. (Users will need to verify scale is correct for there designs.) +- Improved cascading style sheet interpretation for style information. (Some colors could being missed before the improvement.) +- Fixed crash on startup if data in settings file is incorrect. +- Changed writing location for error log files to avoid attempting to write to folders that are write protected. +- Added clarification to the installer script to specify the laser needs to be on when installing the driver. +- Fixed a cannot convert string to float error for SVG files that define circles as ellipses. + +Changes in Version 0.32 +- Added fix for timeout issues. + +Version 0.33: +- Fixed potential error when decimal points are added to integer input values. +- Fixed erroneous closing of loops in some DXF files +- Fixed potential cause of lost data during sending data to the laser +- Fixed bug that caused laser to move in the wrong direction for certain cases while running g-code. +- Made the "Stop" button respond faster and after clicking "OK" the laser will stop motion faster. + +Version 0.34: +- Fixed "float() argument must be a string or number error" for some SVG files. +- Now respects display='none' for vector engrave/cut items in SVG files. + +Version 0.35: +- Fixed arrow button, center and corner button functions when using rotary settings. (These functions where broken in a previous version.) +- Changed vector engrave function so that it does not try to cut inside first. This greatly speeds up processing for complex designs. + +Version 0.36: +- Fixed rapid motion problems during and at end of raster engraving with certain settings. (Especially when raster engraving bottom up.) + +Version 0.37: +- Fixed another occurrence of "float() argument must be a string or number error" for some SVG files. + +Version 0.38: +- Fixed call to Inkscape so it works with Inkscape Version 1.0 Beta +- Replaced USB menu with Tools menu and added additional tools (trace and compute raster time) +- Added Trace boundary option to Tools menu (traces around convex hull of the design) +- Added shortcut keys so that number pad can be used to control laser head position +- Added post run options in General settings (home, beep, display report, execute batch file) +- Improved time estimates for raster engraving (Now uses actual laser path data.) +- Added option to control timeout time for Inkscape subprocess execution +- Added command line option for smaller display (-p or --pi) + +Version 0.39: +- Fixed problem that prevented the main screen from being disabled while the laser is running or calculations are being performed. + +Version 0.40: +- Fixed problem reading some style data from SVG files. + +Version 0.41: +- Fixed more minor issues reading certain SVG files. + +Version 0.42: +- Minor update to DXF reading to include color by layer +- Fix for Inkscape Executable location button. Auto generated text made bu the button did not work for some version of Python +- Fixed trace boundary when X and Y scaling is used +- Limited minimum speed entries to prevent unexpected laser movements. +- Fixed trace boundary space so that it scales with units change. + +Version 0.43: +- Fixed problem that prevented properly sending EGV file to the laser. + +Version 0.44: +- Removed unnecessary commands from data sent to laser. Reduced volume of data sent to laser. +- Improved pause function. Laser not stops immediately rather than waiting for buffer to empty. +- Added code to get around "Backend not found" error in some cases. + +Version 0.45: +- Reverted back to egv.py file from Version 0.43 fixing problem with movements introduced in Version 0.44 + +Version 0.46: +- Fixed error in positioning when using g-code input and the input coordinate system +- Improved display of g-code errors and warnings +- Changed file selection default to g-code if the last file opened was g-code + +Version 0.47: +- Updated to work with Inkscape 1.0 +- Fixed mirroring when using input axes + +Version 0.48: +- Fixed auto convert text to path when using older versions of Inkscape + +Version 0.49: +- Added new default Inkscape executable path to the search list. So users don't need to specify the Inkscape location in the settings. + +Version 0.50: +- Added support for running two of more lasers by opening more K40 Whisperer windows on a single machine. +- Added protection to prevent the user from disrupting a running laser job by typing shortcut keys. + +Version 0.51: +- Fixed problem in 0.50 that resulted in origin shifting if laser was not in upper left corner when a job was started. + +Version 0.52: +- Added margin to prevent warning about out of bounds vector features when they are really close to the page boundary. +- Added explicit font definition for GUI + +Version 0.53: +- Fixed handling of some hidden layers/objects +- Fixed handling of a less common color specification format +- Brought back removal of unnecessary commands from data sent to laser (from V0.43 with a correction). This Reduced volume of data sent to laser. + +Version 0.54: +- Reverted back to egv.py file from Version 0.52 fixing problem with movements introduced in Version 0.53 + +Version 0.55: +- Fixed multi-machine operation under Windows 10 +- Fixed an occurrence of "float argument must be a string or a number" error for some SVG files. + +Version 0.56: +- Added code to fix driver conflict new drivers added to Linux +- Changed default file type to svg/dxf + +Version 0.57: +- Fixed problem with start position (only occurred when using a custom "x scale" factor and "home in upper right") +- Added Command line option to enable debug mode +- Some other minor change to the way icons and internal images are handled (no change for user) + +Version 0.58: +- Fixed problem with right mouse click motions that occurred when using "home in upper right" + +Version 0.59: +- Now automatically removes zero length features from input files (caused unnecessary movements before) +- Removed redundant data sent to laser during raster engraving. Should reduce pauses for higher speed engraving. +- Added option for reduced memory use. This can be enabled to allow for larger designs to be loaded in K40 Whisperer or just increase speed. This option does reduce the resolution of the data coming from Inkscape 500dpi vs 1000dpi. This should not visibly affect the output in most cases. + +Version 0.60: +- Fixed scaling problem when loading an SVG file with 'Reduced Memory Use' enabled. + The problem only occurred if the user was prompted for additional scaling information. + +Version 0.61: +- Added option in the General Settings to disable waiting for the laser to finish the job after the last data has been sent to the laser. This can be used to allow the user to start loading the next design as the laser finishes executing the the final data. + +Version 0.62: +- Fixed problem when using M3 Nano board, new job finished code is now detected. +- Fixed problem when using M3 Nano board, laser no longer remains on while moving back to starting position after raster engraving. +- Fixed registration issue between raster and vector operations when custom rapid speed was used. +- Added Option in the Tools menu that may unfreeze the controller after a job is improperly terminated (will not always work) + +Version 0.63: +- Fixed rapid moves with M3 Nano. Rapid moves failed if the move command filled exactly one packet of data. Sending an additional packet of data without any data after the full packet resolved the issue. + +Version 0.64: +- Fixed arcs being drawn in the wrong direction when the radius value was specified as a negative number in an SVG file. diff --git a/LaserSpeed.py b/LaserSpeed.py new file mode 100644 index 0000000..29801df --- /dev/null +++ b/LaserSpeed.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python + +from math import floor, ceil + + +class LaserSpeed: + """ + MIT License. + + This is the standard library for converting to and from speed code information for LHYMICRO-GL. + + The units in the speed code have particular bands/gears which switches the equations used to convert + between values and speeds. The fundamental units within the speed code value is period. All values + are linearly related to the delay between ticks. The device controlled is ultimately a stepper motor + and the speed a stepper motor travels at is the result of the time between ticks. We are dealing with + a 1000 dpi stepper motor, so for example to travel at 1 inch a second requires that the device tick + at 1 kHz. To do this it must delay 1 ms between ticks. This corresponds to a value of 48296 in the M2 + board. Which has an equation of 60416 - (12120 * T) where T is the period requested in ms. This is + equal to 25.4 mm/s. If we want a 2 ms delay, which is half the speed (0.5kHz, 0.5 inches/second, + 12.7 mm/s) we do 60416 - (12120 * 2) which gives us a value of 36176. This would be encoded a 16 bit + number broken up into 2 ascii 3 digit strings between 0-255. 141 for the high bits and 80 for the low + bits. So CV1410801 where the final 1 is the gearing equation we used. + + The speed in mm/s is also used for determining which gearing to use and as a factor for the horizontal + encoded value. Slow down the device down while traveling diagonal to make the diagonal and orthogonal + take the same amount of time (thereby cutting to the same depth). + """ + + def __init__(self): + pass + + @staticmethod + def get_speed_from_code(speed_code, board="M2"): + code_value, gear, step_value, diagonal, raster_step = LaserSpeed.parse_speed_code(speed_code) + # b, m, gear = LaserSpeed.get_gearing(board, code_value, raster_step == 0) + b, m, gear = LaserSpeed.get_gearing(board, gear=gear, uses_raster_step=raster_step != 0) + return LaserSpeed.get_speed_from_value(code_value, b, m) + + @staticmethod + def get_code_from_speed(mm_per_second, raster_step=0, board="M2", d_ratio=0.261199033289, gear=None): + """ + Get a speedcode from a given speed. The raster step appends the 'G' value and uses speed ranges. + The d_ratio uses the default/auto ratio, but might be improved at sqrt(2)-1 (0.41421356). + The gearing is optional and forces the speedcode to work for that particular gearing. Gear=0 + refers to C-suffix notation speeds. + + :param mm_per_second: speed to convert to code. + :param raster_step: raster step mode to use. + :param board: Nano Board Model to do the conversion for. + :param d_ratio: M1, M2, B1, B2 have ratio of optional speed + :param gear: Optional force gearing rather than default gear for that speed. + :return: speed code produced. + """ + if mm_per_second > 240 and raster_step == 0: + mm_per_second = 19.05 # Arbitrary default speed for out range value. + b, m, gear = LaserSpeed.get_gearing(board, mm_per_second, raster_step != 0, gear) + + speed_value = LaserSpeed.get_value_from_speed(mm_per_second, b, m) + if (speed_value - round(speed_value)) > 0.005: + speed_value = ceil(speed_value) + speed_value = round(speed_value) + encoded_speed = LaserSpeed.encode_value(speed_value) + + if raster_step != 0: + if gear == 0: # There is no C suffix notation for gear raster step. + gear = 1 + return "V%s%1dG%03d" % ( + encoded_speed, + gear, + raster_step + ) + + if d_ratio == 0 or board == "A" or board == "B" or board == "M": + # We do not need the diagonal code. + if raster_step == 0: + if gear == 0: + return "CV%s1C" % ( + encoded_speed + ) + else: + return "CV%s%1d" % ( + encoded_speed, + gear) + else: + step_value = min(int(floor(mm_per_second) + 1), 128) + frequency_kHz = float(mm_per_second) / 25.4 + try: + period_in_ms = 1 / frequency_kHz + except ZeroDivisionError: + period_in_ms = 0 + d_value = d_ratio * -m * period_in_ms / float(step_value) + encoded_diagonal = LaserSpeed.encode_value(d_value) + if gear == 0: + return "CV%s1%03d%sC" % ( + encoded_speed, + step_value, + encoded_diagonal + ) + else: + return "CV%s%1d%03d%s" % ( + encoded_speed, + gear, + step_value, + encoded_diagonal) + + @staticmethod + def parse_speed_code(speed_code): + is_shortened = False + normal = False + if speed_code[0] == "C": + speed_code = speed_code[1:] + normal = True + if speed_code[-1] == "C": + speed_code = speed_code[:-1] + is_shortened = True + # This is an error speed. + if "V1677" in speed_code or "V1676" in speed_code: + # The 4th character can only be 0,1,2 except for error speeds. + code_value = LaserSpeed.decode_value(speed_code[1:12]) + speed_code = speed_code[12:] + # The value for this speed is so low, it's negative + # and bit-shifted in 24 bits of a negative number. + else: + code_value = LaserSpeed.decode_value(speed_code[1:7]) + speed_code = speed_code[7:] + gear = int(speed_code[0]) + speed_code = speed_code[1:] + + if is_shortened: + gear = 0 # Flags as step zero during code error. + raster_step = 0 + + if normal: + step_value = 0 + diagonal = 0 + if len(speed_code) > 1: + step_value = int(speed_code[:3]) + diagonal = LaserSpeed.decode_value(speed_code[3:]) + return code_value, gear, step_value, diagonal, raster_step + else: + if "G" in speed_code: + raster_step = int(speed_code[-3:]) + return code_value, gear, 1, 1, raster_step + + @staticmethod + def get_value_from_speed(mm_per_second, b, m): + """ + Takes in speed in mm per second and returns speed value. + """ + try: + frequency_kHz = float(mm_per_second) / 25.4 + period_in_ms = 1 / frequency_kHz + return LaserSpeed.get_value_from_period(period_in_ms, b, m) + except ZeroDivisionError: + return b + + @staticmethod + def get_value_from_period(x, b, m): + """ + Takes in period in ms and converts it to value. + This is a simple linear relationship. + """ + return m * x + b + + @staticmethod + def get_speed_from_value(value, b, m): + try: + period_in_ms = LaserSpeed.get_period_from_value(value, b, m) + frequency_kHz = 1 / period_in_ms + return 25.4 * frequency_kHz + except ZeroDivisionError: + return 0 + + @staticmethod + def get_period_from_value(y, b, m): + try: + return (y - b) / m + except ZeroDivisionError: + return float('inf') + + @staticmethod + def decode_value(code): + b1 = int(code[0:-3]) + if b1 > 16000000: + b1 -= 16777216 # decode error negative numbers + b2 = int(code[-3:]) + return (b1 << 8) + b2 + + @staticmethod + def encode_value(value): + value = int(value) + b0 = value & 255 + b1 = (value >> 8) & 0xFFFFFF # unsigned shift, to emulate bugged form. + return "%03d%03d" % (b1, b0) + + @staticmethod + def get_gear_for_speed(mm_per_second, uses_raster_step=False): + if mm_per_second <= 25.4: + return 1 + if 25.4 < mm_per_second <= 60: + return 2 + if not uses_raster_step: + if 60 < mm_per_second < 127: + return 3 + if 127 <= mm_per_second: + return 4 + else: + if 60 < mm_per_second < 127: + return 2 + if 127 <= mm_per_second <= 320: + return 3 + if 320 <= mm_per_second: + return 4 + + @staticmethod + def get_gearing(board, mm_per_second=None, uses_raster_step=False, gear=None): + if gear is None: + gear = LaserSpeed.get_gear_for_speed(mm_per_second, uses_raster_step) + # A, B, B1, B2 + b_values = [64752.0, 64752.0, 64640.0, 64512.0] + m = -2000.0 + if board[0] == "M": # any M series board + b_values = [60416.0, 60416.0, 59904.0, 59392.0] + m = -12120.0 + if board == "B2": + m = -24240.0 + if gear == 0: + if board == "B2": + if uses_raster_step: + return b_values[0], m / 12, 1 + else: + return b_values[0], m / 12, 0 + elif board == "M" or board == "M1": + return b_values[0], m, 0 + elif board == "M2": + return 65528.0, m / 12, 0 + elif mm_per_second is not None: + if board == "B2": + if mm_per_second < 7: + if uses_raster_step: + return b_values[0], m / 12, 1 + else: + return b_values[0], m / 12, 0 + elif board == "M": + if mm_per_second < 6: + return b_values[0], m, 0 + elif board == "M1": + if mm_per_second < 6 or (not uses_raster_step and mm_per_second < 7): + return b_values[0], m, 0 + elif board == "M2": + if mm_per_second < 7: + return 65528.0, m / 12, 0 + return b_values[gear - 1], m, gear diff --git a/README_Linux.txt b/README_Linux.txt new file mode 100644 index 0000000..c65f16b --- /dev/null +++ b/README_Linux.txt @@ -0,0 +1,50 @@ +Setting up K40whisperer on Linux (by Dr. med. Jan Schiefer): + +# Requirements + +Prerequirements: +* python +* unzip +* udev +* inkscape + +## Instructions + +1. Create a group for the users who are allowed to use the laser cutter: sudo groupadd lasercutter + +2. Add your yourself to this group, replace [YOUR USERNAME] with your unix username: sudo usermod -a -G lasercutter [YOUR USERNAME] + +3. Eventually add other users who will use the laser cutter to the group + +4. Plug in your laser cutter to your computer + +5. Create a udev control file four your laser cutter as root (i will use gedit in this example): sudo gedit /etc/udev/rules.d/97-ctc-lasercutter.rules + +Put the following text into the file and replace [VENDOR ID] and [PRODUCT ID] with the information you obtained from lsusb: +SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", ENV{DEVTYPE}=="usb_device", MODE="0664", GROUP="lasercutter" + +Now save the file. + +6. Reboot your computer! + +7. Download and the K40whisperer source code, for example "K40_Whisperer-0.07_src.zip" + +8. Unzip the source code, for example: unzip K40_Whisperer-0.07_src.zip -d /home/[YOUR USERNAME]/ + +9. Go to the K40 whisperer source directory, for example: cd /home/[YOUR USERNAME]/K40_Whisperer-0.07_src/ + +10. Install the requires python packages using the following commands: + pip install lxml + pip install pyusb + pip install pillow + pip install pyclipper + +11. Run K40whisperer: python ./k40_whisperer.py +11a. If K40 Whisperer starts but you cannot initialize the laser you can try running using the command: sudo python ./k40_whisperer.py + If everything works that way you should revisit step 5. because the user is not able to access the usb port. You can always run using sudo but it is generally a bad practice. + +12. Go to Setting --> General settings + +13. Select your laser control board name (usually LASER-M2 which is the default.) + +14. If you click the "save" button in the general settings your current settings will be saved for future sessions. diff --git a/README_MacOS.md b/README_MacOS.md new file mode 100644 index 0000000..8eda04d --- /dev/null +++ b/README_MacOS.md @@ -0,0 +1,47 @@ +------------------------------------------------------------------------------------ +Thanks to Pete Peterson (@ipetepete on Twitter) for these instructions +for setting up K40 Whisperer on a Mac computer +------------------------------------------------------------------------------------ + +# Requirements + +* Python 2.7 (this works nicely if you use virtualenv) +* Inkscape (build from source using brew) +* Must be run as `root` -see below for more info + +## Instructions + +### Install Inkscape + +This did not work using the Quartz binary for Inkscape. Only by building from source did it work correctly. +Suggested approach is installing using __Homebrew__: + +` brew install caskformula/caskformula/inkscape` + +### Install Python & Libraries + +Suggested approach is to use [Virtualenv](https://virtualenv.pypa.io/en/stable/) and install Python 2.7 even if your system is currently running 2.7. + +__Install requirements:__ + +`pip install -r requirements.txt` + +__Run K40Whisperer__ + +`sudo python k40_whisperer.py` + +_Why does this need to be run as root?_ + +In general all devices require elevated permissions. To allow PyUSB access to a certain device as non-root, some work needs to be done, namely; create a user-group, set perms to the device when connected as belonging to the group, add your user to the newly created user-group. + +Read more here: https://stackoverflow.com/questions/3738173/why-does-pyusb-libusb-require-root-sudo-permissions-on-linux#8582398 + +This can potentially be automated, but more work needs to be done. + + + +------------------------------------------------------------------------------------ +Thanks to Pete Peterson (@ipetepete on Twitter) for these instructions +for setting up K40 Whisperer on a Mac computer +------------------------------------------------------------------------------------ + diff --git a/bezmisc.py b/bezmisc.py new file mode 100644 index 0000000..9148c32 --- /dev/null +++ b/bezmisc.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +''' +Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +import math, cmath + +def rootWrapper(a,b,c,d): + if a: + # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots + a,b,c = (b/a, c/a, d/a) + m = 2.0*a**3 - 9.0*a*b + 27.0*c + k = a**2 - 3.0*b + n = m**2 - 4.0*k**3 + w1 = -.5 + .5*cmath.sqrt(-3.0) + w2 = -.5 - .5*cmath.sqrt(-3.0) + if n < 0: + m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) + n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) + else: + if m+math.sqrt(n) < 0: + m1 = -pow(-(m+math.sqrt(n))/2,1./3) + else: + m1 = pow((m+math.sqrt(n))/2,1./3) + if m-math.sqrt(n) < 0: + n1 = -pow(-(m-math.sqrt(n))/2,1./3) + else: + n1 = pow((m-math.sqrt(n))/2,1./3) + x1 = -1./3 * (a + m1 + n1) + x2 = -1./3 * (a + w1*m1 + w2*n1) + x3 = -1./3 * (a + w2*m1 + w1*n1) + return (x1,x2,x3) + elif b: + det=c**2.0-4.0*b*d + if det: + return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b) + else: + return -c/(2.0*b), + elif c: + return 1.0*(-d/c), + return () + +def bezierparameterize(xxx_todo_changeme): + #parametric bezier + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme + x0=bx0 + y0=by0 + cx=3*(bx1-x0) + bx=3*(bx2-bx1)-cx + ax=bx3-x0-cx-bx + cy=3*(by1-y0) + by=3*(by2-by1)-cy + ay=by3-y0-cy-by + + return ax,ay,bx,by,cx,cy,x0,y0 + #ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + +def linebezierintersect(xxx_todo_changeme1, xxx_todo_changeme2): + #parametric line + ((lx1,ly1),(lx2,ly2)) = xxx_todo_changeme1 + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme2 + dd=lx1 + cc=lx2-lx1 + bb=ly1 + aa=ly2-ly1 + + if aa: + coef1=cc/aa + coef2=1 + else: + coef1=1 + coef2=aa/cc + + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + #cubic intersection coefficients + a=coef1*ay-coef2*ax + b=coef1*by-coef2*bx + c=coef1*cy-coef2*cx + d=coef1*(y0-bb)-coef2*(x0-dd) + + roots = rootWrapper(a,b,c,d) + retval = [] + for i in roots: + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i)) + return retval + +def bezierpointatt(xxx_todo_changeme3,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3 + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + x=ax*(t**3)+bx*(t**2)+cx*t+x0 + y=ay*(t**3)+by*(t**2)+cy*t+y0 + return x,y + +def bezierslopeatt(xxx_todo_changeme4,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4 + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + dx=3*ax*(t**2)+2*bx*t+cx + dy=3*ay*(t**2)+2*by*t+cy + return dx,dy + +def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5 + (dy,dx) = xxx_todo_changeme6 + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + #quadratic coefficents of slope formula + if dx: + slope = 1.0*(dy/dx) + a=3*ay-3*ax*slope + b=2*by-2*bx*slope + c=cy-cx*slope + elif dy: + slope = 1.0*(dx/dy) + a=3*ax-3*ay*slope + b=2*bx-2*by*slope + c=cx-cy*slope + else: + return [] + + roots = rootWrapper(0,a,b,c) + retval = [] + for i in roots: + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + retval.append(i) + return retval + +def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t): + (x1,y1) = xxx_todo_changeme7 + (x2,y2) = xxx_todo_changeme8 + return x1+t*(x2-x1),y1+t*(y2-y1) +def beziersplitatt(xxx_todo_changeme9,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9 + m1=tpoint((bx0,by0),(bx1,by1),t) + m2=tpoint((bx1,by1),(bx2,by2),t) + m3=tpoint((bx2,by2),(bx3,by3),t) + m4=tpoint(m1,m2,t) + m5=tpoint(m2,m3,t) + m=tpoint(m4,m5,t) + + return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3)) + +''' +Approximating the arc length of a bezier curve +according to + +if: + L1 = |P0 P1| +|P1 P2| +|P2 P3| + L0 = |P0 P3| +then: + L = 1/2*L0 + 1/2*L1 + ERR = L1-L0 +ERR approaches 0 as the number of subdivisions (m) increases + 2^-4m + +Reference: +Jens Gravesen +"Adaptive subdivision and the length of Bezier curves" +mat-report no. 1992-10, Mathematical Institute, The Technical +University of Denmark. +''' +def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11): + (x1,y1) = xxx_todo_changeme10 + (x2,y2) = xxx_todo_changeme11 + return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) +def Gravesen_addifclose(b, len, error = 0.001): + box = 0 + for i in range(1,4): + box += pointdistance(b[i-1], b[i]) + chord = pointdistance(b[0], b[3]) + if (box - chord) > error: + first, second = beziersplitatt(b, 0.5) + Gravesen_addifclose(first, len, error) + Gravesen_addifclose(second, len, error) + else: + len[0] += (box / 2.0) + (chord / 2.0) +def bezierlengthGravesen(b, error = 0.001): + len = [0] + Gravesen_addifclose(b, len, error) + return len[0] + +# balf = Bezier Arc Length Function +balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0 +def balf(t): + retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2 + return math.sqrt(retval) + +def Simpson(f, a, b, n_limit, tolerance): + n = 2 + multiplier = (b - a)/6.0 + endsum = f(a) + f(b) + interval = (b - a)/2.0 + asum = 0.0 + bsum = f(a + interval) + est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) + est0 = 2.0 * est1 + #print multiplier, endsum, interval, asum, bsum, est1, est0 + while n < n_limit and abs(est1 - est0) > tolerance: + n *= 2 + multiplier /= 2.0 + interval /= 2.0 + asum += bsum + bsum = 0.0 + est0 = est1 + for i in range(1, n, 2): + bsum += f(a + (i * interval)) + est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) + #print multiplier, endsum, interval, asum, bsum, est1, est0 + return est1 + +def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12 + global balfax,balfbx,balfcx,balfay,balfby,balfcy + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy + return Simpson(balf, 0.0, 1.0, 4096, tolerance) + +def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13 + global balfax,balfbx,balfcx,balfay,balfby,balfcy + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy + t = 1.0 + tdiv = t + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + targetlen = l * curlen + diff = curlen - targetlen + while abs(diff) > tolerance: + tdiv /= 2.0 + if diff < 0: + t += tdiv + else: + t -= tdiv + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + diff = curlen - targetlen + return t + +#default bezier length method +bezierlength = bezierlengthSimpson + +if __name__ == '__main__': + import timing + #print linebezierintersect(((,),(,)),((,),(,),(,),(,))) + #print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0))) + tol = 0.00000001 + curves = [((0,0),(1,5),(4,5),(5,5)), + ((0,0),(0,0),(5,0),(10,0)), + ((0,0),(0,0),(5,1),(10,0)), + ((-10,0),(0,0),(10,0),(10,10)), + ((15,10),(0,0),(10,0),(-5,10))] + ''' + for curve in curves: + timing.start() + g = bezierlengthGravesen(curve,tol) + timing.finish() + gt = timing.micro() + + timing.start() + s = bezierlengthSimpson(curve,tol) + timing.finish() + st = timing.micro() + + print g, gt + print s, st + ''' + for curve in curves: + print(beziertatlength(curve,0.5)) + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/build_exe.bat b/build_exe.bat new file mode 100644 index 0000000..35673ee --- /dev/null +++ b/build_exe.bat @@ -0,0 +1,20 @@ +PROMPT $g +rem --------------------------------------------------------------------- +rem This file executes the build command for the windows executable file. +rem It is here because I am lazy +rem --------------------------------------------------------------------- +del *.pyc +rmdir /S /Q dist +rmdir /S /Q dist32 +rmdir /S /Q dist64 + +C:\Python27_32\python.exe py2exe_setup.py py2exe +rmdir /S /Q build +move dist dist32 +rem pause + +del *.pyc +C:\Python27_64\python.exe py2exe_setup.py py2exe +rmdir /S /Q build +move dist dist64 +pause \ No newline at end of file diff --git a/convex_hull.py b/convex_hull.py new file mode 100644 index 0000000..9a105d5 --- /dev/null +++ b/convex_hull.py @@ -0,0 +1,70 @@ +""" +2D Convex Hull Code from Wikibooks +https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain +""" + +class hull2D: + + def convex_hull(self,points): + """Computes the convex hull of a set of 2D points. + + Input: an iterable sequence of (x, y) pairs representing the points. + Output: a list of vertices of the convex hull in counter-clockwise order, + starting from the vertex with the lexicographically smallest coordinates. + Implements Andrew's monotone chain algorithm. O(n log n) complexity. + """ + + # Sort the points lexicographically (tuples are compared lexicographically). + # Remove duplicates to detect the case we have just one unique point. + points = sorted(set(points)) + + # Boring case: no points or a single point, possibly repeated multiple times. + if len(points) <= 1: + return points + + # 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + # Returns a positive value, if OAB makes a counter-clockwise turn, + # negative for clockwise turn, and zero if the points are collinear. + def cross(o, a, b): + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + # Build lower hull + lower = [] + for p in points: + while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0: + lower.pop() + lower.append(p) + + # Build upper hull + upper = [] + for p in reversed(points): + while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0: + upper.pop() + upper.append(p) + + # Concatenation of the lower and upper hulls gives the convex hull. + # Last point of each list is omitted because it is repeated at the beginning of the other list. + return lower[:-1] + upper[:-1] + + + def convexHullecoords(self,ecoords): + p=[] + for line in ecoords: + p.append((line[0],line[1])) + + hull_data = self.convex_hull(p) + ecoords=[] + for i in range(0,len(hull_data)): + ecoords.append([hull_data[i][0],hull_data[i][1],1]) + ecoords.append(ecoords[0]) + return ecoords + + ###################################################################### + + +if __name__ == '__main__': + my_hull=hull2D() + p = [(1,1),(0,3),(0,0),(4,5),(10,10)] + c = my_hull.convex_hull(p) + print(p) + print(c) diff --git a/cspsubdiv.py b/cspsubdiv.py new file mode 100644 index 0000000..6c101e1 --- /dev/null +++ b/cspsubdiv.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +from bezmisc import * +from ffgeom import * + +def maxdist(xxx_todo_changeme): + ((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = xxx_todo_changeme + p0 = Point(p0x,p0y) + p1 = Point(p1x,p1y) + p2 = Point(p2x,p2y) + p3 = Point(p3x,p3y) + + s1 = Segment(p0,p3) + return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2)) + + +def cspsubdiv(csp,flat): + for sp in csp: + subdiv(sp,flat) + +def subdiv(sp,flat,i=1): + while i < len(sp): + p0 = sp[i-1][1] + p1 = sp[i-1][2] + p2 = sp[i][0] + p3 = sp[i][1] + + b = (p0,p1,p2,p3) + m = maxdist(b) + if m <= flat: + i += 1 + else: + one, two = beziersplitatt(b,0.5) + sp[i-1][2] = one[1] + sp[i][0] = two[2] + p = [one[2],one[3],two[1]] + sp[i:1] = [p] + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/cubicsuperpath.py b/cubicsuperpath.py new file mode 100644 index 0000000..703dd2d --- /dev/null +++ b/cubicsuperpath.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +""" +cubicsuperpath.py + +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +""" +import simplepath +from math import * + +def matprod(mlist): + prod=mlist[0] + for m in mlist[1:]: + a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0] + a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1] + a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0] + a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1] + prod=[[a00,a01],[a10,a11]] + return prod +def rotmat(teta): + return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]] +def applymat(mat, pt): + x=mat[0][0]*pt[0]+mat[0][1]*pt[1] + y=mat[1][0]*pt[0]+mat[1][1]*pt[1] + pt[0]=x + pt[1]=y +def norm(pt): + return sqrt(pt[0]*pt[0]+pt[1]*pt[1]) + +def ArcToPath(p1,params): + A=p1[:] + rx,ry,teta,longflag,sweepflag,x2,y2=params[:] + rx=abs(rx) + ry=abs(ry) + teta = teta*pi/180.0 + B=[x2,y2] + if rx==0 or ry==0 or A==B: + return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]]) + mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta))) + applymat(mat, A) + applymat(mat, B) + k=[-(B[1]-A[1]),B[0]-A[0]] + d=k[0]*k[0]+k[1]*k[1] + k[0]/=sqrt(d) + k[1]/=sqrt(d) + d=sqrt(max(0,1-d/4)) + if longflag==sweepflag: + d*=-1 + O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]] + OA=[A[0]-O[0],A[1]-O[1]] + OB=[B[0]-O[0],B[1]-O[1]] + start=acos(OA[0]/norm(OA)) + if OA[1]<0: + start*=-1 + end=acos(OB[0]/norm(OB)) + if OB[1]<0: + end*=-1 + + if sweepflag and start>end: + end +=2*pi + if (not sweepflag) and start + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +""" +from math import * +Zero = 0.00001 + +#################################################### +## PointClass from dxf2gcode_b02_point.py ## +#################################################### +class PointClass: + def __init__(self,x=0,y=0): + self.x=x + self.y=y + def __str__(self): + return ('X ->%6.3f Y ->%6.3f' %(self.x,self.y)) + +#################################################### +## Begin Excerpts from dxf2gcode_b02_nurbs_calc ## +#################################################### +class NURBSClass: + def __init__(self,degree=0,Knots=[],Weights=None,CPoints=None): + self.degree=degree #Spline degree + self.Knots=Knots #Knot Vector + self.CPoints=CPoints #Control points of splines [2D] + self.Weights=Weights #Weighting of the individual points + + #Initializing calculated variables + self.HCPts=[] #Homogeneous points vectors [3D] + + #Convert Points in Homogeneous points + self.CPts_2_HCPts() + + #Creating the BSplineKlasse to calculate the homogeneous points + self.BSpline=BSplineClass(degree=self.degree,\ + Knots=self.Knots,\ + CPts=self.HCPts) + + #Calculate a number points using error limiting + def calc_curve(self,n=0, lin_tol=.001): + #Initial values for step and u + u=0; Points=[] + i=1 + while self.Knots[i]==0: + i=i+1 + step=self.Knots[i]/3 + + Pt1=self.NURBS_evaluate(n=n,u=0.0) + Points.append(Pt1) + while u self.Knots[-1]): + step = self.Knots[-1]-u + + Pt2=self.NURBS_evaluate(n=n,u=u+step) + Pt_test=self.NURBS_evaluate(n=n,u=u + step/2) + + ### + DX = Pt2.x-Pt1.x + DY = Pt2.y-Pt1.y + cord = sqrt(DX*DX + DY*DY) + DXtest = Pt_test.x-(Pt1.x+Pt2.x)/2.0 + DYtest = Pt_test.y-(Pt1.y+Pt2.y)/2.0 + t = sqrt(DXtest*DXtest + DYtest*DYtest) + + if t > lin_tol: + step = step/2 + else: + u+=step + Points.append(Pt2) + step = step*2 + Pt1=Pt2 + return Points + + + #Calculate a point of NURBS + def NURBS_evaluate(self,n=0,u=0): + + #Calculate the homogeneous points to the n th derivative + HPt=self.BSpline.bspline_ders_evaluate(n=n,u=u) + + #Point back to normal coordinates transform + Point=self.HPt_2_Pt(HPt[0]) + return Point + + #Convert the NURBS control points and weight in a homogeneous vector + def CPts_2_HCPts(self): + for P_nr in range(len(self.CPoints)): + HCPtVec=[self.CPoints[P_nr].x*self.Weights[P_nr],\ + self.CPoints[P_nr].y*self.Weights[P_nr],\ + self.Weights[P_nr]] + self.HCPts.append(HCPtVec[:]) + + #Convert a homogeneous vector point in a point + def HPt_2_Pt(self,HPt): + return PointClass(x=HPt[0]/HPt[-1],y=HPt[1]/HPt[-1]) + +class BSplineClass: + def __init__(self,degree=0,Knots=[],CPts=[]): + self.degree=degree + self.Knots=Knots + self.CPts=CPts + + self.Knots_len=len(self.Knots) + self.CPt_len=len(self.CPts[0]) + self.CPts_len=len(self.CPts) + + # Incoming inspection, fit the upper node number, etc. + if self.Knots_len< self.degree+1: + raise Exception("SPLINE: degree greater than number of control points.") + if self.Knots_len != (self.CPts_len + self.degree+1): + raise Exception("SPLINE: Knot/Control Point/degree number error.") + + #Modified Version of Algorithm A3.2 from "THE NURBS BOOK" pg.93 + def bspline_ders_evaluate(self,n=0,u=0): + #Calculating the position of the node vector + span=self.findspan(u) + + #Compute the basis function up to the n th derivative at the point u + dN=self.ders_basis_functions(span,u,n) + + p=self.degree + du=min(n,p) + + CK=[] + dPts=[] + for i in range(self.CPt_len): + dPts.append(0.0) + for k in range(n+1): + CK.append(dPts[:]) + + for k in range(du+1): + for j in range(p+1): + for i in range(self.CPt_len): + CK[k][i]+=dN[k][j]*self.CPts[span-p+j][i] + return CK + + #Algorithm A2.1 from "THE NURBS BOOK" pg.68 + def findspan(self,u): + #Special case when the value is == Endpoint + if(u==self.Knots[-1]): + return self.Knots_len-self.degree-2 + + # Binary search + # (The interval from high to low is always halved by + # [mid: mi +1] value lies between the interval of Knots) + low=self.degree + high=self.Knots_len + mid=int((low+high)/2) + while ((u=self.Knots[mid+1])): + if (u=k): + a[s2][0]=a[s1][0]/ndu[pk+1][rk] + der=a[s2][0]*ndu[rk][pk] + if (rk>=-1): + j1=1 + else: + j1=-rk + if (r-1<=pk): + j2=k-1 + else: + j2=d-r + + #Here he is not in the first derivative of pure + for j in range(j1,j2+1): + a[s2][j]=(a[s1][j]-a[s1][j-1])/ndu[pk+1][rk+j] + der+=a[s2][j]*ndu[rk+j][pk] + + if(r<=pk): + a[s2][k]=-a[s1][k-1]/ndu[pk+1][r] + der+=a[s2][k]*ndu[r][pk] + + ders[k][r]=der + j=s1; s1=s2; s2=j #Switch rows + + #Multiply through by the the correct factors + r=d + for k in range(1,n+1): + for j in range(d+1): + ders[k][j] *=r + r*=(d-k) + return ders + +#################################################### +## End Excerpts from dxf2gcode_b02_nurbs_calc.py ## +#################################################### + +class Header: + def __init__(self): + self.variables = dict() + self.last_var = None + def new_var(self, kw): + self.variables.update({kw: dict()}) + self.last_var = self.variables[kw] + def new_val(self, val): + self.last_var.update({ str(val[0]) : val[1] }) + +class Entity: + def __init__(self, _type): + self.type = _type + self.data = dict() + def update(self, value): + key = str(value[0]) + val = value[1] + if key in self.data: + if type(self.data[key]) != list: + self.data[key] = [self.data[key]] + self.data[key].append(val) + else: + self.data.update({key:val}) + +class Entities: + def __init__(self): + self.entities = [] + self.last = None + def new_entity(self, _type): + e = Entity(_type) + self.entities.append(e) + self.last = e + def update(self, value): + self.last.update(value) + +class Layer: + def __init__(self): + self.data = dict() + def update(self, value): + key = str(value[0]) + val = value[1] + if key in self.data: + if type(self.data[key]) != list: + self.data[key] = [self.data[key]] + self.data[key].append(val) + else: + self.data.update({key:val}) + +class Layers: + def __init__(self): + self.layers = [] + self.last = None + def new_layer(self): + e = Layer() + self.layers.append(e) + self.last = e + def update(self, value): + self.last.update(value) + +class Block: + def __init__(self, master): + self.master = master + self.data = dict() + self.entities = [] + self.le = None + def new_entity(self, value): + self.le = Entity(value) + self.entities.append(self.le) + def update(self, value): + if self.le == None: + val = str(value[0]) + self.data.update({val:value[1]}) + if val == "2": + self.master.blocks[value[1]] = self + else: + self.le.update(value) + +class Blocks: + def __init__(self): + self.blocks = dict() + self.last_var = None + def new_block(self): + b = Block(self) + self.last_block = b + self.last_var = b + def new_entity(self, value): + self.last_block.new_entity(value) + def update(self, value): + self.last_block.update(value) + +class DXF_CLASS: + def __init__(self): + self.units = 0 + self.dxf_messages = "" + self.coords = [] + self.cut_coords = [] + self.eng_coords = [] + strings = [] + floats = [] + ints = [] + self.layer_color=dict() + + strings += list(range(0, 10)) #String (255 characters maximum; less for Unicode strings) + floats += list(range(10, 60)) #Double precision 3D point + ints += list(range(60, 80)) #16-bit integer value + ints += list(range(90,100)) #32-bit integer value + strings += [100] #String (255 characters maximum; less for Unicode strings) + strings += [102] #String (255 characters maximum; less for Unicode strings + strings += [105] #String representing hexadecimal (hex) handle value + floats += list(range(140, 148)) #Double precision scalar floating-point value + ints += list(range(170, 176)) #16-bit integer value + ints += list(range(280, 290)) #8-bit integer value + strings += list(range(300, 310)) #Arbitrary text string + strings += list(range(310, 320)) #String representing hex value of binary chunk + strings += list(range(320, 330)) #String representing hex handle value + strings += list(range(330, 369)) #String representing hex object IDs + strings += [999] #Comment (string) + strings += list(range(1000, 1010))#String (255 characters maximum; less for Unicode strings) + floats += list(range(1010, 1060)) #Floating-point value + ints += list(range(1060, 1071)) #16-bit integer value + ints += [1071] #32-bit integer value + + + self.funs = [] + for i in range(0,1072): + self.funs.append(self.read_none) + + for i in strings: + self.funs[i] = self.read_string + + for i in floats: + self.funs[i] = self.read_float + + for i in ints: + self.funs[i] = self.read_int + + self.unit_vals = ["Unitless", + "Inches", + "Feet", + "Miles", + "Millimeters", + "Centimeters", + "Meters", + "Kilometers", + "Microinches", + "Mils"] + self.POLY_FLAG = None + self.POLY_CLOSED = None + + def dxf_message(self,text): + self.dxf_messages = self.dxf_messages + "\n" + text + + def read_int(self,data): + return int(float(data)) + + def read_float(self,data): + return float(data) + + def read_string(self,data): + return str(data) + + def read_none(self,data): + return None + +### def read_dxf_file(self, name, data): +### fd = file(name) +### Skip = True +### for line in fd: +### group_code = int(line) +### +### value = fd.next().replace('\r', '') +### value = value.replace('\n', '') +### value = value.lstrip(' ') +### value = value.rstrip(' ') +### value = self.funs[group_code](value) +### if (value != "SECTION") and Skip: +### continue +### else: +### Skip = False +### data.append((group_code, value)) +### fd.close() + + def read_dxf_data(self, fd, data): + self.comment="None" + Skip = True + fd_iter = iter(fd) + for line in fd_iter: + try: + group_code = int(line) + value = next(fd_iter).replace('\r', '') + value = value.replace('\n', '') + value = value.lstrip(' ') + value = value.rstrip(' ') + value = self.funs[group_code](value) + if (value != "SECTION") and Skip: + if group_code==999: + self.comment=value + continue + else: + Skip = False + data.append((group_code, value)) + except: + pass + + ########################################################################## + # routine takes an sin and cos and returns the angle (between 0 and 360) # + ########################################################################## + def Get_Angle(self,y,x): + angle = 90.0-degrees(atan2(x,y)) + if angle < 0: + angle = 360 + angle + return angle + + ############################################################################ + # routine takes an x and a y coords and does a coordinate transformation # + # to a new coordinate system at angle from the initial coordinate system # + # Returns new x,y tuple # + ############################################################################ + def Transform(self,x,y,angle): + newx = x * cos(angle) - y * sin(angle) + newy = x * sin(angle) + y * cos(angle) + return newx,newy + + def bulge_coords(self,x0,y0,x1,y1,bulge,lin_tol=.001): + global Zero + bcoords=[] + if bulge < 0.0: + sign = 1 + bulge=abs(bulge) + else: + sign = -1 + + dx = x1-x0 + dy = y1-y0 + c = sqrt(dx**2 + dy**2) + alpha = 2.0 * (atan(bulge)) + R = c / (2*sin(alpha)) + L = R * cos(alpha) + + if (lin_tol < R): + step_deg = 2*acos((R-lin_tol)/R)*180/pi + else: + step_deg = 45 + steps = ceil(2*alpha / radians(step_deg)) + + if abs(c) < Zero: + phi = 0 + bcoords.append([x0,y0,x1,y1]) + return bcoords + + seg_sin = dy/c + seg_cos = dx/c + phi = self.Get_Angle(seg_sin,seg_cos) + + d_theta = 2*alpha / steps + theta = alpha - d_theta + + xa = x0 + ya = y0 + for i in range(1,int(steps)): + xp = c/2 - R*sin(theta) + yp = R*cos(theta) - L + xb,yb = self.Transform(xp,yp*sign,radians(phi)) + xb=xb+x0 + yb=yb+y0 + + bcoords.append([xa,ya,xb,yb]) + xa = xb + ya = yb + theta = theta -d_theta + bcoords.append([xa,ya,x1,y1]) + return bcoords + + def add_coords(self,line,offset,scale,rotate,color=None,layer=None): + slcolor = 0 + if(type(layer)!=list): + if layer in self.layer_color: + slcolor = self.layer_color[layer] + else: + for layer_item in layer: + if layer_item in self.layer_color: + slcolor = self.layer_color[layer_item] + + # Now test for Hidden layer IE Color < 0 + if ( slcolor != None) and (slcolor < 0): + return + if (color == None) or (color == 256): # 256 is ColorByLayer + # default to layer color + color = slcolor + if ( color != None) and (color < 0): + return + + x0s = line[0]*scale[0] + y0s = line[1]*scale[1] + x1s = line[2]*scale[0] + y1s = line[3]*scale[1] + + if abs(rotate) > Zero: + rad = radians(rotate) + x0r = x0s*cos(rad) - y0s*sin(rad) + y0r = x0s*sin(rad) + y0s*cos(rad) + x1r = x1s*cos(rad) - y1s*sin(rad) + y1r = x1s*sin(rad) + y1s*cos(rad) + else: + x0r = x0s + y0r = y0s + x1r = x1s + y1r = y1s + + x0 = x0r + offset[0] + y0 = y0r + offset[1] + x1 = x1r + offset[0] + y1 = y1r + offset[1] + + # Check if line is blue + Color_Blue = (color == 5) or (color >= 140 and color <=180) + try: + Layer_Engrave = str.find(layer.upper(),'ENGRAVE') != -1 + except: + Layer_Engrave = False + try: + for lay in layer: + Layer_Engrave = (str.find(lay.upper(),'ENGRAVE') != -1) or Layer_Engrave + except: + pass + + if Color_Blue or Layer_Engrave: + self.eng_coords.append([x0,y0,x1,y1]) + else: #if (color >= 10 and color <=28) or (color >= 230 and color <=249): #red + self.cut_coords.append([x0,y0,x1,y1]) + + self.coords.append([x0,y0,x1,y1]) + + def eval_entity(self,e,bl,lin_tol=.001,offset=[0,0],scale=[1,1],rotate=0): + try: + color = e.data["62"] + except: + color = None + if ( color != None) and (color < 0): + return + try: + layer = e.data["8"] + except: + layer = 0 + + slcolor = 0 + if(type(layer)!=list): + if layer in self.layer_color: + slcolor = self.layer_color[layer] + else: + for layer_item in layer: + if layer_item in self.layer_color: + slcolor = self.layer_color[layer_item] + + # Now test for Hidden layer IE Color < 0 + if ( slcolor != None) and (slcolor < 0): + return + + ############# LINE ############ + if e.type == "LINE": + x0 = e.data["10"] + y0 = e.data["20"] + x1 = e.data["11"] + y1 = e.data["21"] + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + ############# ARC ############# + elif e.type == "ARC": + x = e.data["10"] + y = e.data["20"] + r = e.data["40"] + start = e.data["50"] + end = e.data["51"] + + if end < start: + end=end+360.0 + delta = end-start + + if (lin_tol < r): + step_deg = 2*acos((r-lin_tol)/r)*180/pi + else: + step_deg = 45 + angle_steps = max(floor(delta/step_deg),2) + + start_r = radians(start) + end_r = radians(end) + + step_phi = radians( delta/angle_steps ) + x0 = x + r * cos(start_r) + y0 = y + r * sin(start_r) + pcnt = 1 + while pcnt < angle_steps+1: + phi = start_r + pcnt*step_phi + x1 = x + r * cos(phi) + y1 = y + r * sin(phi) + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + x0=x1 + y0=y1 + pcnt += 1 + + ######### LWPOLYLINE ########## + elif e.type == "LWPOLYLINE": + flag=0 + lpcnt=-1 + try: + xy_data = zip(e.data["10"], e.data["20"]) + except: + try: + xy_data = [[e.data["10"], e.data["20"]]] + except: + self.dxf_message("DXF Import zero length %s Ignored" %(e.type)) + xy_data = [] + for x,y in xy_data: + x1 = x + y1 = y + lpcnt=lpcnt+1 + try: + bulge1 = e.data["42"][lpcnt] + except: + bulge1 = 0 + + if flag==0: + x0=x1 + y0=y1 + bulge0=bulge1 + flag=1 + else: + if bulge0 != 0: + bcoords = self.bulge_coords(x0,y0,x1,y1,bulge0,lin_tol=lin_tol) + for line in bcoords: + self.add_coords(line,offset,scale,rotate,color,layer) + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + x0 = x1 + y0 = y1 + bulge0 = bulge1 + + if (e.data["70"] & 1): #check if bit-code 1 is set for closed polygon + try: + x1 = e.data["10"][0] + y1 = e.data["20"][0] + except: + x1 = e.data["10"] + y1 = e.data["20"] + if bulge0 != 0: + bcoords = self.bulge_coords(x0,y0,x1,y1,bulge1,lin_tol=lin_tol) + for line in bcoords: + self.add_coords(line,offset,scale,rotate,color,layer) + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + ########### CIRCLE ############ + elif e.type == "CIRCLE": + x = e.data["10"] + y = e.data["20"] + r = e.data["40"] + + start = 0 + end = 360 + if end < start: + end=end+360.0 + delta = end-start + + if (lin_tol < r): + step_deg = 2*acos((r-lin_tol)/r)*180/pi + else: + step_deg = 45 + angle_steps = max(floor(delta/step_deg),2) + + start_r = radians( start ) + end_r = radians( end ) + + step_phi = radians( delta/angle_steps) + x0 = x + r * cos(start_r) + y0 = y + r * sin(start_r) + pcnt = 1 + while pcnt < angle_steps+1: + phi = start_r + pcnt*step_phi + x1 = x + r * cos(phi) + y1 = y + r * sin(phi) + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + x0=x1 + y0=y1 + pcnt += 1 + + ############ SPLINE ########### + elif e.type == "SPLINE": + self.Spline_flag=[] + self.degree=1 + self.Knots=[] + self.Weights=[] + self.CPoints=[] + + self.Spline_flag = int(e.data["70"]) + self.degree = int(e.data["71"]) + self.Knots = e.data["40"] + try: + self.Weights = e.data["41"] + except: + for K in self.Knots: + self.Weights.append(1) + pass + + kmin = min(self.Knots) + kmax = max(self.Knots) + for i in range(len(self.Knots)): + self.Knots[i] = (self.Knots[i]-kmin)/(kmax-kmin) + + try: + xy_data = zip(e.data["10"], e.data["20"]) + except: + self.dxf_message("DXF Import zero length %s Ignored" %(e.type)) + xy_data = [] + + if xy_data!=[]: + for x,y in xy_data: + self.CPoints.append(PointClass(float(x), float(y))) + + self.MYNURBS=NURBSClass(degree=self.degree, \ + Knots=self.Knots, \ + Weights=self.Weights,\ + CPoints=self.CPoints) + + mypoints=self.MYNURBS.calc_curve(n=0, lin_tol=lin_tol) + flag = 0 + for XY in mypoints: + x1 = XY.x + y1 = XY.y + if flag==0: + x0=x1 + y0=y1 + flag=1 + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + x0=x1 + y0=y1 + + ########### ELLIPSE ########### + elif e.type == "ELLIPSE": + #X and Y center points + xcp = e.data["10"] + ycp = e.data["20"] + + #X and Y of major axis end point + xma = e.data["11"] + yma = e.data["21"] + + #Ratio of minor axis to major axis + ratio = e.data["40"] + + #Start and end angles (in radians 0 and 2pi for full ellipse) + start = degrees( e.data["41"] ) + end = degrees( e.data["42"] ) + + rotation = atan2(yma, xma) + a = sqrt(xma**2 + yma**2) + b = a * ratio + + ################## + if end < start: + end=end+360.0 + delta = end-start + + + start_r = radians( start ) + end_r = radians( end ) + + tol = radians(2) + phi = start_r + x1 = xcp + ( a*cos(phi) * cos(rotation) - b*sin(phi) * sin(rotation) ); + y1 = ycp + ( a*cos(phi) * sin(rotation) + b*sin(phi) * cos(rotation) ); + step=tol + while phi < end_r: + if (phi+step > end_r): + step = end_r-phi + + x2 = xcp + ( a*cos(phi+step) * cos(rotation) - b*sin(phi+step) * sin(rotation) ); + y2 = ycp + ( a*cos(phi+step) * sin(rotation) + b*sin(phi+step) * cos(rotation) ); + + x_test = xcp + ( a*cos(phi+step/2) * cos(rotation) - b*sin(phi+step/2) * sin(rotation) ); + y_test = ycp + ( a*cos(phi+step/2) * sin(rotation) + b*sin(phi+step/2) * cos(rotation) ); + + x_mid = (x1+x2)/2 + y_mid = (y1+y2)/2 + dx_mid = x_mid-x_test + dy_mid = y_mid-y_test + delta = sqrt(dx_mid*dx_mid + dy_mid*dy_mid) + + if delta > lin_tol: + step = step/2 + else: + phi+=step + self.add_coords([x1,y1,x2,y2],offset,scale,rotate,color,layer) + step = step*2 + x1=x2 + y1=y2 + + ########### LEADER ########### + elif e.type == "LEADER": + flag=0 + + try: + xy_data = zip(e.data["10"], e.data["20"]) + except: + self.dxf_message("DXF Import zero length %s Ignored" %(e.type)) + xy_data = [] + + for x,y in xy_data: + x1 = x + y1 = y + if flag==0: + x0=x1 + y0=y1 + flag=1 + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + x0=x1 + y0=y1 + + ########### POLYLINE ########### + elif e.type == "POLYLINE": + self.POLY_CLOSED = 0 + self.POLY_FLAG = -1 + try: + TYPE=e.data["70"] + if TYPE >=128: + #print "#128 = The linetype pattern is generated continuously around the vertices of this polyline." + TYPE=TYPE-128 + if TYPE >=64: + #print "#64 = The polyline is a polyface mesh." + TYPE=TYPE-64 + if TYPE >=32: + #print "#32 = The polygon mesh is closed in the N direction." + TYPE=TYPE-32 + if TYPE >=16: + #print "#16 = This is a 3D polygon mesh." + TYPE=TYPE-16 + if TYPE >=8: + #print "#8 = This is a 3D polyline." + TYPE=TYPE-8 + if TYPE >=4: + #print "#4 = Spline-fit vertices have been added." + TYPE=TYPE-4 + if TYPE >=2: + #print "#2 = Curve-fit vertices have been added." + TYPE=TYPE-2 + if TYPE >=1: + #print "#1 = This is a closed polyline (or a polygon mesh closed in the M direction)." + self.POLY_CLOSED=1 + TYPE=TYPE-1 + except: + pass + + ########### SEQEND ########### + elif e.type == "SEQEND": + if (self.POLY_FLAG == 1): # None): + self.POLY_FLAG=None + if (self.POLY_CLOSED==1): + self.POLY_CLOSED==0 + x0 = self.PX + y0 = self.PY + x1 = self.PX0 + y1 = self.PY0 + + if self.bulge != 0: + bcoords = self.bulge_coords(x0,y0,x1,y1,self.bulge,lin_tol=lin_tol) + for line in bcoords: + self.add_coords(line,offset,scale,rotate,color,layer) + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + + else: + self.dxf_message("DXF Import Ignored: - %s - Entity" %(e.type)) + + ########### VERTEX ########### + elif e.type == "VERTEX": + SKIP=False + + try: + TYPE=e.data["70"] + #print "TYPE=",TYPE + #if (TYPE==0 or TYPE==8): + # pass + #elif (TYPE==1): + # self.POLY_CLOSED=1 + + if TYPE >=128: + #print "128 = Polyface mesh vertex" + TYPE=TYPE-128 + if TYPE >=64: + #print "64 = 3D polygon mesh" + TYPE=TYPE-64 + if TYPE >=32: + #print "32 = 3D polyline vertex" + TYPE=TYPE-32 + if TYPE >=16: + #print "16 = Spline frame control point" + TYPE=TYPE-16 + SKIP=True + if TYPE >=8: + #print "8 = Spline vertex created by spline-fitting" + TYPE=TYPE-8 + if TYPE >=4: + #print "4 = Not used" + TYPE=TYPE-4 + if TYPE >=2: + #print "2 = Curve-fit tangent defined for this vertex. A curve-fit tangent direction of 0 may be omitted from DXF output but is significant if this bit is set." + TYPE=TYPE-2 + if TYPE >=1: + #print "1 = Extra vertex created by curve-fitting" + TYPE=TYPE-1 + except: + pass + + if (self.POLY_FLAG == 1 and not SKIP): + x0 = self.PX + y0 = self.PY + x1 = e.data["10"] + y1 = e.data["20"] + self.PX=x1 + self.PY=y1 + + if self.bulge != 0: + bcoords = self.bulge_coords(x0,y0,x1,y1,self.bulge,lin_tol=lin_tol) + for line in bcoords: + self.add_coords(line,offset,scale,rotate,color,layer) + else: + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + try: + self.bulge = e.data["42"] + except: + self.bulge = 0 + + elif (self.POLY_FLAG == -1 and not SKIP): + self.PX = e.data["10"] + self.PY = e.data["20"] + self.PX0 = self.PX + self.PY0 = self.PY + try: + self.bulge = e.data["42"] + except: + self.bulge = 0 + self.POLY_FLAG = 1 + + else: + self.dxf_message("DXF Import Ignored: - %s - Entity" %(e.type)) + + ########### END VERTEX ########### + ########### INSERT ########### + elif e.type == "INSERT": + key = e.data["2"] + xoff = e.data["10"]+offset[0] + yoff = e.data["20"]+offset[1] + + try: + xscale = e.data["41"] + except: + xscale = 1 + try: + yscale = e.data["42"] + except: + yscale = 1 + try: + rotate = e.data["50"] + except: + rotate = 0 + + try: + x_block_ref = bl.blocks[key].data.get("10") + y_block_ref = bl.blocks[key].data.get("20") + except: + x_block_ref = 0 + y_block_ref = 0 + + xoff = xoff - x_block_ref + yoff = yoff - y_block_ref + + for e in bl.blocks[key].entities: + self.eval_entity(e,bl,lin_tol,offset=[xoff,yoff],scale=[xscale,yscale],rotate=rotate) + + ########### END INSERT ########### + + elif e.type == "SOLID": + x0 = e.data["10"] + y0 = e.data["20"] + x1 = e.data["11"] + y1 = e.data["21"] + x2 = e.data["12"] + y2 = e.data["22"] + try: + x3 = e.data["13"] + y3 = e.data["23"] + except: + x3 = x2 + y3 = y2 + self.add_coords([x0,y0,x1,y1],offset,scale,rotate,color,layer) + self.add_coords([x1,y1,x3,y3],offset,scale,rotate,color,layer) + self.add_coords([x3,y3,x2,y2],offset,scale,rotate,color,layer) + self.add_coords([x2,y2,x0,y0],offset,scale,rotate,color,layer) + + elif e.type == "HATCH": + #quietly ignore HATCH + pass + else: + self.dxf_message("DXF Import Ignored: %s Entity" %(e.type)) + pass + + + def GET_DXF_DATA(self,fd, lin_tol=.001,get_units=False,units=None): + data = [] + try: + self.read_dxf_data(fd, data) + except: + self.dxf_message("\nUnable to read input DXF data!") + return 1 + data = iter(data) + g_code, value = None, None + sections = dict() + + he = Header() + bl = Blocks() + la = Layers() + while value != "EOF": + g_code, value = next(data) + if value == "SECTION": + g_code, value = next(data) + sections[value] = [] + + while value != "ENDSEC": + + + if value == "HEADER": + while True: + g_code, value = next(data) + if value == "ENDSEC": + break + elif g_code == 9: + he.new_var(value) + else: + he.new_val((g_code, value)) + + elif value == "BLOCKS": + while True: + g_code, value = next(data) + if value == "ENDSEC": + break + elif value == "ENDBLK": + continue + elif value == "BLOCK": + bl.new_block() + elif g_code == 0 and value != "BLOCK": + bl.new_entity(value) + else: + bl.update((g_code, value)) + + elif value == "ENTITIES": + TYPE="" + en = Entities() + while True: + g_code, value = next(data) + + ################################### + if g_code==0: + TYPE = value + if TYPE == "LWPOLYLINE" and g_code==10 and g_code_last==20: + # Add missing code 42 + en.update((42, 0.0)) + g_code_last = g_code + ################################### + + if value == "ENDSEC": + break + elif g_code == 0 and value != "ENDSEC": + en.new_entity(value) + else: + en.update((g_code, value)) + + #------------------------------------------------------------------ + elif value == "TABLE": + while True: + g_code, value = next(data) + if value == "AcDbLayerTableRecord" or value == "LAYER": + la.new_layer() + while True: + g_code, value = next(data) + if g_code==0: + if value == "AcDbLayerTableRecord" or value == "LAYER": + la.new_layer() + else: + break + la.update((g_code, value)) + if value == "ENDTAB": + break + try: + g_code, value = next(data) + except: + break + + #Process Units + try: + unit_val = he.variables.get("$INSUNITS").get("70") + self.units = self.unit_vals[int(unit_val)] + except: + self.units = self.unit_vals[0] + + if get_units: + return + + #Process Layers + for l in la.layers: + try: + color = l.data["62"] + except: + color = None + try: + name = l.data["2"] + except: + name = None + self.layer_color[name]=color + + #Process Entities + for e in en.entities: + self.eval_entity(e,bl,lin_tol) + + + def DXF_COORDS_GET(self,new_origin=True): + if (new_origin==True): + ymin=99999 + xmin=99999 + for line in self.coords: + XY=line + if XY[0] < xmin: + xmin = XY[0] + if XY[1] < ymin: + ymin = XY[1] + if XY[2] < xmin: + xmin = XY[2] + if XY[3] < ymin: + ymin = XY[3] + else: + xmin=0 + ymin=0 + + coords_out=[] + for line in self.coords: + XY=line + coords_out.append([XY[0]-xmin, XY[1]-ymin, XY[2]-xmin, XY[3]-ymin]) + return coords_out + + + def DXF_COORDS_GET_TYPE(self,engrave=True,new_origin=True): + if engrave==True: + coords = self.eng_coords + else: + coords = self.cut_coords + + if (new_origin==True): + ymin=99999 + xmin=99999 + for line in coords: + XY=line + if XY[0] < xmin: + xmin = XY[0] + if XY[1] < ymin: + ymin = XY[1] + if XY[2] < xmin: + xmin = XY[2] + if XY[3] < ymin: + ymin = XY[3] + else: + xmin=0 + ymin=0 + + coords_out=[] + for line in coords: + XY=line + coords_out.append([XY[0]-xmin, XY[1]-ymin, XY[2]-xmin, XY[3]-ymin]) + return coords_out + + + + ################################################## + ### Begin Dxf_Write G-Code Writing Function ### + ################################################## + def WriteDXF(self,close_loops=False): + + if close_loops: + self.V_Carve_It(clean_flag=0,DXF_FLAG = close_loops) + + dxf_code = [] + # Create a header section just in case the reading software needs it + dxf_code.append("999") + dxf_code.append("DXF created by dxf library ") + + dxf_code.append("0") + dxf_code.append("SECTION") + dxf_code.append("2") + dxf_code.append("HEADER") + dxf_code.append("0") + dxf_code.append("ENDSEC") + # + #Tables Section + #These can be used to specify predefined constants, line styles, text styles, view + #tables, user coordinate systems, etc. We will only use tables to define some layers + #for use later on. Note: not all programs that support DXF import will support + #layers and those that do usually insist on the layers being defined before use + # + # The following will initialise layers 1 and 2 for use with moves and rapid moves. + dxf_code.append("0") + dxf_code.append("SECTION") + dxf_code.append("2") + dxf_code.append("TABLES") + dxf_code.append("0") + dxf_code.append("TABLE") + dxf_code.append("2") + dxf_code.append("LTYPE") + dxf_code.append("70") + dxf_code.append("1") + dxf_code.append("0") + dxf_code.append("LTYPE") + dxf_code.append("2") + dxf_code.append("CONTINUOUS") + dxf_code.append("70") + dxf_code.append("64") + dxf_code.append("3") + dxf_code.append("Solid line") + dxf_code.append("72") + dxf_code.append("65") + dxf_code.append("73") + dxf_code.append("0") + dxf_code.append("40") + dxf_code.append("0.000000") + dxf_code.append("0") + dxf_code.append("ENDTAB") + dxf_code.append("0") + dxf_code.append("TABLE") + dxf_code.append("2") + dxf_code.append("LAYER") + dxf_code.append("70") + dxf_code.append("6") + dxf_code.append("0") + dxf_code.append("LAYER") + dxf_code.append("2") + dxf_code.append("1") + dxf_code.append("70") + dxf_code.append("64") + dxf_code.append("62") + dxf_code.append("7") + dxf_code.append("6") + dxf_code.append("CONTINUOUS") + dxf_code.append("0") + dxf_code.append("LAYER") + dxf_code.append("2") + dxf_code.append("2") + dxf_code.append("70") + dxf_code.append("64") + dxf_code.append("62") + dxf_code.append("7") + dxf_code.append("6") + dxf_code.append("CONTINUOUS") + dxf_code.append("0") + dxf_code.append("ENDTAB") + dxf_code.append("0") + dxf_code.append("TABLE") + dxf_code.append("2") + dxf_code.append("STYLE") + dxf_code.append("70") + dxf_code.append("0") + dxf_code.append("0") + dxf_code.append("ENDTAB") + dxf_code.append("0") + dxf_code.append("ENDSEC") + + #This block section is not necessary but apperantly it's good form to include one anyway. + #The following is an empty block section. + dxf_code.append("0") + dxf_code.append("SECTION") + dxf_code.append("2") + dxf_code.append("BLOCKS") + dxf_code.append("0") + dxf_code.append("ENDSEC") + + # Start entities section + dxf_code.append("0") + dxf_code.append("SECTION") + dxf_code.append("2") + dxf_code.append("ENTITIES") + dxf_code.append(" 0") + + ################################# + ## GCODE WRITING for Dxf_Write ## + ################################# + #for line in side: + for line in self.coords: + XY = line + + #if line[0] == 1 or (line[0] == 0 and Rapids): + dxf_code.append("LINE") + dxf_code.append(" 5") + dxf_code.append("30") + dxf_code.append("100") + dxf_code.append("AcDbEntity") + dxf_code.append(" 8") #layer Code #dxf_code.append("0") + + ########################## + #if line[0] == 1: + # dxf_code.append("1") + #else: + # dxf_code.append("2") + #dxf_code.append(" 62") #color code + #if line[0] == 1: + # dxf_code.append("10") + #else: + # dxf_code.append("150") + dxf_code.append("1") + dxf_code.append(" 62") #color code + dxf_code.append("150") + ########################### + + dxf_code.append("100") + dxf_code.append("AcDbLine") + dxf_code.append(" 10") + dxf_code.append("%.4f" %(line[0])) #x1 coord + dxf_code.append(" 20") + dxf_code.append("%.4f" %(line[1])) #y1 coord + dxf_code.append(" 30") + dxf_code.append("%.4f" %(0)) #z1 coord + dxf_code.append(" 11") + dxf_code.append("%.4f" %(line[2])) #x2 coord + dxf_code.append(" 21") + dxf_code.append("%.4f" %(line[3])) #y2 coord + dxf_code.append(" 31") + dxf_code.append("%.4f" %(0)) #z2 coord + dxf_code.append(" 0") + + dxf_code.append("ENDSEC") + dxf_code.append("0") + dxf_code.append("EOF") + ###################################### + ## END G-CODE WRITING for Dxf_Write ## + ###################################### + return dxf_code + diff --git a/ecoords.py b/ecoords.py new file mode 100644 index 0000000..4160173 --- /dev/null +++ b/ecoords.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +""" + Copyright (C) <2018> + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +""" +from math import * + +class ECoord: + def __init__(self): + self.reset() + + def reset(self): + self.image = None + self.reset_path() + + def reset_path(self): + self.ecoords = [] + self.len = None + self.move = 0 + self.sorted = False + self.rpaths = False + self.bounds = (0,0,0,0) + self.gcode_time = 0 + self.hull_coords= [] + self.n_scanlines= 0 + + def make_ecoords(self,coords,scale=1): + self.reset() + self.len = 0 + self.move = 0 + + xmax, ymax = -1e10, -1e10 + xmin, ymin = 1e10, 1e10 + self.ecoords=[] + Acc=.001 + oldx = oldy = -99990.0 + first_stroke = True + loop=0 + for line in coords: + XY = line + x1 = XY[0]*scale + y1 = XY[1]*scale + x2 = XY[2]*scale + y2 = XY[3]*scale + dxline= x2-x1 + dyline= y2-y1 + len_line=sqrt(dxline*dxline + dyline*dyline) + if len_line==0.0: + continue + dx = oldx - x1 + dy = oldy - y1 + dist = sqrt(dx*dx + dy*dy) + # check and see if we need to move to a new discontinuous start point + if (dist > Acc) or first_stroke: + loop = loop+1 + self.ecoords.append([x1,y1,loop]) + if not first_stroke: + self.move = self.move + dist + first_stroke = False + + self.len = self.len + len_line + self.ecoords.append([x2,y2,loop]) + oldx, oldy = x2, y2 + xmax=max(xmax,x1,x2) + ymax=max(ymax,y1,y2) + xmin=min(xmin,x1,x2) + ymin=min(ymin,y1,y2) + self.bounds = (xmin,xmax,ymin,ymax) + + def set_ecoords(self,ecoords,data_sorted=False): + self.ecoords = ecoords + self.computeEcoordsLen() + self.data_sorted=data_sorted + + def set_image(self,PIL_image): + self.image = PIL_image + self.reset_path() + + def computeEcoordsLen(self): + xmax, ymax = -1e10, -1e10 + xmin, ymin = 1e10, 1e10 + + if self.ecoords == [] : + self.len=0 + return + on = 0 + move = 0 + time = 0 + for i in range(2,len(self.ecoords)): + x1 = self.ecoords[i-1][0] + y1 = self.ecoords[i-1][1] + x2 = self.ecoords[i][0] + y2 = self.ecoords[i][1] + loop = self.ecoords[i ][2] + loop_last = self.ecoords[i-1][2] + + xmax=max(xmax,x1,x2) + ymax=max(ymax,y1,y2) + xmin=min(xmin,x1,x2) + ymin=min(ymin,y1,y2) + + dx = x2-x1 + dy = y2-y1 + dist = sqrt(dx*dx + dy*dy) + + if len(self.ecoords[i]) > 3: + feed = self.ecoords[i][3] + time = time + dist/feed*60 + + if loop == loop_last: + on = on + dist + else: + move = move + dist + + self.bounds = (xmin,xmax,ymin,ymax) + self.len = on + self.move = move + self.gcode_time = time + diff --git a/egv.py b/egv.py new file mode 100644 index 0000000..85cd829 --- /dev/null +++ b/egv.py @@ -0,0 +1,727 @@ +#!/usr/bin/env python +''' +This script reads/writes egv format + +Copyright (C) 2017-2022 Scorch www.scorchworks.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +''' + +import sys +import struct +import os +from shutil import copyfile +from math import * +from interpolate import interpolate +from time import time +from LaserSpeed import LaserSpeed + +############################################################################## +class egv: + def __init__(self, target=lambda s: sys.stdout.write(s)): + self.write = target + self.Modal_dir = 0 + self.Modal_dist = 0 + self.Modal_on = False + self.Modal_AX = 0 + self.Modal_AY = 0 + + self.RIGHT = 66 #ord("B")=66 + self.LEFT = 84 #ord("T")=84 + self.UP = 76 #ord("L")=76 + self.DOWN = 82 #ord("R")=82 + self.ANGLE = 77 #ord("M")=77 + self.ON = 68 #ord("D")=68 + self.OFF = 85 #ord("U")=85 + + # % Yxtart % Xstart % Yend % Xend % I % C VXXXXXXX CUT_TYPE + # + # %Ystart_pos %Xstart_pos %Yend_pos %Xend_pos (start pos is the location of the head before the code is run) + # I is always I ? + # C is C for cutting or Marking otherwise it is omitted + # V is the start of 7 digits indicating the feed rate 255 255 1 + # CUT_TYPE cutting/marking, Engraving=G followed by the raster step in thousandths of an inch + + def move(self,direction,distance,laser_on=False,angle_dirs=None): + + if angle_dirs==None: + angle_dirs = [self.Modal_AX,self.Modal_AY] + + if direction == self.Modal_dir \ + and laser_on == self.Modal_on \ + and angle_dirs[0] == self.Modal_AX \ + and angle_dirs[1] == self.Modal_AY: + self.Modal_dist = self.Modal_dist + distance + + else: + self.flush() + if laser_on != self.Modal_on: + if laser_on: + self.write(self.ON) + else: + self.write(self.OFF) + self.Modal_on = laser_on + + if direction == self.ANGLE: + if angle_dirs[0]!=self.Modal_AX: + self.write(angle_dirs[0]) + self.Modal_AX = angle_dirs[0] + if angle_dirs[1]!=self.Modal_AY: + self.write(angle_dirs[1]) + self.Modal_AY = angle_dirs[1] + + self.Modal_dir = direction + self.Modal_dist = distance + + if direction == self.RIGHT or direction == self.LEFT: + self.Modal_AX = direction + if direction == self.UP or direction == self.DOWN: + self.Modal_AY = direction + + + def flush(self,laser_on=None): + if self.Modal_dist > 0: + self.write(self.Modal_dir) + for code in self.make_distance(self.Modal_dist): + self.write(code) + if (laser_on!=None) and (laser_on!=self.Modal_on): + if laser_on: + self.write(self.ON) + else: + self.write(self.OFF) + self.Modal_on = laser_on + self.Modal_dist = 0 + + def make_distance(self,dist_mils): + dist_mils=float(dist_mils) + if abs(dist_mils-round(dist_mils,0)) > 0.000001: + raise Exception('Distance values should be integer value (inches*1000)') + DIST=0.0 + code = [] + v122 = 255 + dist_milsA = int(dist_mils) + + for i in range(0,int(floor(dist_mils/v122))): + code.append(122) + dist_milsA = dist_milsA-v122 + DIST = DIST+v122 + if dist_milsA==0: + pass + elif dist_milsA < 26: # codes "a" through "y" + code.append(96+dist_milsA) + elif dist_milsA < 52: # codes "|a" through "|z" + code.append(124) + code.append(96+dist_milsA-25) + elif dist_milsA < 255: + num_str = "%03d" %(int(round(dist_milsA))) + code.append(ord(num_str[0])) + code.append(ord(num_str[1])) + code.append(ord(num_str[2])) + else: + raise Exception("Error in EGV make_distance_in(): dist_milsA=",dist_milsA) + return code + + def make_dir_dist(self,dxmils,dymils,laser_on=False): + adx = abs(dxmils) + ady = abs(dymils) + if adx > 0 or ady > 0: + if ady > 0: + if dymils > 0: + self.move(self.UP ,ady,laser_on) + else: + self.move(self.DOWN,ady,laser_on) + if adx > 0: + if dxmils > 0: + self.move(self.RIGHT,adx,laser_on) + else: + self.move(self.LEFT ,adx,laser_on) + + def make_cut_line(self,dxmils,dymils,Spindle): + XCODE = self.RIGHT + if dxmils < 0.0: + XCODE = self.LEFT + YCODE = self.UP + if dymils < 0.0: + YCODE = self.DOWN + + if abs(dxmils-round(dxmils,0)) > 0.0 or abs(dymils-round(dymils,0)) > 0.0: + raise Exception('Distance values should be integer value (inches*1000)') + + adx = abs(dxmils/1000.0) + ady = abs(dymils/1000.0) + + if dxmils == 0: + self.move(YCODE,abs(dymils),laser_on=Spindle) + elif dymils == 0: + self.move(XCODE,abs(dxmils),laser_on=Spindle) + elif dxmils==dymils: + self.move(self.ANGLE,abs(dxmils),laser_on=Spindle,angle_dirs=[XCODE,YCODE]) + else: + h=[] + if adx > ady: + slope = ady/adx + n = int(abs(dxmils)) + CODE = XCODE + CODE1 = YCODE + else: + slope = adx/ady + n = int(abs(dymils)) + CODE = YCODE + CODE1 = XCODE + + for i in range(1,n+1): + h.append(round(i*slope,0)) + + Lh=0.0 + d1=0.0 + d2=0.0 + d1cnt=0.0 + d2cnt=0.0 + for i in range(len(h)): + if h[i]==Lh: + d1=d1+1 + if d2>0.0: + self.move(self.ANGLE,d2,laser_on=Spindle,angle_dirs=[XCODE,YCODE]) + d2cnt=d2cnt+d2 + d2=0.0 + else: + d2=d2+1 + if d1>0.0: + self.move(CODE,d1,laser_on=Spindle) + d1cnt=d1cnt+d1 + d1=0.0 + Lh=h[i] + + if d1>0.0: + self.move(CODE,d1,laser_on=Spindle) + d1cnt=d1cnt+d1 + d1=0.0 + if d2>0.0: + self.move(self.ANGLE,d2,laser_on=Spindle,angle_dirs=[XCODE,YCODE]) + d2cnt=d2cnt+d2 + d2=0.0 + + + DX = d2cnt + DY = (d1cnt+d2cnt) + if adx < ady: + error = max(DX-abs(dxmils),DY-abs(dymils)) + else: + error = max(DY-abs(dxmils),DX-abs(dymils)) + if error > 0: + raise Exception("egv.py: Error delta =%f" %(error)) + + + def make_speed(self,Feed=None,board_name="LASER-M2",Raster_step=0): + board_code = board_name.split('-')[1] + speed_text = LaserSpeed.get_code_from_speed(Feed, abs(Raster_step), board=board_code) + + speed=[] + for c in speed_text: + speed.append(ord(c)) + return speed + + + def make_move_data(self,dxmils,dymils): + if (abs(dxmils)+abs(dymils)) > 0: + self.write(73) # I + self.make_dir_dist(dxmils,dymils) + self.flush() + self.write(83) #S + self.write(49) #1 (one) + self.write(80) #P + + ####################################################################### + def none_function(self,dummy=None): + #Don't delete this function (used in make_egv_data) + pass + + def ecoord_adj(self,ecoords_adj_in,scale,FlipXoffset): + if FlipXoffset > 0: + e0 = int(round((FlipXoffset-ecoords_adj_in[0])*scale,0)) + else: + e0 = int(round(ecoords_adj_in[0]*scale,0)) + e1 = int(round(ecoords_adj_in[1]*scale,0)) + e2 = ecoords_adj_in[2] + return e0,e1,e2 + + + def make_egv_data(self, ecoords_in, + startX=0, + startY=0, + units = 'in', + Feed = None, + board_name="LASER-M2", + Raster_step=0, + update_gui=None, + stop_calc=None, + FlipXoffset=0, + Rapid_Feed_Rate=0, + use_laser=True): + + #print("make_egv_data",Rapid_Feed_Rate,len(ecoords_in)) + #print("Rapid_Feed_Rate=",Rapid_Feed_Rate) + ######################################################## + if stop_calc == None: + stop_calc=[] + stop_calc.append(0) + if update_gui == None: + update_gui = self.none_function + ######################################################## + if units == 'in': + scale = 1000.0 + if units == 'mm': + scale = 1000.0/25.4; + + startX = int(round(startX*scale,0)) + startY = int(round(startY*scale,0)) + + ######################################################## + variable_feed_scale=None + Spindle = True and use_laser + if Feed==None: + variable_feed_scale = 25.4/60.0 + Feed = round(ecoords_in[0][3]*variable_feed_scale,2) + Spindle = False + + speed = self.make_speed(Feed,board_name=board_name,Raster_step=Raster_step) + + ##self.write(ord("I")) + #for code in speed: + # self.write(code) + + if Raster_step==0: + #self.write(ord("I")) + for code in speed: + self.write(code) + + lastx,lasty,last_loop = self.ecoord_adj(ecoords_in[0],scale,FlipXoffset) + if not Rapid_Feed_Rate: + self.make_dir_dist(lastx-startX,lasty-startY) + + self.flush(laser_on=False) + self.write(ord("N")) + if lasty-startY <= 0: + self.write(self.DOWN) + else: + self.write(self.UP) + + if lastx-startX >= 0: + self.write(self.RIGHT) + else: + self.write(self.LEFT) + + # Insert "S1E" + self.write(ord("S")) + self.write(ord("1")) + self.write(ord("E")) + ########################################################### + laser = False + + if Rapid_Feed_Rate: + self.rapid_move_slow(lastx-startX,lasty-startY,Rapid_Feed_Rate,Feed,board_name) + timestamp=0 + for i in range(1,len(ecoords_in)): + e0,e1,e2 = self.ecoord_adj(ecoords_in[i] ,scale,FlipXoffset) + stamp=int(3*time()) #update every 1/3 of a second + if (stamp != timestamp): + timestamp=stamp #interlock + update_gui("Generating EGV Data: %.1f%%" %(100.0*float(i)/float(len(ecoords_in)))) + if stop_calc[0]==True: + raise Exception("Action Stopped by User.") + + if ( e2 == last_loop) and (not laser): + laser = True + elif ( e2 != last_loop) and (laser): + laser = False + dx = e0 - lastx + dy = e1 - lasty + + min_rapid = 5 + if (abs(dx)+abs(dy))>0: + if laser: + if variable_feed_scale!=None: + Feed_current = round(ecoords_in[i][3]*variable_feed_scale,2) + Spindle = ecoords_in[i][4] > 0 and use_laser + if Feed != Feed_current: + Feed = Feed_current + self.flush() + self.change_speed(Feed,board_name,laser_on=Spindle) + self.make_cut_line(dx,dy,Spindle) + else: + if ((abs(dx) < min_rapid) and (abs(dy) < min_rapid)) or Rapid_Feed_Rate: + self.rapid_move_slow(dx,dy,Rapid_Feed_Rate,Feed,board_name) + else: + self.rapid_move_fast(dx,dy) + + lastx = e0 + lasty = e1 + last_loop = e2 + + if laser: + laser = False + + dx = startX-lastx + dy = startY-lasty + if ((abs(dx) < min_rapid) and (abs(dy) < min_rapid)) or Rapid_Feed_Rate: + self.rapid_move_slow(dx,dy,Rapid_Feed_Rate,Feed,board_name) + else: + self.rapid_move_fast(dx,dy) + + ########################################################### + else: # Raster + ########################################################### + Rapid_flag=True + ################################################### + scanline = [] + scanline_y = None + if Raster_step < 0.0: + irange = range(len(ecoords_in)) + else: + irange = range(len(ecoords_in)-1,-1,-1) + timestamp=0 + for i in irange: + #if i%1000 == 0: + stamp=int(3*time()) #update every 1/3 of a second + if (stamp != timestamp): + timestamp=stamp #interlock + update_gui("Preprocessing Raster Data: %.1f%%" %(100.0*float(i)/float(len(ecoords_in)))) + y = ecoords_in[i][1] + if y != scanline_y: + scanline.append([ecoords_in[i]]) + scanline_y = y + else: + if bool(FlipXoffset) ^ bool(Raster_step > 0.0): # ^ is bitwise XOR + scanline[-1].insert(0,ecoords_in[i]) + else: + scanline[-1].append(ecoords_in[i]) + update_gui("Raster Data Ready") + ################################################### + lastx,lasty,last_loop = self.ecoord_adj(scanline[0][0],scale,FlipXoffset) + + DXstart = lastx-startX + DYstart = lasty-startY + + if Rapid_Feed_Rate: + self.make_egv_rapid(DXstart,DYstart,Rapid_Feed_Rate,board_name,finish=False) + + ##self.write(ord("I")) + for code in speed: + self.write(code) + + if not Rapid_Feed_Rate: + self.make_dir_dist(DXstart,DYstart) + + #insert "NRB" + self.flush(laser_on=False) + self.write(ord("N")) + if (Raster_step < 0.0): + self.write(ord("R")) + else: + self.write(ord("L")) + self.write(ord("B")) + # Insert "S1E" + self.write(ord("S")) + self.write(ord("1")) + self.write(ord("E")) + dx_last = 0 + + sign = -1 + cnt = 1 + timestamp=0 + for scan_raw in scanline: + scan = [] + for point in scan_raw: + e0,e1,e2 = self.ecoord_adj(point,scale,FlipXoffset) + scan.append([e0,e1,e2]) + stamp=int(3*time()) #update every 1/3 of a second + if (stamp != timestamp): + timestamp=stamp #interlock + update_gui("Generating EGV Data: %.1f%%" %(100.0*float(cnt)/float(len(scanline)))) + if stop_calc[0]==True: + raise Exception("Action Stopped by User.") + cnt = cnt+1 + ###################################### + ## Flip direction and reset loop ## + ###################################### + sign = -sign + last_loop = None + y = scan[0][1] + dy = y-lasty + if sign == 1: + xr = scan[0][0] + else: + xr = scan[-1][0] + dxr = xr - lastx + ###################################### + ## Make Rapid move if needed ## + ###################################### + if abs(dy-Raster_step) != 0 and not Rapid_flag: + + if dxr*sign < 0: + yoffset = -Raster_step*3 + else: + yoffset = -Raster_step + + if (dy+yoffset)*(abs(yoffset)/yoffset) < 0: + self.flush(laser_on=False) + + if not Rapid_Feed_Rate: + self.write(ord("N")) + self.make_dir_dist(0,dy+yoffset) + self.flush(laser_on=False) + self.write(ord("S")) + self.write(ord("E")) + else: + DX=0 + DY=dy+yoffset + self.raster_rapid_move_slow(DX,DY,Raster_step,Rapid_Feed_Rate,Feed,board_name) + + Rapid_flag=True + else: + adj_steps = int(dy/Raster_step) + for stp in range(1,adj_steps): + + adj_dist=5 + self.make_dir_dist(sign*adj_dist,0) + lastx = lastx + sign*adj_dist + + sign = -sign + if sign == 1: + xr = scan[0][0] + else: + xr = scan[-1][0] + dxr = xr - lastx + lasty = y + + + ###################################### + if sign == 1: + rng = range(0,len(scan),1) + else: + rng = range(len(scan)-1,-1,-1) + ###################################### + ## Pad row end if needed ## + ########################### + pad = 2 + if (dxr*sign <= 0.0): + if (Rapid_flag == False): + self.make_dir_dist(-sign*pad,0) + self.make_dir_dist( dxr,0) + self.make_dir_dist( sign*pad,0) + else: + self.make_dir_dist( dxr,0) + lastx = lastx+dxr + + Rapid_flag=False + ###################################### + for j in rng: + x = scan[j][0] + dx = x - lastx + ################################## + loop = scan[j][2] + if loop==last_loop: + self.make_cut_line(dx,0,True) + else: + if dx*sign > 0.0: + self.make_dir_dist(dx,0) + lastx = x + last_loop = loop + lasty = y + + # Make final move to ensure last move is to the right + self.make_dir_dist(pad,0) + lastx = lastx + pad + # If sign is negative the final move will have incremented the + # "y" position so adjust the lasty to acoomodate + if sign < 0: + lasty = lasty + Raster_step + + self.flush(laser_on=False) + + + dx_final = (startX - lastx) + if Raster_step < 0: + dy_final = (startY - lasty) + Raster_step + else: + dy_final = (startY - lasty) - Raster_step + + ############################################################## + max_return_feed = 50.0 + final_feed = 0 + if Rapid_Feed_Rate: + final_feed = Rapid_Feed_Rate + elif Feed > max_return_feed: + final_feed = max_return_feed + + if final_feed: + self.change_speed(final_feed,board_name,laser_on=False,pad=False) + dy_final = dy_final + abs(Raster_step) + self.make_dir_dist(dx_final,dy_final) + else: + self.write(ord("N")) + self.make_dir_dist(dx_final,dy_final) + self.flush(laser_on=False) + self.write(ord("S")) + self.write(ord("E")) + ############################################################## + + + # Append Footer + self.flush(laser_on=False) + self.write(ord("F")) + self.write(ord("N")) + self.write(ord("S")) + self.write(ord("E")) + update_gui("EGV Data Complete") + return + + def make_egv_rapid(self, DX,DY,Feed = None,board_name="LASER-M2",finish=True): + speed = self.make_speed(Feed,board_name=board_name,Raster_step=0) + if finish: + self.write(ord("I")) + for code in speed: + self.write(code) + self.flush(laser_on=False) + self.write(ord("N")) + self.write(ord("R")) + self.write(ord("B")) + # Insert "S1E" + self.write(ord("S")) + self.write(ord("1")) + self.write(ord("E")) + ########################################################### + # Move Distance + self.make_cut_line(DX,DY,Spindle=0) + ########################################################### + # Append Footer + self.flush(laser_on=False) + if finish: + self.write(ord("F")) + else: + self.write(ord("@")) + self.write(ord("N")) + self.write(ord("S")) + self.write(ord("E")) + return + + def rapid_move_slow(self,dx,dy,Rapid_Feed_Rate,Feed,board_name): + if Rapid_Feed_Rate: + self.change_speed(Rapid_Feed_Rate,board_name,laser_on=False) + self.make_dir_dist(dx,dy) + self.change_speed(Feed,board_name,laser_on=False) + else: + self.make_dir_dist(dx,dy) + + def raster_rapid_move_slow(self,DX,DY,Raster_step,Rapid_Feed_Rate,Feed,board_name): + tiny_step = Raster_step/abs(Raster_step) + self.change_speed(Rapid_Feed_Rate,board_name,laser_on=False,pad=False) + self.make_dir_dist(DX,DY-tiny_step) + self.flush(laser_on=False) + self.change_speed(Feed,board_name,laser_on=False,Raster_step=Raster_step,pad=False) + #Tiny Rapid + self.write(ord("N")) + self.make_dir_dist(0,tiny_step) + self.flush(laser_on=False) + self.write(ord("S")) + self.write(ord("E")) + + + def rapid_move_fast(self,dx,dy): + pad = 3 + if pad == -dx: + pad = pad+3 + self.make_dir_dist(-pad, 0 ) #add "T" move + self.make_dir_dist( 0, pad) #add "L" move + self.flush(laser_on=False) + + if dx+pad < 0.0: + self.write(ord("B")) + else: + self.write(ord("T")) + self.write(ord("N")) + self.make_dir_dist(dx+pad,dy-pad) + self.flush(laser_on=False) + self.write(ord("S")) + self.write(ord("E")) + + + def change_speed(self,Feed,board_name,laser_on=False,Raster_step=0,pad=True): + cspad = 5 + if laser_on: + self.write(self.OFF) + + if pad: + self.make_dir_dist(-cspad,-cspad) + self.flush(laser_on=False) + + self.write(ord("@")) + self.write(ord("N")) + self.write(ord("S")) + self.write(ord("E")) + speed = self.make_speed(Feed,board_name,Raster_step=Raster_step) + #print Feed,speed + for code in speed: + self.write(code) + self.write(ord("N")) + self.write(ord("R")) + self.write(ord("B")) + ## Insert "SIE" + self.write(ord("S")) + self.write(ord("1")) + self.write(ord("E")) + self.write(ord("U")) + + if pad: + self.make_dir_dist(cspad,cspad) + self.flush(laser_on=False) + + if laser_on: + self.write(self.ON) + + def strip_redundant_codes(self, EGV_data): + E = ord('E') + new_data=[] + modal_value = -1 + for code in EGV_data: + if code == modal_value and modal_value != E: + continue + elif (code == self.RIGHT) or (code == self.LEFT) or \ + (code == self.UP ) or (code == self.DOWN) or \ + (code == self.ANGLE) or (code == E): + modal_value = code + new_data.append(code) + return new_data + + +if __name__ == "__main__": + EGV=egv() + bname = "LASER-M2" + values = [.1,.2,.3,.4,.5,.6,.7,.8,.9,1,2,3,4,5,6,7,8,9,10,20,30,40,50,70,90,100] + step=0 + for value_in in values: + #print ("% 8.2f" %(value_in),": ",end='') + val=EGV.make_speed(value_in,board_name=bname,Raster_step=step) + txt="" + for c in val: + txt=txt+chr(c) + print(txt) + print("DONE") + + + + + + diff --git a/embedded_images.py b/embedded_images.py new file mode 100644 index 0000000..c96290a --- /dev/null +++ b/embedded_images.py @@ -0,0 +1,634 @@ + +class K40_Whisperer_Images: + def __init__(self): + self.CC_image=0 + print("init") + + CC_B64=b'R0lGODlhFAAUAIUAAA\ + AAAAQEBAcHBzAwMDo6Ojs7O0dHR2hoaGlpaWtra21tbW9vb3BwcHFxcXJycnNzc3R0dHV1dXd3d3p6eo\ + GBgYSEhIeHh4uLi46Ojo+Pj5CQkJWVlbOzs7+/v8nJycrKytLS0tPT09TU1NXV1fHx8fb29vj4+Pn5+f\ + r6+vz8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAACsALAAAAAAUABQAQAj/AFcIHEiwoEGDIAQAABAAxIqHECFq0LBCxYqLGD\ + Gi8PBAxYqPHydUWEGyJEkIBVCsWMmS5QUEIR40EMFBwoqbOFeoSJBhhc+fQH0+GGBihdGjKyxUWMF0hQ\ + QDKVZInUpVagMAWAE4WMG1q9cVGCSsGEt2RYoOEVSsWLtWhYoVcOPKjXtihd27ePPeVUFihV8VBzSsGE\ + y4sIoSHzasWLxYxQIKKyJLXoGiQ4YVmDNrfsBARAMHIDhYWEG6tOnSDACoBuBghevXsGGPEAAAQAAQK3\ + Lr3q3bwoMVwFdYMHBihfHjxyUoULGiufMVGQiUWEGd+oEDK7Jr3559wwASK8KLBR9PvnxAADs=' + + + LL_B64=b'R0lGODlhFAAUAIUAAA\ + AAAAoKCgwMDA0NDRAQEBQUFBgYGB4eHiMjIyoqKiwsLEREREpKSmRkZIKCgoSEhKKioqurq7Kysrm5ub\ + 29vcPDw8TExMXFxcvLy8zMzM7OztXV1dfX19jY2Nra2t3d3d7e3uHh4eLi4ubm5unp6erq6uvr6+zs7O\ + 7u7u/v7/Dw8PHx8fLy8vX19fb29vf39/j4+Pr6+vv7+/z8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAADUALAAAAAAUABQAQAjOAGsIFDgBwIYaCBMqXIgww4IQNSJKnEgxIg0UNT\ + Jq3Mixo8ePLwgAGHnAxQQSKSasGDEhxgcKNWLGBPHARI2bOHPqvHkhwYcaQIMKHQq0hYcYNZIqXcoUhg\ + EAUBPI2MDCxYYXKjbMMNGhhtevYMOKBRujxIwaaNOqXYuWQgAONeLKnUs37oYGI2ro3cu3r9+/gGtoYC\ + CihuHDiBPXkDAAQAELNSJLnjy5A4DLlzkAiFABAAYIAE44EFCjdI0LCAAo2FCjtevXsGPLnk07dkAAOw\ + ==' + + + LR_B64=b'R0lGODlhFAAUAIUAAA\ + AAAAoKCgwMDA0NDRAQEBQUFBgYGB4eHiMjIyoqKiwsLEREREpKSmRkZIKCgoSEhKKioqurq7Kysrm5ub\ + 29vcPDw8TExMXFxcvLy8zMzM7OztXV1dfX19jY2Nra2t3d3d7e3uHh4eLi4ubm5unp6erq6uvr6+zs7O\ + 7u7u/v7/Dw8PHx8fLy8vX19fb29vf39/j4+Pr6+vv7+/z8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAADUALAAAAAAUABQAQAjdAGsIHEhQIAUBHWooXMiwoUISEVjUmEixosWJLz\ + rAqMGxo8ePHEdAUFGjZMkNM0xwqDHiQ40QImIUAEBTQY2bOHPqrBHjxIwaQIMKHQp0AoANNZIqXco0qQ\ + sOMmpIlTohBggKNTZcqJFBQ4sBAMIioFGjrNmzaNOqVfvBgYkacOPKnQt3AoANNfLq3cs37woML2oIHk\ + y4sOAUFVzUWMy4sePFEwBsqEG5suXLNSwcAJBgQ43PNQCUeBCgRoMENRYw8ACgdWsUNWLLnj1bAgEABi\ + 7U2M27t+/fwIMHDwgAOw==' + + + S001_B64=b'R0lGODlhgACAAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTEx\ + QUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGx0dHR8fHyEhISIiIiQkJCUlJSYmJicnJykpKSoqKisrKywsLC\ + 0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQE\ + FBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFJSUlNTU1RUVFZWVl\ + dXV1hYWFpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2\ + xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+foCAgI\ + GBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJ\ + WVlZaWlpeXl5iYmJmZmZqampubm52dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqa\ + qqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb\ + 6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0d\ + LS0tPT09TU1NXV1dbW1tjY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5u\ + fn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+v\ + v7+/z8/P39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAP8ALAAAAA\ + CAAIAAQAj/AP8JHEiwoMGDCBMq/AegocOHEAH8m0ixosWLGDNq3Mix40YAIEOKHAnSQA02pqzRW8mypc\ + uW86aJQhODAICbOHPqvPmvp8+fQIMKHUp0KICjSJMCgLKNntOnUKM+lVfqBoCrWLNqBSAgiCx6YMOKHT\ + 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' + + + S009_B64=b'R0lGODlhEAAQAIYAAA\ + AAADQ0NDo6Oj09PUNDQ01NTVFRUVdXV1hYWFpaWltbW1xcXF1dXWFhYWJiYmZmZmlpaWpqamtra2xsbG\ + 1tbXJycnNzc3R0dHV1dX5+fn9/f4KCgoSEhIqKio6OjpGRkZKSkpWVlZaWlpeXl5iYmJqampubm6Ojo6\ + ampqurq62tra6urrCwsLKysrS0tLe3t7m5ubq6uru7u7y8vMHBwcLCwsTExMfHx8vLy87OztDQ0NTU1N\ + XV1dfX19ra2tvb29/f3+Dg4OHh4eTk5OXl5ejo6Onp6fHx8fPz8/X19fb29vf39/j4+Pn5+fv7+/39/f\ + 7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAFIALAAAAAAQABAAQAj2AKUIFAigoEEpCBECiMLQiR\ + EMAgZcmLEkShQlAKRQiMIxgQgCETLAKPEkCgkpKE9EgRIFxQIOHaLIzAFAik0AKqLQqHAAgQUdUU4AkC\ + IFQJSjP5ZcCODARhQoUaIcAQCAR5SrV3E8iMKVaxEAUsICIAEEiRImQXiAACClrVspE3wUYXHiyA4HUv\ + LqVRElioIUHkyg4DAkigwAUgD0iMJYgocWKDTUyBAlChEAAKJEefICgosQKkgUoLEkihIAAKKoVt1kxY\ + YjUWJHOQJASoMouHGPMJAkiu8oH6QIBxAjCg0GSIQsuBElBwAp0KNLAUC9upTr1wMCADs=' + + + UL_B64=b'R0lGODlhFAAUAIUAAA\ + AAAAoKCgwMDA0NDRAQEBQUFBgYGB4eHiMjIyoqKiwsLEREREpKSmRkZIKCgoSEhKKioqurq7Kysrm5ub\ + 29vcPDw8TExMXFxcvLy8zMzM7OztXV1dfX19jY2Nra2t3d3d7e3uHh4eLi4ubm5unp6erq6uvr6+zs7O\ + 7u7u/v7/Dw8PHx8fLy8vX19fb29vf39/j4+Pr6+vv7+/z8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAADUALAAAAAAUABQAQAjeAGsIHEiwoMGBGxIAOGChhsOHECPW2ABgQo2LGD\ + NqrHHBAAACEmqIHEmyZA0XFVLUWMmypcuXMGGiAECTpgcGC2okaFAjwIMSAGoIFboBwIQaSJMqXYr0BY\ + YVNaJKnUo1qgkHH2po3cq1q9evYGvA6PCihtmzaNPSQACg7YAWGjLUuLChBgUQMSbU2FtDAYC/BWKICF\ + Hjw4gaHEzM2FCjcWMZHFzUmEy5suXJLCKQqMG5s+fPnDcAmFCjtOnTqEurgDCihuvXsGO7nnEiRo3buH\ + Prvt1BAIUawIMLBx4QADs=' + + + UR_B64=b'R0lGODlhFAAUAIUAAA\ + AAAAoKCgwMDA0NDRAQEBQUFBgYGB4eHiMjIyoqKiwsLEREREpKSmRkZIKCgoSEhKKioqurq7Kysrm5ub\ + 29vcPDw8TExMXFxcvLy8zMzM7OztXV1dfX19jY2Nra2t3d3d7e3uHh4eLi4ubm5unp6erq6uvr6+zs7O\ + 7u7u/v7/Dw8PHx8fLy8vX19fb29vf39/j4+Pr6+vv7+/z8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAADUALAAAAAAUABQAQAjOAGsIHEiwoMGDAy0UADBAQo2HECNKrMEhAIUaGD\ + Nq3FhjgwIACC7UGEmypMmTKFOmFODgBAAIGABUiACAA4CbNzvU2Mmzp08RDDTUGEq0qNGhIxpsqMG0qd\ + OnTGeUiFGjqtWrWLNq3VoVBY0aYMF2MDFjg4oXG1yw2CAjAYC3BmDUmEvhQ4wJI1ZMSEFigosDAAITeF\ + GjsOHDiGN4aFGjsePHkBuHWJChhuXLmDNb/pDgQo3PoEOLHk26dA0TD0DUWM26tevVGwBMqEGbdkAAOw\ + ==' + + + aclfh_tyf71_001_B64=b'R0lGODlhgACAAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTEx\ + QUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGx0dHR8fHyEhISIiIiQkJCUlJSYmJicnJykpKSoqKisrKywsLC\ + 0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQE\ + FBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFJSUlNTU1RUVFZWVl\ + dXV1hYWFpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2\ + xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+foCAgI\ + GBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJ\ + WVlZaWlpeXl5iYmJmZmZqampubm52dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqa\ + qqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb\ + 6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0d\ + LS0tPT09TU1NXV1dbW1tjY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5u\ + fn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+v\ + v7+/z8/P39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAPQALAAAAA\ + CAAIAAQAj/AAEIHEiwoMGDCBMqXMiwocOHECNKNGigBhtT1uhp3MixI8d500ShiUEAgMmTKFOqXMmypc\ + uXME9C2Uavps2bOG3KK3UDgM+fQIMCEBBEFr2jSJMqVWrtCICnUKNKnUq1qlWrD4jR28q1q1evQwCIHS\ + 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' + + + aclfh_tyf71_002_B64=b'R0lGODlhYABgAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA4ODg8PDxAQEBERERISEhMTExQUFB\ + UVFRYWFhcXFxgYGBkZGRsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJykpKSoqKi\ + srKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODo6Ojs7Ozw8PD09PT4+Pj8/P0\ + BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1\ + RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGJiYmNjY2RkZGVlZWZmZmdnZ2hoaG\ + lpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e319fX\ + 5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZ\ + KSkpOTk5SUlJWVlZaWlpeXl5mZmZubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaenp6ioqK\ + mpqaqqqqurq6ysrK6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb\ + 6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJyczMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09\ + TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uTk5OXl5ebm5unp6erq6u\ + vr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v\ + ///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAPAALAAAAA\ + BgAGAAQAj/AAEIHEiwoMGDCBMqXMiwocOHBgdAcdUOnsWLGDNq3MgulRIBAEKKHEmypMmTKEUmUAWvpc\ + uXL2cVAECzJgAJWiaxAtbqDRAQAIIKDboGntGjSJOGMgCgqdOnUKNKbQoJntWrWLNaFQSga1dg8BwBGD\ + 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\ + t+DTu27Nm0a9u+fTsgADs=' + + + aclfh_tyf71_003_B64=b'R0lGODlhQABAAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ8PDxAQEBERERISEhQUFBUVFR\ + YWFhcXFxgYGBkZGRoaGhsbGxwcHB4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKi\ + srKy0tLS8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PUBAQEJCQkNDQ0\ + REREVFRUZGRkdHR0hISExMTE1NTVBQUFFRUVJSUlNTU1RUVFVVVVZWVlhYWFlZWVtbW1xcXF1dXV5eXl\ + 9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqam1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dH\ + V1dXZ2dnd3d3h4eHl5eXp6ent7e319fX9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiIqKioyMjI\ + 2NjY+Pj5CQkJKSkpSUlJWVlZeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpK\ + Wlpaampqurq6ysrK+vr7CwsLGxsbKysrOzs7S0tLW1tbi4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwM\ + HBwcLCwsPDw8TExMjIyMnJycrKysvLy8zMzM3Nzc7OztDQ0NLS0tPT09TU1NXV1dbW1tfX19jY2Nra2t\ + vb29zc3N3d3d7e3uDg4OLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7vDw8PHx8f\ + Ly8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAANUALAAAAA\ + BAAEAAQAj/AAEIHEiwoMGDCBMqXMgQgIdO1SJKjIiMjQMAGDMCEMADVbWPIEGKEgGgpMmTKFMCEBCqms\ + uXMKvRAEATVyIAOGvUitbMzgAAABT0igSgKJ9qSJMqrdYpAICnUKMC0FKtqtWrWKuyAcAVgAgofCa5AQ\ + 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\ + 8hRpQ4kWJFixctBgQAOw==' + + + aclfh_tyf71_004_B64=b'R0lGODlhMAAwAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERUVFRcXFxgYGB\ + kZGRoaGhsbGxwcHB0dHR4eHiEhISIiIiMjIyQkJCUlJSYmJicnJygoKCoqKisrKywsLC0tLS4uLi8vLz\ + ExMTIyMjMzMzQ0NDU1NTg4ODk5OTo6Ojw8PD4+Pj8/P0BAQENDQ0REREVFRUdHR0hISElJSUpKSktLS0\ + 9PT1BQUFFRUVlZWVxcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmhoaGxsbG1tbW5ubnJycn\ + Nzc3R0dHd3d3p6ent7e3x8fH19fX5+foCAgIGBgYKCgoODg4SEhIWFhYeHh4iIiImJiYqKiouLi4yMjI\ + 2NjY6OjpCQkJGRkZKSkpSUlJWVlZubm5ycnJ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6mpqa\ + urq6ysrK+vr7CwsLS0tLa2tri4uLm5ubq6uru7u7y8vL6+vsHBwcLCwsPDw8bGxsfHx8rKysvLy8zMzM\ + 3Nzc7Ozs/Pz9DQ0NHR0dPT09TU1NbW1tnZ2dra2tvb29zc3N3d3d/f3+Hh4eLi4uPj4+Tk5OXl5ebm5u\ + fn5+np6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/\ + z8/P39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAALcALAAAAA\ + AwADAAQAj/AAEIHEiwoMGDCBMqBFDolsOHt0wpAEDmlsVJkm5pfFNgQKRbIEPeKgSgpMmTBUDdWsmy5c\ + odAJzcuvXozB5Wt/4A8HALVgYJrG4JHUr0EYCjABzcWsq0qdNbdSQEAECVagAGZ2jd2sq1q9dWAMKGpX\ + KrrNmzaM/mCCDpltu3cOO+NQKgrt27ABToucW37y0gAEa4ukW48A8AQm4pXqyYD4DHkCNLnvzgCaxbmD\ + PfAhUpkqdboEPfahVlAYDTqFOrBvCg1K3XsGHXAeDlVhwAWWpJOXQrEwEJslJhqGDqlvHjxjUBWM4cwK\ + Vb0KNLn36rlipDJQA4mnWru/dYt8KL/x8fHhSA8wDm3FrPvr179wYI3JpPv759+5AA6N8PQMYtgLcEDi\ + Q4EBYMAD6kAChB69ZDiBEj2gBQ0eJFjBcnFHl0y+NHkCAfHZEAwORJlClVrjRZwMQQOZduzaRpaU4QEw\ + QA7OTZ0+dPn19uDSV6K9EJAABE4MhBAgAADYZuTaV6qwsArFm1bgXw5dZXsF8/PbjQ6hYTAABs4AAAgM\ + itWzUAvLlV127dNAD07uUL69ZfwIEZAeBxy9GACFICAPBT68YBSbdUHNh0y/Lly6wAbOY85NZn0KFDSx\ + oAIAISNlg8EACw5tZr2LFlv74CwHaAW7l179b9BACBHpxu3dICAP/PrVuolBgIAECBoFvRpU+/tQrAdQ\ + C3tG/n3r07LAAwbo0nX968eVMA1AMwAevWe/jx5ccPIQDVLfz59e/XD8A/QAACAUC5ZfAgwoQGUZUA0O\ + YWxIgSJ0IMA+AixowCAN3q6PHjrR0AXtwqWQkAgEW3VrJseYsPgJgyZ9KUeUPTrZy3Qrm65fPnT0tJTN\ + 0qeguTDQBKlzJt6vTp0gMTNoD4sGHCAQBat3Lt6vUr2K8QGgAoa/Ys2rRq15alAgrWrbhy58aFNcoKgL\ + x69/Lty3dFqVuCB9+Ck+FAgQIGIrC55fjxrVczAFCubPny5US3NnO+tcgAgBaebJG2lYoGAAD/eW6xbn\ + 0rEIDYsmfTBrDoFu7cuGkA2HLrlh0BAAAEUHPrFhkAHG4xb858D4Do0qdHV3HrOnbsjADwuPWoAARSt2\ + 6BcgCgzS1KAFLcau/ePQ4A8ufPf3LrPv78dQB4ueUGIIAxUAAAuDSrRYJLtEws6HQLYsSIXgBUtAigyi\ + 2NGzneAgXhQqpbSACU7BBr1ogFm2SNaPDpVkyZM40AsGnTBKxbO3n23NmKAYAprW4VveUHAABFt26ZEn\ + ULalSpUAFUrTrnVlatW7l6umEAQFgAI/DcMnsWbdqzjgC0BXALbly5cUHR8XQL760lABDd8nsL1R0dAF\ + 7cMnwY8S1TABg//7j1GHJkyKCYSAAAQEabMgC45MkBAIACIH1ulTZ92nQrAKsB3HL9Gnbs14SaHABQwM\ + ifW7t59/bNexUA4QAs3TJ+HHny5AAw3HL+HHr06KAAVAcQ5FZ27du5c5cg4FZ48ePJk7cCAD2AAaNutX\ + f/Hv57FwE23bJ/H3/++48A9PcPEECrWwQLGjxYEEgARLcaOnwIseErABQrUlTg6pbGjRw7rrIAAACCWb\ + dKmjyJEhOAlSxbAuhyK6bMmTFlfQCA4latGADE3PoJNChQMwCKGj16tNCtpUyX1mIBAMOtqVMFAYBw6p\ + bWrVstAfgKNqxYsGhumT1LAIKsW2zbsk0AYD/Qrbl0zQC4izev3r13vbC6BTiw4MGBYYEBgDix4sWMGy\ + OOcKURKFOrXLVaZQoUpCsUAHj+DDq06NGkS5s2HRAAOw==' + + + aclfh_tyf71_005_B64=b'R0lGODlhKAAoAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgwMDA4ODg8PDxAQEBERERISEhMTExQUFBUVFR\ + YWFhcXFxgYGBkZGRoaGhsbGxwcHB4eHiAgICEhISIiIiMjIyYmJicnJysrKywsLC0tLS4uLi8vLzIyMj\ + Q0NDU1NTc3Nzk5OTo6Ojs7Ozw8PD09PT4+PkBAQENDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE\ + 1NTU5OTk9PT1BQUFFRUVZWVldXV1tbW11dXV5eXl9fX2BgYGJiYmNjY2RkZGdnZ2pqamxsbG1tbW9vb3\ + BwcHFxcXR0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4GBgYODg4WFhYeHh4mJiYuLi4\ + yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJaWlpiYmJmZmZycnKCgoKGhoaKioqOjo6Wlpaampqenp6\ + ioqKmpqaqqqqurq6ysrK6urrGxsbW1tba2trm5ubq6uru7u7y8vL6+vsLCwsPDw8TExMXFxcbGxsfHx8\ + jIyMrKysvLy8zMzM7Ozs/Pz9DQ0NHR0dPT09XV1dbW1tjY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Hh4e\ + Pj4+Tk5OXl5ebm5ufn5+np6erq6uvr6+zs7O7u7u/v7/Hx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+f\ + r6+v39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAALcALAAAAA\ + AoACgAQAj/AAEIHEiwoMGDCBMC8HKrocMjAC54moIAAIAJgfAEAPDmlsePWACIHClSya2TKFPeahUEgE\ + uXFQTdmkmzZk0iAHICGHGrp8+fQO0A0HCrqNGjSG+BAMC0KdMAbm5JncqHAAAZsW7dwgJAQKFbYMPeUg\ + OgrNmzaMsaeCLrlttbpe6MgdJm1q27t2I1KQCgr9+/fjPIukW48C1JAACEigAgAQAAjgIA8GMCAJpbmD\ + PfelUBgGfPeW6JHk3alJgPAFKrBiAiTapbsGPLvoUGgG0ACQCsucW7t+/eWgAA0HKruPHjxs0AWM68Of\ + MFAABg+KLplvXr2C95sQCgu/fv4MOH/y+RhA0fQoT4rEEyAoD79/DjywcQoMut+/cXSQAAAAILgA4EAA\ + AQQ9UthLdsdQHQ0OHDhhFI3aJYcQ4AAKgMAIgyxxILACg6AQAA6dZJlKIgAGDZEsCmWzFlygwDYICcSD\ + uYeDIB4MQtDwMS3SJalKglAEmT8rnV1KlTQAEkSNF0SwWAMbcoOXEAAICFW2HFirUDwCyANbfUrmXbtg\ + mAFrfkzqVb91YZAHnzfrnV1+/fv48AAIB1y/BhxIi3AGDcuAIAH7ZuTaZMGU4AADw2AEhB69Zn0KBt4Q\ + BQ2vTp0z1I3bolC8aNVbdky97EQACnW7lF6QDQ2/dv4MGFDyde3P/4ceTFM4DZdMv5c+iZvlwAUN36de\ + zZf5C61f3WJyECAIwH8EDMrFvpb43aAcD9e/jxA7y5Vb9+qAkALugZ1YcOwFFWBgCwUesWQoRsADBs6J\ + Dhj1sSJ7IiAEBOFgAABgQAcIDQEgAcbN0qWdJWDgAqV65kc+slzFt/ACigBACADAAGDgEAoKkDADm3hh\ + K9dQYA0qQAwNxq6rRprQoAruwhAADAikcAACgyACDLrbBiw3IBYNYsiVtq17Kd9QIAAA4uajAAYEDRrV\ + qxbvHt6zcEgMAA2NwqbPiwJ1m3Gs0wAOBIq1u3YMWZQeUW5syZzQDoDEDPrdCiRS9KAABAABr2JwDsaA\ + HgdQIjk27Rrl07DYDcAPbc6u37d+9agYAEACCgCKJbypczZ14HAHQASG5Rr279OhsAJW5x7+79+60hAM\ + aP13TrPPr06QMBSHDrPfz48isBqG8fQ6xb+vfz398JIAAAoG4VNHjQ4CsKABg2BOAg1C2JEyneIgMAY4\ + NOtzh29HhL1AMAI0mW7HILZUqUcgQAsCHL0AAAXG7VtGnTCgCdO3nyrGLrVtBTBwC8cHULKS0VADa4uv\ + X0lq0uAKhWtXq1qoEnsW519foVE6Nbt2A1KQAAbVq1a9kiAPCWBJI0egYN0nPmiAgAe/n29fsXcGDBgw\ + MCADs=' + + + aclfh_tyf71_006_B64=b'R0lGODlhJAAkAIcAAA\ + AAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAoKCgwMDA0NDRAQEBERERISEhMTExUVFRYWFhcXFxgYGB\ + oaGhsbGxwcHB0dHR4eHiAgICEhISIiIiMjIyUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS8vLzAwMD\ + ExMTMzMzQ0NDU1NTc3Nzk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQENDQ0REREZGRkdHR0lJSUpKSktLS0\ + 1NTU5OTk9PT1BQUFFRUVJSUlNTU1VVVVlZWVxcXF5eXmBgYGJiYmNjY2VlZWZmZmpqamxsbG1tbW9vb3\ + BwcHJycnR0dHV1dXZ2dnd3d3h4eHt7e3x8fH19fX9/f4CAgIGBgYKCgoODg4SEhIWFhYqKiouLi4yMjI\ + 2NjY6OjpCQkJOTk5SUlJ2dnaCgoKKioqOjo6SkpKWlpaampqenp6mpqaqqqqurq6ysrK2tra6urq+vr7\ + GxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5uby8vL29vb6+vr+/v8DAwMHBwcPDw8TExMXFxcnJycvLy8\ + zMzM3Nzc/Pz9DQ0NHR0dLS0tPT09XV1dbW1tfX19jY2Nvb29zc3N3d3d7e3uHh4eLi4uPj4+Tk5OXl5e\ + fn5+jo6Onp6erq6uvr6+zs7O3t7e/v7/Dw8PHx8fPz8/T09PX19fb29vf39/j4+Pr6+vv7+/z8/P39/f\ + 7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAALUALAAAAA\ + AkACQAQAj/AAEIHEiwoMGDCAmKqcWw4aIGcS59oHEo0ZAKeySVuFKro8cuAEKG/FKrpEmTjoiooVTrEY\ + o1tTb5kTICBJ5aOHPihAKgp4xaQIMKDQqry4pRtZIqXZp0lgwAUKNKhTqnltWrWEuJqlVrzgwAYMOKHQ\ + tWTK2ztV79aQGAAIEBKebAqkW3FhkAePPqdVOrr9++p55ccIRKTZpQgEJUqbXFSqtakCPXEgOgspJamD\ + Nr1gyKR5JaoEOLHg0EgOnTfWqpXq3azoYfo0LtSFGplu3btu9AAMC7t+/fL9ykqkW8OHFUbmYAWM68uf\ + Plj2pJrzWrToEptUodIkQqVpkFXGqJ/xdfCID58+cP1VrPvtYgBocQYTCiQEWnLC9EUYmRqZZ/gLUEzg\ + FQEECKWgkVLqxFSccEDhQY+GBUy+JFjBcBbERTy+NHkLLyICEBQYGDDjvOdKrV0uXLWloAADiAqtZNnD\ + lxkjpyo9ZPoEGDRgJQtKiMWkmVLq01S42EBFpqTaVatZaqGQC0buVap9bXWpMw1NhUyywtMxHK1GLL9k\ + 4GAHHlzqVL98QPKFq0PAECwO9fwIEFD0aQQsmXM2jAKElxAMBjyJElTwbg4lAtzLNaZdqjx9IrWbVEI5\ + IBwPRp1KkBEKrVutaqMgFgxMGUiQ4OA1FY1eJdqw8A4MGFAy9Uy/+48U0xcNTis0FAAQ1taFXpMKjW9e\ + t2AGznvt1FLfDha/VpAAnQAzl2BHGiQYRVkR+las2f/wrAffwwau3nv78SQA1YTiFhgCFJKCY2Tjmhwa\ + kWxIi1AFAEgCBVrYwaNV5KAQIOpkxuUMyQVItWrZQqVUYC4FJJrZgyZb4KVauWoSUPwNTqqaqRHk21hh\ + Id+gMAADS1ljJdSkuRCwssvJQhwYRNDwwjmhh6Vesr2K9aAAAAU+ss2rRoK2GJ0MJQrFpy59Kd+wQAgB\ + S19vLt29dTkB+1BhMubNgEgMRuajFu7LhxqicxWNWqbPmy5TAANm9GVOsz6NCfXUHxIKkW6tRwqlHLAe\ + D6NYAxtWbTpk3rjQYhTy7cqeX7N/AvAIYTL16oFnLksvhsyOGpVi1BIpCwqmX9eqAMALZz786dy6tatT\ + 6ZqVTr/HlTUrTQqlVL1RcIAObTr2+/vhhJtfbz3z8JYBgZAAgWNHgQYUKFCwkGBAA7' + + + aclfh_tyf71_007_B64=b'R0lGODlhHAAcAIYAAA\ + AAAAMDAwQEBAYGBgkJCQsLCwwMDA4ODg8PDxAQEBERERUVFRYWFhcXFxgYGBoaGhwcHCAgICEhISMjIy\ + UlJSYmJigoKCwsLC8vLzIyMjMzMzg4ODk5OTo6Ojs7Ozw8PENDQ0ZGRkdHR05OTk9PT1BQUFFRUVNTU1\ + VVVVpaWltbW2JiYmRkZGVlZWlpaWxsbG9vb3BwcHJycnR0dHd3d3l5eXp6ent7e3x8fH5+fn9/f4GBgY\ + KCgoSEhIiIiIuLi4+Pj5CQkJKSkpOTk5SUlJiYmJmZmZ2dnZ+fn6KioqSkpKWlpaioqKmpqaqqqqurq6\ + ysrLKysrOzs7S0tLW1tbe3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcPDw8fHx8nJyczMzM\ + 7Ozs/Pz9HR0dPT09XV1dfX19zc3N7e3uHh4eLi4ubm5unp6evr6+zs7O3t7e7u7u/v7/Pz8/T09Pb29v\ + f39/j4+Pn5+fr6+vz8/P39/f7+/v///yH5BAEAAH8ALAAAAAAcABwAQAj/AAEIHEiwoMGDArf8WcgQwZ\ + MhEhSkkDPgTYQnfzJmzAKgY5E/IEOK/POCx5+TKFP+wQKgpUsAMPz8+RNEzp+bf5jo8POHDwwAQIMKBd\ + Dkj1E/KjBYkSMHi4cQdf5IXQKgqtUsf7Jq/YPEzh8/Far8+ROGA4g/aNOWAcAWwJ+3cOG+2fCAzZ+7eO\ + /yAcC3r18AWuL8GaxGzZ/Dcr4AWMy4MQAafyJHZoBEiQEoUAj8EIPgj2fPMQCIBtDij+nTpjWcILOGyI\ + Eof/j8mU37jw0AAMb82c17txgaFwY0KJEkzp/jyI+nAcBczp/n0KMXWcHnj/Xr2AFo377kj3fvdDqM//\ + iDB8KMP+jTTwHAvr179zawlEmTpkyWGwDy69/Pn38LgEWylCmTpQgLAAkVLmQIgMYfiHmAGBAQgECPPX\ + 80ygDQ0ePHJn9E/iEy4U4QChSG9BGR48/LP1AAzKQJ4M9NnADOoPBxpIuUAnYagPlTtCgApEjj/GHaFE\ + eGP0JYuBHi4M+CM3+0bgXQtcgfsGHBzqnRQUEILn/U/vHzx61bLAAAjPlT124eDAlYoNFg5AgGBC3a/C\ + FcOA0AAFn+LGbM2I8dEz32/KFc2XIZAABY/OHc2fOfHTL+jCZd+o8NAKm9/GHd2rUTE3r+zKZN+woA3L\ + hl/OHd+w8cCyhcfPhT3DJ4cRgAlC9X/uTP8z8kQvyhbqYAlT/Z/0wB0N37d+9Y/ownP97PnywA1K9n39\ + 79e/gBAQA7' + + + aclfh_tyf71_008_B64=b'R0lGODlhGAAYAIYAAA\ + AAAAMDAwQEBAYGBgsLCwwMDA8PDxAQEBISEhQUFBcXFxgYGBkZGRoaGhsbGx0dHR8fHyAgICIiIiQkJC\ + UlJSYmJigoKCoqKisrKy8vLzMzMzQ0NDg4ODo6Oj4+PkBAQEdHR01NTVBQUFFRUVNTU1VVVVZWVlxcXG\ + JiYmpqanR0dHh4eHp6eoCAgIaGhomJiYuLi4yMjI2NjZGRkZKSkpWVlZiYmJmZmZ+fn6CgoKioqKmpqa\ + ysrK2tra6urq+vr7KysrS0tLa2tre3t7q6uru7u76+vsHBwcLCwsTExMrKys7OztPT09TU1Nzc3N3d3d\ + 7e3uDg4OHh4eXl5ebm5ufn5+np6ezs7O7u7u/v7/Dw8PLy8vPz8/T09Pb29vj4+Pn5+fr6+vv7+/z8/P\ + 39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAGYALAAAAAAYABgAQAj/AAEIHEiwoEGCNMwoXEhiRZ\ + YkVigoYbLEjEWLKwAAUGCmo0ePAFSYGUly5JcBAFICsODFjMsRLLAAAFDFBYYtZsx0AcCTpxYzQIN6GS\ + IiAIQWTcwoXaoUgFOnDMKYMQPAg5mrUQCMMWMmjAMAYMOGNQDDjJkyPgAA2CHGjBkZAOLKjQvDjF0zPT\ + 5wCZHjhA8APzSYGWzmBoDDVswoVjwGihkoDY6YMcMjhZnLmAEAgGKms2fPAHCYGU2a9BQAqAtwMcO6NY\ + ASAEyYmU07TAIAuHPnFrCiipnfZqisGACguPHjxxXQcGKFCxcrUGYAmE69+nQLV8yY+TLBA5AgICSUvD\ + FjhguA8+gBGLhipj2UCmUg6PBBAMuHImbyhwHAH8ACgGYEDrxgRsIDFEUAIKFgxqEZMQAkOjFT0SKZDQ\ + c4AHBhxuNHj1UAANBixqRJKQA+POlgpMaBCEbMzKQJAIAVMzl16gQgxMxPoEEBAJhhxujRowBemGHatO\ + kKAFFjmKFa1UwZABnMbOW61QYAsGAxdDFTtgyAEGbMACBixq2ZLQ4AzKU7F8ENMWb07iViBoyNBAAEDy\ + Zc2PBhAAEBADs=' + + + aclfh_tyf71_009_B64=b'R0lGODlhEAAQAIYAAA\ + AAADQ0NDo6Oj09PUNDQ01NTVFRUVdXV1hYWFpaWltbW1xcXF1dXWFhYWJiYmZmZmlpaWpqamtra2xsbG\ + 1tbXJycnNzc3R0dHV1dX5+fn9/f4KCgoSEhIqKio6OjpGRkZKSkpWVlZaWlpeXl5iYmJqampubm6Ojo6\ + ampqurq62tra6urrCwsLKysrS0tLe3t7m5ubq6uru7u7y8vMHBwcLCwsTExMfHx8vLy87OztDQ0NTU1N\ + XV1dfX19ra2tvb29/f3+Dg4OHh4eTk5OXl5ejo6Onp6fHx8fPz8/X19fb29vf39/j4+Pn5+fv7+/39/f\ + 7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFEALAAAAAAQABAAQAjmAAEIHEiwoMAoCJ0YwSBgwI\ + UZS6JEUQIAAIUoGBOIIBAhA4wST6KQAEDyRBQoUVAs4NAhisscAGLGVBGFRoUDCCzoiHICgE8AUYL+WH\ + IhgAMbUaBEiXIEAAAeUaJGxfEgilWrRQBo1UoCCBIlTILwAAGgrNmyE3wUYXHiyA4HAOLKVRElioIUHk\ + yg4DAkigwAgHtEGSzBQwsUGmpkiBKFCAAAUaI8eQHBRQgVJArQWBJFCQAAUUKHbrJiw5EoqKMcAQCgQZ\ + TXr0cYSBKldpQPAHIDiBGFBgMkQhbciJIDgPHjyJMjDwgAOw==' + + + aclfh_tyf71_010_B64=b'R0lGODlhCAAIAIUAAA\ + AAAAEBAQgICBUVFR0dHS4uLkBAQEZGRk5OTlFRUVRUVFVVVVhYWFlZWVxcXF5eXl9fX2BgYGRkZGZmZm\ + 9vb4GBgYWFhYeHh46OjpSUlJycnLq6usHBwcjIyOTk5Ofn5+7u7u/v7/Ly8vX19fb29vj4+Pv7+////w\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAACH5BAEAACcALAAAAAAIAAgAAAhIAAEoAECwwIITFAh4iOAgBAAIAEA8qIChwQgAAD\ + poSJBBgggAA0hwgLChxAkAAU5cMNDAxAkAEwAg+GCBgYADJw4A2AkAwYmAADs=' + + + down_B64=b'R0lGODlhFAAUAIMAAA\ + AAAAEBARUVFTAwMFFRUV9fX2BgYJ2dnZ6enp+fn8/Pz+rq6v7+/v///wAAAAAAACH5BAEAAA0ALAAAAA\ + AUABQAQAjnABsIHAigoMGCDRIqJACgYcMAACJKDACgYkUCDTJqbCAAgEcAAhqIHElyJICTKE82WMlyJQ\ + MFAGLKnBlTAYMGOHPiBMCzJ88GQIMKBQqgqNGiDZIqTSoAgNOnUKEKaEC1aoMCALJqzWqggdevYL0yUA\ + AAgAIGDdKqXasWgNu3bhvInUtXLoC7eO822Mu3714AgAMDbkC4sGHCABIrTtygseMGAwIAmEy5cuUAAx\ + poXgCgs+fPnxc0GE06AYDTqFMnaMC6tesBAQDIBhBgQIPbuHPjXgCgN4AFDYILH068wQEEDZIrVx4QAD\ + s=' + + + left_B64=b'R0lGODlhFAAUAIMAAA\ + AAAAEBARUVFTAwMFFRUV9fX2BgYJ2dnZ6enp+fn8/Pz+rq6v7+/v///wAAAAAAACH5BAEAAA0ALAAAAA\ + AUABQAQAjUABsIHEiwAYEGCBMmVACgocOHEB02mEixgQIAGAE02MixY8cEAEKGbECyZEkGCwQEAMCypc\ + uXDWLKbMBggYAAABro3MlT5wAAQIMGAEC0KFEEAJIqXcp0aYOnTw0AmEq1gdWrWLNeHQCggdevYMN+HQ\ + CggdmzaNMqAMAWQIO3cOPCLQCgrt0GePPmZbBAQAAAgAMLHnwAgOHDiBMjbqAAgOPHkCM/bkC5wQAAmD\ + MHAMC5M+cGoEMnAECadIPTqFOrZrBAQAAADWLLnk17NoEGuHPjDggAOw==' + + + right_B64=b'R0lGODlhFAAUAIMAAA\ + AAAAEBARUVFTAwMFFRUV9fX2BgYJ2dnZ6enp+fn8/Pz+rq6v7+/v///wAAAAAAACH5BAEAAA0ALAAAAA\ + AUABQAQAjVABsIHCiQQIODCBM2AMCwocOHDRU0mEiRIoCLABQ02Mixo0cAIEEmaECyJICTKFOqDCBgAY\ + MGMGPGBBBAwAIGDXLqbACgp8+eAQAIHTqggdEGAJIqXcp06YEGUKNCBUC1aoEGWLNqzQpgQIOvYMOKBQ\ + tgQIOzaNOqRQugLQAFDeLKnUsXgN27Bhro1Qugr9+/gAMIWMCgAYDDiBMrTowAgOPHkCM/VtAAgOXLlg\ + MA2Mx5QIPPoEN/BkCadIIGqFOrVg0ggIAFDBrInk27NoEGuHPrbhAQADs=' + + + up_B64=b'R0lGODlhFAAUAIMAAA\ + AAAAEBARUVFTAwMFFRUV9fX2BgYJ2dnZ6enp+fn8/Pz+rq6v7+/v///wAAAAAAACH5BAEAAA0ALAAAAA\ + AUABQAQAjoABsIHDgQwYEGCBMqbLAAgMOHECEuaECxIkUAGDNibMCxo0eOAwIAGAkgwIAGKFOqRAmgpc\ + uWDWLKnClzAYCbABY02MmzZ4MEAIIKHZqggVGjAwIAWMq0adMAAxpIndoAgNWrVhto3cpVK4CvYL82GE\ + u2LFkGCgAAUMCggdu3cOEKAEAXgIAGePPqxWsAgN+/fgs0GEy4AQMFABIrXpxYAYMGkBsIAEC5smXLAh\ + o0IACgc+cAAEKLDgCgdGkCDVKrBsC6NesGsGPLhg2gtu3aDXLr3p0bgO/fvhsIH05cOIDjyI83WM48IA\ + A7' + diff --git a/emblem b/emblem new file mode 100644 index 0000000000000000000000000000000000000000..990458841b641e9cbf537829a954c4550149348e GIT binary patch literal 151878 zcmeI52fSQG^~W~>3@s2T5>ZT0P(V6JQ8A&YNC#;K>56m^1w~383J4+~9Z^I@%gjBjc2TWX+hv!*>q}0r)u!s+`0>gA>(y$9oI9+x^2*8oOV?_@m@%xj z%rb-5doEt9ePOQQwWXFCygqKxT5Zyp;k6Hcc<}mf^Ve!?d}?@Y^yuXOd1|!<)*D_M zH7a?3&RXrvjfdCfo_p|m!(p}B|27|9Q}VSwp@@v>+NPF`=|4&#}`xZ^-9pybuc#LpO;c($>D8B}%SH|G;gwG3$ zwo|8`e^#{pR7n5S%sU&}tbvc}{iB2f!cMheS@}FBnhzH)A)F1xYG5@ze!lQ6VOj0b zt#p_pekWW@I2+PyU=#g)o3LALRdv2MscajhSHXMoU*^kOg{9U1JoC&Gt+L80(S!*T zqJs}UIQq#?eiB`B$tBSZH{1~2b=O_dz4zW5J^JXQ(IbyM5})6B=biC?7hZT_bka#D zMc?|?x1#N~+b&vZrIn%)BSw@)S5n45>G&z(dmWI`pAxoJzjMtsSG2|&YeWYgcwltd zWtT9!L5f-kqkt0V&yY04HbjvNbl=^+=ZuG!|j1SB!U;XM=qj~3@w{W?Vx_Cgx zS$QlNq4J$5Ed0%{y6URYuYUEbu6*Z#CJ)|bT=?0~eikji{PL~zgSS;bj0JNFXC=TK zF;$qmPKFO39&NJ8Ceckd-4v|rUjE~M|NCEb-F4SRpa1;lqhZ5_E!rXQ6#V>v_nmToAFE?)?+U~gHj%eJtakZ{SrnKN4=@x1fSE6N|BJNmA0sOgerwXRJP=IZnF&p&^3#1ThC@KL=APe1*1 zwCSdsM$Ai;YsD2;jBdaE_QK@8_10U_;fEg{%{SkCx#}jV!%K9O<$p*3_Sk!bxyrZo z)>}u^Z4r3=_1B{lPBSDfM=<{#!uU%OEe68gfi#xfttz`qQ5luDbvJ_rKBFYp>mu{$6|S6~S{=0ekMb zXH$NrjOZ5I0pywjW5E>VD-WTb1o+kI!eCvQ|JGb{&6dBLGtM|8nEn9=9MDcvc;7|8 zWa#!C8Nb?vhaY}8)-(Ik_uP9}_dX=-CBWb6abap*@&DJp_O*y_xK-GG`|VTnIP}m% zTTQy@y{DdfDq48qg;Vo`Zz%)beDlp%mzbvx^~|HfUhzkhCt~gXEV|K`A9vhwZCBME zd+d?g9?{1fZxkKqRl5Pm_g4v1 z>uJ=eQPHip-dbuk@qK6Ii_Yz=v(9QMUu?tgd*Ay~^I#sU63~0XBWIOU|DP}H2H^9U zO5X=Q@PX*Q`|hixnwaOa@~01=i!TD$5QrGE3F#Ex6!+-grJ!d}eWxLMLj2J? z0oZa<_4&;A^xdife(4+E_(od2#Q)J|S6y{gCOOQzDqAZ4_{TraOsDJzuD|~J%=t)k zza;#Kuu6EJ5}zsz*3q1E&RH6-m!}@#)3Lj!(Zqku=YROaAA(6f@x&7m{CrwE*z(Hs zD+&_h`2!ti4m%<`Le@uA1wU58sdaSPX{ULpul5#p1AO(eXu~IP^5n@)#GsS^^rt_a z`u?JeE?QVm-IP(AH~RYeQfW-e|GkPDe_f@^JQJj$YMwZ)`u^|#{x9|yNTZMcn46Fx zZ8-ACBZFn)9lpD(ufE!Tr7P#mcjy~4%J_f!AOHC>0U0yZXKSss)@n*ZJmF9ArS1jz&wu_C zeeQFg3({)JfzKv>8DqwbNqv^O#{Sn!@X$jKMa*TUEP>;Jx?ct?twi1s2J^#))k}a6 z-lsnGsUW?kEOXC2cj`0zKW*ByUMgChmXl9DIhA&m5gBc)uwA%X2?X;h!~Rt-REZ}q zzx;B9-)I``S^u4R=9wi@RBlr60>~p7Ww=29w*#zc4(fRpSYUw&8(FWybI&~&Ew|iq zS#{+7fAh^ZugssItooWUV@8(m0Q~yOZFGRide>V&+{YH*>p=hf=tn;qEQ7Cni!Z)- z^!)SB_j=J{`i?&O=&Z7__Oun)R4)b7UVZh|yQT{%+Aw@4ODwTOuq<{tvAzE3Pk$>xWwXlmnHD-g>N?z^R7W%p>pVw~9-UQB(RaS{ovyWIs~Jz4G%2fGMaO_O z>~F$7$3K54!h;V!m}*PGPJkY1D1y%?(rPz&xaEX-;c`9b?NjK&hVt5sGbD7b!gu7@L3+pfG#gow!Qb>dngwz`pjoO6D*s`{H8FE@9Ya&B^T=%C3+JBvt zcba@A%A&QC>EI`nwzdKt(X0UYI;ORy{#(J*2Z)pHeGfnGZuz~H*&gWhLuKSXW9O^@ ztR>4>J8+n%{T~X|4RN++1>pM*m636wm*WouoZgW$>Td+ zl)i2}&-@-LBYeTE1i$>{FN0-O9sE}~uh76|I`KY_PP`lO^{QwK`1?>fkqc%ec=p+6 zv;4)!na0;KIQJtj&nkd?A8P-htoskO&(cdT9W1Zv!R7(}9IBUt4mv25{E)j(T;EVR z(F@LMK&;GAd3E2m3A1*{`z$);Sq0GVhswzsqFD{jJ@?#Tc~uWHgaENSg3mqv_~Wxo zwGbmHRMuYd@h_{wr%ag=EU)STpMa6V&xh)SShll*+;8eLZH?GbM;&Dzn`MryTgX`3 zLJa!-LhZlWYOBpMbwNQk*kFU;_t1@Xi=C+4G<%2IeJ3INXnE@eTluU6Y3l(ly6B== zsi^g`5JNfCzPis^QPv1}pBak2koAIFC;$HUzvEbgtW6J<6<%ppgfxFQ)x~K-_9F4t z3wx#vWq9C$2O@mBuw94BXzt_JxbC{^#<4=spU*0gv1a_YDnIcAz4b#pjG+c^yzxeK z!37s&`NDb2YwCfu{=}A<73_h;KQiU=9Emgkgy(+z&QOZI&-!E9EGu4PsI0#Gj0wa5 zWZ$TEVpewZ8_I}Zip}puU)j0G`siK;;&%M@x4(_~dfF;fjxz2uCJ^hlSJt`bZ5QG$ zrj&m`|GgFZzpB`6d1((lN30&=uCh)qM0-{DhuWBM^UXJ>)e*5mg8%oDKNNbXYp%H_ z+HAAUvUD23@}!lumH&pqm_Us6R?5aZz0&@yHO$asv|Pqk>&AT#T^V)VYq3Idw0}>J z|M%a2f8JQQ-add|59<}v;ySLs{`&E{PrhfjU5j4hM?#EhVzl%E&@#Dv5`A!7WGI_u!us;dBN;e?~|JT#< zA8ldkG|zFJb=Ha4!`Ic}eEsWR52nLjzb^dkZ-0yT0B&<{&j=rkZmyfS^2#fN4FOPp>+BkCd*=Oe}CSwBrP({atZmuiEuI;v~bv;SvzqYO$p8f83 zzniOGs(uJRhH>M@Mfjpp_pC*_`|i67W$W9&%apt5ILDa490N^x^iJJeSC}z=o~}9w z`hE^|pQx8!dMVx~Zi_9pNR5|xPz;+W=sg_o!LvEn=&DL7MgzUIpPDlzvq7Z z&QM(M%^Dv3Jc&meDwlhoc%jVEue|b#-L!?y&6t3_0G}Id&BTy<^2sL)rBn9989u-} zt1iCa)`z(sO8#FjzW8GF&2N4)D;{2IpIB(2g(CJ1a%G32Z9)v8UWg-vEi{M!*N04d z`;Np;>vdp$Ey|Pm%4+T{x#W`3wbx$T>&4Ebk2p=HOrE3h|I}Z#mp%W-@Ak6)H*)ok zJMNfP#+Lu%yXnSWADN9>;DKNK;umSEl=!}%{p@GGG>6L4 z%6-;Q4Apg^tlLc~^Y8Vaf#AL#y|^w4c|R?Viwc!i?!x~KRX@m#easFwjj!Vtp*rie z^`F>*Gh~{E_Qn1KU$^}7%SSuyv{URO#2&IMt+Z0ZbD{Ec9~r;M9%7XZdIb`mIdf)~ zZeqO3W?&JWzz=xq9&>y?+yVuA< zY5suhIe@K={Q`*1-fbXOmsHdY zNLpM#Q*Zdv`{4A`Pj8?LUxjY3o6FcI2)|DkPSg!=pZVYiKiCs}I&J+9w6k9a{*Epn z!!mBLUjjA*_#)r8oM+G9d+xcXA^DJfm6$o*;v3Olglr~B8z12|zOKh5|9SgBC-!q} zs95KszrWv0m!TOx1^+M`*gF#$A*7>cZy#6AM~7t6^c|5mZAjxoyJh!Fi*u2bX+<4f zApYA6LT~x;Y3U~5OA(@t`>w2x%`HRs6{knvIP9>)>P5q+IBoAB_PVn5gK074u{(4F zGRFP^)mI4EOWIq{@JQYAW5ABX_Z^~*y&G+wi}^qFZ0J5V0|PhQa6{_*_CBI%@m8@9 zb{f!wXY3vS6V*Wog0=v?5U;E0ba}`xgO_daQ_Zu7FJ*C&{bB#{kAKvQm*xv-f7fa2 z>+nVDG_a1@Et_+_x~@YoTz~vS=uLxhu^W))yJN>EVo!GXt`PlM_rLeO@70U9$||b_ z(?OSDfVDxPbXjlQNni}j*zf8g(V7KE>w#dI+`Ut)E+1t|<7JOO{`fixIr@IET$I_4 zcaRo8&wh@%AXHzU_{1mbv~}VnN8HbYvKn9ie@qyR1<3k~P6BdtsD8PR|AhhCCGFX) z|3-}(Ws-{U2@E}t{?Pz?vxlBz-?2^t^G%2r-6!VXV!}Ldg&y*zg+3LZ{7yj{FJ{MV zWQ~6szn{lG%-mt%zylBTmY;fIErEeFeq-g8S2mATe$4Rmr!42?Y53%s)L78SVx0o^ zy{FE7XxA^YzV8|C(Pz`Z`BzwwVeO>x2ve=%%axqjxEXG}7&J~)k!z=mJ|U(>W_p}lIzVY6uE+c$kBl@B1U&OiV8&q|BPJQHeD z?%Of@@!JbM3$4a>gN_%u!u89B*UX9)Wb5?LIp>^Ue)J1`l&XY*fq^W4SLC==gzW%v zVuR%*rec-AxSf{2ZSTw43z_eoXY4)$?bD}EkK;t8eaq0md}I6drumpxtt&$>vf+js zrq+#UW^GTqu(T3*Lm15K*T4RCm0cip-(EXLA>+Fl+tcWUAH+tF?IzFjCN0|!=9XJ- z3DyVC+j^-gDkq1o>p!AntgsA7-4hac((F03-_uV&U9aM_7|*JR)HX}|AD=M; ze0yo~7P5nFOEB-1b$s~Yhf~)xCFS^L84aJLF}IG7CFy$Wko||Nijl>Zzw9d^>#47QKg`sL=~hHh6_B9pyL!KXJZZ~Q=Pzfbg2XP$Xxl(y%iFFnQ@Hy$)=L(}Gb(qYYKRdIia zbC{-+Ov-VKj`(p^3Ck&Qc*|g0zz7ia;7LE~i0=8Brsk{LGVyr(b8*<3Jv0Uf+f*LyT5y6)g zzl~#$JvNTVw|K4@iU8(*L2@U9ZHW79bj#Fm!^83yg zTWqn?ZN+@sdE1&jm{!xYe%NUw-?)pwA8$QVkU$Io(Aa#RJMcHvA zZ@u-_Jo0bcHyQ7->*7P`&I@k8KxQWvP?2?|jLYbP$_D)1)BK*O+XOGuahR|huz)hX zS7>PQ9^nU=!^#B4Z078&yxsqi6YyVvx5uX)8!h)*NekT*bd(L~v~%hu`&GVqgxv(z z4hCb$>A$#*;Ej(v?zmE`iaDOXW%B6c5gh<=Y0C!o+{nS_Q|9}HqlCQx#1NSw%xnw3 zxia}Ac+51rUmlu@K7$Ux_B$-24KWi^O zDx8%7+cEwFsdgj%-_%F2+c9q2w)m>rtle~x4T-y0B)*tc=C^gu`rER7ca&t$0DeU1 zFPhpp>mD{m)@ES8!xyGk0GR`QJ?jG7&_}X1`!mA#0^m>B(paGP;(mh-kiNhgi*Cdv z;XA+l^2;OkCu%3=fXc&o$TvTi@Vy3%1y2bJj|KKStlh)+hy7pK@1|_uTgDpnmB=Qn z3nu17+Fs%|Z5ihS`v23y_d2l7D0Quw{x8%=kR{o_fITdb^-ewY)VTdv=Z72!;>*eY zVCI^&M)?0A3nCj%m@pyM1r(Io^3Zvc%CqKsz0p18-(2_Z6y~YJq4>ug1S&jPxU@j~`2_^Ys+eF51GdpEwY$nJ9pXG3Re-~-AI zJvs5KiEI6;u&O@siqgJS$ewwd3GscK4ZT+b9}!8+7Y7JW68>5^S%~kuU?Xd!vu*VfAYULBT|G zL#Y8{%=JKgSsP)l`v$K~0?F(31d`Y53B;75;jBu{!$hz)pc)d2P-Rs8WW?z+E1MRkt`+k2Tftmfqd49w|kjz*0 z48{z;UoX+b!E2LXl3cw?>+@xjK=OLMO3dp?71Uo(C`fXT^Pr8A%;JMm95Z;mjQ-h0 z*e@Jb8>`y7b+JCkQVU-Xi>)@C6}!9gRr6bB>J8f9PWItHVcaXW{BXcTXhU znhj13jMDYC!qbKK3He~C-!kwmrE`z)6ycYJwC!xD)fQGIAcy=($lhIL)ODUT7<#T3 zenUvRzn1|09k!awh5r@isjn)Zc|$Z@B3xfMD+7LFy9x1!sj@!XNtqG&856IyW^FBdD&bd6+@5pKJvaLK&wn1Dvkx2gczmUZxABpWe57z* z7JZND{42uY!l4X|?S*`#i3`9!G}JNvaP6!~WBnX4tB6@h?6N%jVTxY6-Ttj>_^+V` z=#JqR?Xm>V*$0vR;qW109Y<9_j8S}t_uFs3qP}J4t1Jr*kw(}`bi$tp>zn`Z1K)DX zEhF|>>BfGFv<30iH{N*Tynfuzitdesy$bV*uycf|^-f%iz4zWbdhD^ss_N#2(`B73 zd(q}t%Z03m3^BK`mtc&DnI=rF`*qh{HzMX#uL3@E_*-U`L;v3`TwK@-@L3TtQy8Rs zNyQKEM9g-#UGXo)=McYA)|=u>o(EX3w!{)k1k00@^`AO^O4v=AMma|Uut5r6#?RWt+?Wf4fHwv4HtmtH75Y-+n7&(O16m6>oaT#I#MTkP$yv-ruC@VjBCDpb??U|RsNQN^y9TxHa%sXvkyb4T*RJrd3fZiP@7Rl zRl*N`@PknOqpN6FU$mIg@aodwF{^~MeQ1zPXUv!pXHN|7kggwFK-GYZ9pc|qcgV=? z*f;SnZE9cQ2$dP@FjPk;ope%D-i99bIbx4CVriIr_VHsM`&fkjzY{=g{*-+@&Jta% zz<9mHH`Gu*)@=UuuYc`CvEn?AIO2$g{B3#|i|lK2&b(Abocy#x!y6zo8XBx4a_Xmr zMF5+j*G_x->8GcWQOSR-*%dvxk-ZbU0j|Euq8WWDeg}DBS3THNUgD*6!&br=yVqWO zHPPdvalQ4{%cbwDqJ7|jQ{uCHIcMd=xBkN|8~nw6_uW?o*}2k0hl>5(rOUo1UVRm@ z?Q#*AJVXrlKwKnz^B7mM0DE&&IVpR$fp0H`*NEd&cAG}>XMY4=9qL||o|G}FY1~J~ zEj#u`tM!B&J9g~Yrt-lT^a?!k$Ri;+o_Qb*zN9~!%En&gy#!lrwN(?1jQhO=?Embm zOWng>7=lUqqv1Wq{a&(_Q^(kmT{=4ut8HlQ5~bBp5N03`{`cO^VeQ`ZLbw8`tXN8+(ajP==B<~7nQG0b#G@+{j)Z; z;d%DO>juA{RgXU1JNxnV8W8h0u+I|Xybb7xnr!LpPuy!jnM7+765py90lzK}y|3xs zJVFE8>7?noy#6wsEI0C%kJc*oj47jB_z?K$)jeA`eTMtYLJKWasV{7+WrMG9Y2~_# zJqcT-(S8r#iNHP($BAwOO}>M@$oEjL$=ZuUL^olt$cJ{_n{#~cJTw#U16V-VD#Xwy?ZYMbCa)*b#DdX z*WCZGz1sUN_fpx=?Ys1{exsM+`RAW+qB$u8e9wt)dDyqGSAkf(F1^byzkIKfTc;}! z%lRmk{)NfSpzYsDHH~fdp&}+bc6+7&(gIA}8-GzM& zs@!9y-TDaFEv{AFPj~Adn|Ch&zOwkCFuu4n`mSjQ^l+W@56D|j(B+{SzMr@i4gcZi z+fBe9mv1m2w=~e|(r&I9lX~f!KtE_`vyL-#-lTIr)n@DH@^wiRrzT0-RngCzxwK{@%rC@uYi}< z(6iX5edP$&+sa%tP;nONz-Gp_R6J|>#Vhw?C-qWZo6fE6ZlhC1>0}Gy^r?mM1Pb1 zVWW*Us)~$;bn%zOPnq}&X@Cv-Jgv!CfBp5F%7BcJ7m(+e+kE~L_$pKh%rl<8bDr*< zu&vb zTmO9X%{S+bgVE5Z*qgkHz2x)MZ`v~{`seLc_pA+QCJFE|N=}fLy)Nw&!ll_skKb-{Z)IPt?0NtVT)bGPX^$M^Dy{8<= zU1p6cd{DzP_)vEW==VOozDpC=*c$W3aU+IYFF?Tl4&P~@$^QhIvs1or#K#jMUSurV zW}9u|*akLl$~kL~iPaYxuNaG%OI)D6;kl~hH%*?%j)A>vk2jUCqu2kc6MJ+&^w2}W zwYy6%y>z^XqbW=1NPGxa$nIopgeKHqiLa#sQP1iQ2(*SqNG0x?sY#vr@zlu0A?;TtcI z(*geXt#c8L-ZGtX$|>=C#3S}SW8yer!*~Dx_{TpUzgzYXagg1-bdB%xNTCm|(9MQ- z;30^sQ#PRAbo1gG8zew|`Th^xGj(|X{r7v{Gw~OR|K_H_dcLxO@&)*^Un>6)Fef+B z$2d_oOqw*QDIfASDl$XQuHY-3`WHPA^J7+cE%-#Coz%Yd){AnUsA23=;I6l0f9Hvx}oz!xiM&pr1v zf46-k?r31#e?)aUXIAAW|9fK);G1LQopuR5=%9ld+L!i*Ze%TBg%wt4`k(QMbr#4| zeSLiqZHW8~puZbGetgqAq-*>Mc$RAyiM?9}KzoW0z&6S^FAyiHvG6 z8-J}8+Mj#wxu&$qkMSEozlNWVj|T2BR`Gu*UZQ!A^%?;A`Sf6A$-ekt1pJWD?SDqt z3S6X@n(6?W&3qOYuEn3#S6{s;ZR(Y}0`NWY<)8N+^Pu@o@Uio~hktLofbS$clrMeV z`>C*17_Apy5;mp%jc^xh?tjm^xbs9}&g-0}Uy{W6BN8frn@POS5!r2E1PFT`=K;6FFnWRnQl30pNj1~#yVMQHwJ&c6Nj+wI2-pQEGo z$tXz~?$puPc6C|`EU8qU7dF)sd__BL06!IoQxWHdZjD$XdF-LYUrX^dj?-(`w}86$ z#Q;g_=ASw)F02B^D^b=BH?;xuU~6j?@X<)=PjP}NlVZ3M^)#K(cJ$A8V( z6@B!Xdj}qPV5=m?@9|xQ#z5tPU26?tmB5}a-u)N&u3OoE!Bhxq+uK=lB)XdVg?*1W zqVOxN0&`W0yn*e+j;W!`%3xQe+!PyE_u}%SPia*?KyS|6XwoXUf;z`mgA6u$^ynZx9=b2k zy@iEC4fYkm$b`Aeij2#5fqw-4gxD2Z1@x=vdEjHP9f$NmDfAJm7F+C425gzgYfUzN z-OFqj#s>H&)&vrhgMHNSbwd`#Czo}3oZ~}F+!oe3vY#aTKf)K~$;0!^SA^mkWtiH(iK)@h_oik5`g=kLmblA>+x9g~ZleTiCPp4R5E;G0yOK z{o11^IY-v3O&q+Se6=xy=lz@~4{@qm0OH4!7Z_3nW4Ir`s#UI>_s6#fU+x<`AIAN` z(>S3dnK&VOZ4Be0C`mF-G)Xc}bRc=8Hlm(D#Gi zHZf}W)=XycH`qV;c>G}}iN@b-lBklbCp3}sgzETo@WW2Vg!tw7)#QbVgU|3#^2obm z&N9k%ys%F=tTtA^T?71-HV_^nyhO-Y|F94}?;ApN?f(>VkN++bep|SnaFlQ;!m=WI zq7Z(A25krL*ZYSFmlpOi%q!yHc{BCpdZyL-n5uX87WR^!bUx8Te|=V%S}yFh_%mZW z#D^2zFfpOf3lReddti$Hhn+Ux%Yj1byqf_3#!zh2RzJr_8-Gr0)cDBf(Lth1#8!?D z2EB}Ly^xh3dE4vlb=eX4kaGEhuwGVkj%^gWu~FtJZ!&}j57Y;`bo5Uted8+SKU&xc z_=+-lS=dl^Y#jK3n00o+0=4nSGtWE|uZaox6TKjsuu)VA6O}l#e!cAI0*UqHi&fWZ zQQ32a^+4#!yNWeoGd_19nDU{}DN%{*8H0)BMx-L1d`y;L9TXm{4F6aoCB z7GHevdi^QAI5y?8&pz9b3_cI;dVyAb_|G}#oQC|AC;GlEg+*X-J^Yfeo))pLS^<2J z-E|7|YZC*8zKegoofkH}cEDXrs>;tj9u8* z?E8$B_`3H_}z+Q2wp#`3&>Te$cFS_WWhVsJ~cLEp#9NzPM zl^vT~OwX#Tu39-6)UO-Etdo086AR0fSH~NL^|8Ooy%OL-SVO~F8Y}BiTyZ8pTPw}Ekz_-mmx|IptK5Hruvr{h%NZRWa+ z`43&;si&SA5&I4OrVWgT#8NQ!E%IgmA|HhMM7wbgJoDdgO!=$gf3Srh{TzD_{iaHoGG$6V zzog9gkbK@Q{{9d{6raZpGW?kU*!ktJTU9`+LeLmXSRNqbM}eEmxcL{eJN=R z&WRO;--u|aBQy|u4lw%`5g&y2T7?w-2jxG?&cEF`*5NTDqu(I*nLP$F*CUeyjE^=A z@H>F5yT+c=D&R+8r>}FyJp&E;&vx{2Pd@o%gUtnb1MtO#R^~BH#HYdLXUEbranJ4e zO^YtNXsgAKJo?X%n!+c?hYUUK=)YYZ3$_k?qFmdb$%`ZWFkr9aWG)8mzC>KXR=}hG zzAGuSefXA5Q>!{jd;eR$d%j~gmY!Ws@?`D?8UHBXOE10D)JGUI(E-@w-MO znS1%3?fcX%yuU+V{0zJTV9Uw8OI_v#X!ht8c2GK5fX~}?!aBY9^X3J&&%(vHN6dPC^KNpaER?AT?5Kz9=|F=WyEd&d7|$GATda41kB8sI z-hs?Zd{ATp=rw-R@cwq$p`pk)?DiYIkAAaNzvu?O7$5JvK7j1$?Z)(@PSAn7zOa0& zS6p#LWc$pTx<_A=H=Zolm}cME1~&kI<^a5|L5Iz-@PuQrP0Sejf{aJA~!NOFmE$&WC8j%hqpKRFy58r z15P?Wb;|(X9`b*;>1O<=&-nK6u+ya6*r3c_33eL3bH+)=4?B(I{1-aXZe;@BLc@5V z>$va2!{u530FR6R8}kG69N#S7XF{6 zcaUF0^1J@(3HYR_V6OO!u-!VqFZD>_GQyz-jez>aa8K{9QBcq>b;+%+QGDoX(!Weal@z`>+Pxs)Qp0-dHJYT7VaV( zRvW9|N`Y}}cj4v2M}*G{p%`jer}c&qpM*z+zZLTANMSbty#Z_C?h#U(F(RWQ*PtK6 zmmhs5=iGDUcfE7B5Pbq^bqbh|ej|KU7}E?dL7YQ$De&6JmdH=&tkBbQ&OQE{Hf>t` ztm7|3TC_`9a+Q*50{X~)Li$5opR7M~eMsUSob!iwkyo8{cn2!|vdh7EJsrA1Z2v`c zr&+~8emeg6&lnB z+krjxa!h-g4+Xj*?1_2wNk-O4lkcsyi;Q3N*;Zh0y%h7k=)lsz=ycH=rnQ4v`-q=y z7T}LW{t50|umJEV@w<8KB*;I;78cqY8QTzP(oXExkO$~%2_3XU-oCV~o_zJ}TUq0-D#)kx3YCC!dYg|IU^ubu&d>uJ8?Hjad z4V^RJvff$g#fUu&n_xS@*lN<#(Ko*x%l~F?;db)kUDh(0J$1|4?-qM#QU>@$7vlen ze^9&h^FHG$I`}*8xTB2ZV!tL4%{KtQWb=G|x{iICO35fmj#K81g%2& zR)82V1K6p{)^VZ(rvB4>K#SHFJfG+V;k|(+h5uf0#TDD~A@$M0T2|%}vmdeVnO5$J zUe+p824tY|oMY=K8{mtfzihDVu*h*oK!08vY7tt{y&GSXBJX8Aiwp$KLS&w+IB#dq zr%-;R8{&sT@8r3U9H+FAsgbAAFXefy&@;@#q-*a*Unqf;hmjqWHo7tN1h0@!S%6)V z^)*IT$4<+b$2yF%fcafCBM;IB?_mFH7g#&U_ZqSpb01$r_A+RP4}wnSSkaB1<{c3K zv{rzAxbMFEHpt)be(($InW?m6zX1F|naj8a;jx&T*%z}FNa+6?vd{%v^A`|i6hm3PkLOW@LPjh}o2rOaQn0s0l+ znh@lAyC1pq6uqWSXg48z^mxv9*7}210shkXDWXfnCX2rjep~n<7~dQGaLNF5mC!7N ze;6xlnSWy}Fh1D$Hv8lUd=r#MjI7Z&KtFCJ9*WX>E=g}59g)TU&OAfEDhfvDz&c?A z@VJcg_=y@lqgh)|djQPOtQ%m?FwdCx(~cJZjNaQopH8B~4?jHZ)u#U_FZvw(*NIC7 z(0547AZr5oMv;H{4)7yD9za*Y^#Thlkjux!WNyhX=+*GZfw^^zCNx~a`K-#mVW z?~3n$Zvef68RO|cj7@ys@D)ZL!{*I(UipT05}oipuKY+jjuuADNqND<8$cfo;Fm@k z_*kO{;2bnQ$@pud|Ku7T0-1^SAns@$AfF^n><=!W+j;|D!o17X?(kAFTP(hZo<#;K8ViD$~R)2nF0LS zcqUKWO#IuEG~t;UGt*#s{rQ$Km#&rg+B|D~%c1Y$i!W|ir^a{88dC$T;pZLv$g{xl z)yEE$2e#EiaUNOvbNa6Fje#y3@GUZ9jk!;K)5jR6vSJ93x9E5+p(U^W+>~!`o$#$h z#QaSI{2C44!x#fEgs-}de6OahNE4YL4e$-)xdgETwE~nOP9y7kpp5o9C43j@`^YWJ zFTcE@BVN-5f0_p9?v*dH3CDee?Z7sAIsP7qDUda`66XlMfO(020@60jY4CJuz`9Lq zoe%V52Py;jrtcTV;|+6M);!IaN&g0Y@{L^%#x8tMnajvShz(^~VcBq;5=7>Y^I;7u zbY_*srq!JD4*FJ0mqwb{h)xhz3Cz>S2;(x)Ki~t9AMj^PljG2lq31(a$vZ|aNYY~N z94qV;!1MJ9|1Mh3LJ}-M0mD^opIl1~;`*B`T){Rbzv0MLPE3jWk%f)G!kQJD|jVOydTf zd}?C`39K!c97haRY)!|0o)?u3$6-mO>llMMCYK#MSakG}sjmAuTJ#QZt#Z%+^`pv( z;)ZgYdNB18abG|6dK=JdT$_5;3lq6ca2Y)gZXvA5PwP`k`rE>bgzQ`Mw2(23vF4w` z2ZX;CvQBt4VJBd;GGPpQT&S8TgpcX@?+ZU5EE_(m1ScEavCd=K&_yA4B9pV%J-i9X zIqM+di;-o`^U0C=y-_$uSO#oq&2O>X1J7H;Zb8g)WGj5xM36zo!OhwrkTK{ClOAiy zSn~ldkr$AKh@)))`#W+7bCXF|$Das?3yT19UaX(gG2T0z`Pu+;FuoD+c}9O>{xe7B zL*@|dXzuzp_!6VPA$=kCq`8H8fO@+^7?%-Q7k=3W_yfaNv!=$?^B@PC^)m2$?*F7k znygVW`RI7Hki4@XZc}6@6U)M;&78x>8$JO3-fnjjub#R~^S2@`{JTVu!ILtGqrb+_ zQnoCBCx!;zL5D-TBO4i=C;G@dz&aq4uZ|4aK3D`Dh5m@pE4ghN$_L)u=$qI#fc7K} z{Hkc@JV0KSorQ7UC$FADy2$-`^*$zV^h&&I5S<1wUyA_W418IVhRf@I(8O~-?X=U1 zBwK$L9j2kLs6NvqVr*AR2ibX1p@EohM(1SrbMs!Fqa7pHwm0Q@*F1x~W5t<gwM=;Dyy+68NWqF;(Oc+05O@7B;_^p7UKkh$N=(dhX^ z*AyY+VM}&6bkX!X{1MF<&3u9%ZaXo4vE4$G@ChNZUTl}mTUKmW_>FtwVVHE|BfKfR zYaTthoww!piFgy!2hW=ausfTvoN`fbW-TDs#E!6I;Bb%U(L>;$lLsgh?+Kyr8P2ox ziY8rr51@(Io9K?rm}K}V<135)34api%OtOue>Ul)9nDyv_ns6!#QQ~Jz08o!!|0aK zPoi^&za|}I4*UoVFjk`*=9#>)_N;kss?xlabc@79A#Z44ohR?Q2p?3`2iuf~qqYA3 ze5HLb=_8Bf1?-M??1{08Rf9hh>r{XUIT4dAa#*+oyEAx_7|g|YrO>pM1a`LMS#hUmQ*jSYml!j53>GyW{O#8l%y`VW3V{Eywi=3CQZ_Q2m;F>+k~Udh!K z5zZvtwEfWV12X-M`k+to4#+oP+7<558B(5X+qwkx+cVN5iwrilryn`$Y59aejqkb9B&9gdA z5@y-I^+*FymNSKMI((<-*L>q6>-eyJ!@Ceq`ph%W^wPx~=h3gB7sy+04Fo0^Tj_`{ zF{YFCFU&J}=R*1%>#UGnowcc?$@f?W#QI$&g!;bTF33W=YaJ@&Jfn=%y~Xt&mApXOjtF*Qy|yFn_VJ&SO_0Z{W4E-4&Qr` z@Y}-Gg)M>XQ{&=_7>La&*jF%1)hh=(%s3$#j}g8iT+liW=zN5*Uq~l6$3C4?2y@hF ze9f_Hu=DLxW?c6RZTbdu4c%^eOgT-t>vdu3#njWAgY|}uX({1`!V>LFqm|rhLVWRl zEc}CzH3=^!$oVxw_<*kpR~7OOMJ*F(@@<5d36Z6rBL*AeIpZMn1IW0E46pz17d|U| zQurI;_QDSf+l7%z0NwQU!k2`%Fc!oAWdZsq*2g7lHJ?w?fCrdg*b0o*OUDZ7I+Kg| zAHxrtxP{QoyZH4h9r!ldnffjQpVY(5eJ{aJ*>e~CBK$b}gZlt~7vyu=&*tlpJC9b( z!GyoLLTRs%lOR3?#_fU2FTcDgOWFb5JAAskR-XK!72PYcvkzG74G*gH=>u6bF-HGh z_(W(wR{S)PC6N8l?b|wg_Ta%!iTTO+@A+tAE=y>l4L&0D0mIIT!q@Vw%YcW3CcZKB zP{bp~7KTr3s4Z>UsHddu=uf^r&QSG^iLq|K>pQ1y(6=lp8H)W@_}1|c&02eJr$v8K zp7e2-_nuGprt)nT0eES2GRTt1f%M@bKp#)OJNlA6K5eb*mUNHsevFCemh+5DF1^SQ z%KHT&W2PC$Z!20iCS7b>thK?;)rv1Zb)1ZujN{BLOmpv`TWtlf1JifV8>M|yX=x|x zoS9pg@AgJUZFLPPHelpTcR!u9{N4Z9Z(ejT-!TVY*h;){lP2R2`-!E=?&jIFW1@>W zLG(-`o=B_kjO_W?OX1(!frKxBe|a&Fe$1B_;~q9g<_Gj+tVL{f-9LR$v^*kwD@)g8 z)53hg9wbI)VT{FZ1i6~EBaG3=G(~NXj9t`?@a;VGFdh>Z8d)A&7 zo&%a#1D7KPKT}TTO#_sj^@Zr}+&;)$z{ErFu zDG>7;S$^;M_>e!(VHbrCY^01o=#Uw6@C(51hkSvZih0)ctFr4H*+coiEJVIDfM3`B zS?gBeW6{rHPo!=5uHawbUE!Un2kL_{fw`VF$$U5V+8vt~zE|ZtSqMKiMrgyjx=4+& zgFOOzSbIyq#(qTp)E71#Vke_FBW4hN8e3PIEhSl_jC{A5O-OQ%+5mq3KOxzIv5xvj z?+X|?1HCK0cl0g%z4@-_)65?sKQ8K2c`+U%bB+}HU?JV?7e39n>jJzSG8uWJO9yBR z>Syxg$+5pY^C@!(Yd%A|dCIFi2876kV})t3g#Mf&e40F5z^9J3*u52CqbUsSR5$KD5JNH9=CV!6oLYuw;T^}fXB|%dzQ%+NEQxB>a zmH@;b9Uu2v!Z!7=e2<$5j}~4nq}`rM@PXtW|M3jZwgZRgWzwF8jS`;@_#gNWkaPUu z6C3TrdJft5yTYO{RxkWr_*5Rh5yS5#YX!-ZyhjW3z~*{rCj6X9hcOoYo^OpEHe6&5 z^DJXpBBNoS+AJeO+Ta_QSx@P(8-Kd%3R`VaU(_$K5o0DTbh z89LSwBzaO!w{GXwy}xIzSx1Kf{|zE)}kW2xO!as zIde{XDs8*pj@0$k5btE(vybqW=n;IjVEZ|94o|8);0^db;SC=0^*!^vd&Dmvp5L8c z-RJCUjl4+)z)@ioF_gFj6>I?ts`*X;~IScJZ=$S+)!SOhmSEX~E z_&nN<`Ps)ih0CO`+j8_Sbo{pqCJ!|6R_u&Ei~+R$(Aal}VA$-^zjNX7$qtg+uEO~!qWtRDyg& z`~pll7z?w2_7EMkIq)&&oIIdsQ{^U;24iH>ujspbux=#}kUqYpc_3Mv%J*@39{Uh! zl0Nfc9>7mW&+`pVwS9xq-X;A!z9c3MJ^z4kMw+dhxfGrfo*8}Tm@#8wJBu49$)sr? zl{RBZFn#1i{U7V((PP3>!b=i63O*X%$H!ZmJd*T-^~3jR+J+;tDRY$p=6&onAyE2^ zgTHm>Jo|b03T!RVYxd_p_Sj?XN4)2ZiAtMs_v>kUZNU4|*U%%N$E7d8)381XJ~Xs8 z!0Mk%8Q#@L2>wg}zD4-&Am1i@WxQsayj)=172R(MnLmKreR^|j3)ov2ml)r$-C^@d zY!|Vw5d6IhjO|L3`PaZm_(Ycf~k$3WdJbU|P?(GxD z1m*?vJnxbQX^|#r!-v7#jTIK;!>H@CgvSYgnBb3eJX?saGz5$4kJFe#XJ`!!^-Z6p zAHkQPcc4x5%=yAOoJ4r{B*R-U7P97#G#HEdHck9u#uYt#gO$ddI^Q2362SCLd;ob5 z`2#;J8x*m?2e@Bub?y?c)E3g7?mhkP68 zy?k~o?&;kVgp=&|`F`l<*iD(Yu{xV5Hte&{KC!Kpaffz9-%gwkTSiX! z5c=dN@Mo|AT?K1i&@VBM@*X4@K9>`WN~KF(-MRx`MBv zF7?d*Lib&h4rSrfKUR39aFP()H+ZygDIwoW9bQ>bHZ6UPeY^=~fu7-6-WeD%nAzNV zhp{GYoKgF%iQYkvHDF?Y7|nOWe_S(SLF!!4r-%O^cT(hu literal 0 HcmV?d00001 diff --git a/ffgeom.py b/ffgeom.py new file mode 100644 index 0000000..a6d2682 --- /dev/null +++ b/ffgeom.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +""" + ffgeom.py + Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org + + This file is part of FretFind 2-D. + + FretFind 2-D is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + FretFind 2-D is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FretFind 2-D; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +import math +try: + NaN = float('NaN') +except ValueError: + PosInf = 1e300000 + NaN = PosInf/PosInf + +class Point: + precision = 5 + def __init__(self, x, y): + self.__coordinates = {'x' : float(x), 'y' : float(y)} + def __getitem__(self, key): + return self.__coordinates[key] + def __setitem__(self, key, value): + self.__coordinates[key] = float(value) + def __repr__(self): + return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision)) + def copy(self): + return Point(self['x'],self['y']) + def translate(self, x, y): + self['x'] += x + self['y'] += y + def move(self, x, y): + self['x'] = float(x) + self['y'] = float(y) + +class Segment: + def __init__(self, e0, e1): + self.__endpoints = [e0, e1] + def __getitem__(self, key): + return self.__endpoints[key] + def __setitem__(self, key, value): + self.__endpoints[key] = value + def __repr__(self): + return repr(self.__endpoints) + def copy(self): + return Segment(self[0],self[1]) + def translate(self, x, y): + self[0].translate(x,y) + self[1].translate(x,y) + def move(self,e0,e1): + self[0] = e0 + self[1] = e1 + def delta_x(self): + return self[1]['x'] - self[0]['x'] + def delta_y(self): + return self[1]['y'] - self[0]['y'] + #alias functions + run = delta_x + rise = delta_y + def slope(self): + if self.delta_x() != 0: + return self.delta_x() / self.delta_y() + return NaN + def intercept(self): + if self.delta_x() != 0: + return self[1]['y'] - (self[0]['x'] * self.slope()) + return NaN + def distanceToPoint(self, p): + s2 = Segment(self[0],p) + c1 = dot(s2,self) + if c1 <= 0: + return Segment(p,self[0]).length() + c2 = dot(self,self) + if c2 <= c1: + return Segment(p,self[1]).length() + return self.perpDistanceToPoint(p) + def perpDistanceToPoint(self, p): + len = self.length() + if len == 0: return NaN + return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \ + ((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len + def angle(self): + return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180 + def length(self): + return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2)) + def pointAtLength(self, len): + if self.length() == 0: return Point(NaN, NaN) + ratio = len / self.length() + x = self[0]['x'] + (ratio * self.delta_x()) + y = self[0]['y'] + (ratio * self.delta_y()) + return Point(x, y) + def pointAtRatio(self, ratio): + if self.length() == 0: return Point(NaN, NaN) + x = self[0]['x'] + (ratio * self.delta_x()) + y = self[0]['y'] + (ratio * self.delta_y()) + return Point(x, y) + def createParallel(self, p): + return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p) + def intersect(self, s): + return intersectSegments(self, s) + +def intersectSegments(s1, s2): + x1 = s1[0]['x'] + x2 = s1[1]['x'] + x3 = s2[0]['x'] + x4 = s2[1]['x'] + + y1 = s1[0]['y'] + y2 = s1[1]['y'] + y3 = s2[0]['y'] + y4 = s2[1]['y'] + + denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1)) + num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3)) + num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3)) + + num = num1 + + if denom != 0: + x = x1 + ((num / denom) * (x2 - x1)) + y = y1 + ((num / denom) * (y2 - y1)) + return Point(x, y) + return Point(NaN, NaN) + +def dot(s1, s2): + return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y() + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/g_code_library.py b/g_code_library.py new file mode 100644 index 0000000..e6b8e64 --- /dev/null +++ b/g_code_library.py @@ -0,0 +1,2071 @@ +#!/usr/bin/python +""" + g_code_library + Copyright (C) <2017> + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" +import sys +from math import * +import os +import re +import binascii +import getopt +import webbrowser + + +################# +### START LIB ### +################# +############################################################################ +class G_Code_Rip: + def __init__(self): + self.Zero = 0.0000001 + self.g_code_data = [] + self.scaled_trans = [] + self.right_side = [] + self.left_side = [] + self.probe_gcode = [] + self.probe_coords = [] + self.arc_angle = 2 + self.accuracy = .001 + self.units = "in" + + ################################################################################ + # Function for outputting messages to different locations # + # depending on what options are enabled # + ################################################################################ + def fmessage(self,text,newline=True): + if newline==True: + try: + sys.stdout.write(text) + sys.stdout.write("\n") + except: + pass + else: + try: + sys.stdout.write(text) + except: + pass + + def Read_G_Code(self,filename, XYarc2line = False, arc_angle=2, units="in", Accuracy=""): + self.g_code_data = [] + self.scaled_trans = [] + self.right_side = [] + self.left_side = [] + self.probe_gcode = [] + self.probe_coords = [] + self.arc_angle = arc_angle + self.units = units + if Accuracy == "": + if units == "in": + self.accuracy = .001 + else: + self.accuracy = .025 + else: + self.accuracy = float(Accuracy) + + READ_MSG = [] + + # Try to open file for reading + try: + fin = open(filename,'r') + except: + READ_MSG.append("Unable to open file: %s" %(filename)) + return READ_MSG + + scale = 1 + variables = [] + line_number = 0 + + xind=0 + yind=1 + zind=2 + + mode_arc = "incremental" # "absolute" + mode_pos = "absolute" # "incremental" + + mvtype = 1 # G0 (Rapid), G1 (linear), G2 (clockwise arc) or G3 (counterclockwise arc). + plane = "17" # G17 (Z-axis, XY-plane), G18 (Y-axis, XZ-plane), or G19 (X-axis, YZ-plane) + pos =['','',''] + pos_last=['','',''] + POS =[complex(0,1),complex(0,1),complex(0,1)] + feed = 0 + spindle = 0 + + ######################### + for line in fin: + line_number = line_number + 1 + #print line_number + line = line.replace("\n","") + line = line.replace("\r","") + code_line=[] + + ##################### + ### FIND COMMENTS ### + ##################### + if line.find("(") != -1: + s = line.find("(") + while s != -1: + e = line.find(")") + code_line.append([ ";", line[s:e+1] ]) + line = self.rm_text(line,s,e) + s = line.find("(") + + if line.find(";") != -1: + s = line.find(";") + e = len(line) + code_line.append([ ";", line[s:e] ]) + line = self.rm_text(line,s,e) + # If comment exists write it to output + if code_line!= []: + for comment in code_line: + self.g_code_data.append(comment) + code_line=[] + + # Switch remaining non comment data to upper case + # and remove spaces + line = line.upper() + line = line.replace(" ","") + + + ##################################################### + # Find # chars and check for a variable definition # + ##################################################### + if line.find("#") != -1: + s = line.rfind("#") + while s != -1: + if line[s+1] == '<': + e = s+2 + while line[e] != '>' and e <= len(line): + e = e+1 + e = e+1 + vname = line[s:e].lower() + else: + vname = re.findall(r'[-+]?\d+',line[s:])[0] + e = s + 1 + len(vname) + vname = line[s:e] + + DEFINE = False + if e < len(line): + if line[e]=="=": + DEFINE = True + if DEFINE: + try: + vval = "%.4f" %(float(line[e+1:])) + line = '' + except: + try: + vval = self.EXPRESSION_EVAL(line[e+1:]) + line = '' + except: + READ_MSG.append(str(sys.exc_info()[1])) + return READ_MSG + + variables.append([vname,vval]) + line = self.rm_text(line,s,e-1) + else: + line = self.rm_text(line,s,e-1) + VALUE = '' + for V in variables: + if V[0] == vname: + VALUE = V[1] + + line = self.insert_text(line,VALUE,s) + + s = line.rfind("#") + + ######################### + ### FIND MATH REGIONS ### + ######################### + if line.find("[") != -1 and line.find("[") != 0: + ############################ + s = line.find("[") + while s != -1: + e = s + 1 + val = 1 + while val > 0: + if e >= len(line): + MSG = "ERROR: Unable to evaluate expression: G-Code Line %d" %(line_number) + raise ValueError(MSG) + if line[e]=="[": + val = val + 1 + elif line[e] == "]": + val = val - 1 + e = e + 1 + + new_val = self.EXPRESSION_EVAL(line[s:e]) + + line = self.rm_text(line,s,e-1) + line = self.insert_text(line,new_val,s) + s = line.find("[") + ############################# + + + #################################### + ### FIND FULLY UNSUPPORTED CODES ### + #################################### + # D Tool radius compensation number + # E ... + # L ... + # O ... Subroutines + # Q Feed increment in G73, G83 canned cycles + # A A axis of machine + # B B axis of machine + # C C axis of machine + # U U axis of machine + # V V axis of machine + # W W axis of machine + + UCODES = ("A","B","C","D","E","L","O","Q","U","V","W") + skip = False + for code in UCODES: + if line.find(code) != -1: + READ_MSG.append("Warning: %s Codes are not supported ( G-Code File Line: %d )" %(code,line_number)) + skip = True + if skip: + continue + + + ############################## + ### FIND ALL CODES ### + ############################## + # F Feed rate + # G General function + # I X offset for arcs and G87 canned cycles + # J Y offset for arcs and G87 canned cycles + # K Z offset for arcs and G87 canned cycles. Spindle-Motion Ratio for G33 synchronized movements. + # M Miscellaneous function (See table Modal Groups) + # P Dwell time in canned cycles and with G4. Key used with G10. Used with G2/G3. + # R Arc radius or canned cycle plane + # S Spindle speed + # T Tool selection + # X X axis of machine + # Y Y axis of machine + # Z Z axis of machine + + ALL = ("A","B","C","D","E","F","G","H","I","J",\ + "K","L","M","N","O","P","Q","R","S","T",\ + "U","V","W","X","Y","Z","#","=") + temp = [] + line = line.replace(" ","") + for code in ALL: + index=-1 + index = line.find(code,index+1) + while index != -1: + temp.append([code,index]) + index = line.find(code,index+1) + temp.sort(key=lambda a:a[1]) + + code_line=[] + if temp != []: + x = 0 + while x <= len(temp)-1: + s = temp[x][1]+1 + if x == len(temp)-1: + e = len(line) + else: + e = temp[x+1][1] + + CODE = temp[x][0] + VALUE = line[s:e] + code_line.append([ CODE, VALUE ]) + x = x + 1 + + ################################# + + mv_flag = 0 + POS_LAST = POS[:] + #CENTER = ['','',''] + CENTER = POS_LAST[:] + passthru = "" + for com in code_line: + if com[0] == "G": + Gnum = "%g" %(float(com[1])) + if Gnum == "0" or Gnum == "1": + mvtype = int(Gnum) + elif Gnum == "2" or Gnum == "3": + mvtype = int(Gnum) + #CENTER = POS_LAST[:] + elif Gnum == "17": + plane = Gnum + elif Gnum == "18": + plane = Gnum + elif Gnum == "19": + plane = Gnum + elif Gnum == "20": + if units == "in": + scale = 1 + else: + scale = 25.4 + elif Gnum == "21": + if units == "mm": + scale = 1 + else: + scale = 1.0/25.4 + elif Gnum == "81": + READ_MSG.append("Warning: G%s Codes are not supported ( G-Code File Line: %d )" %(Gnum,line_number)) + elif Gnum == "90.1": + mode_arc = "absolute" + + elif Gnum == "90": + mode_pos = "absolute" + + elif Gnum == "91": + mode_pos = "incremental" + + elif Gnum == "91.1": + mode_arc = "incremental" + + elif Gnum == "92": + #READ_MSG.append("Aborting G-Code Reading: G%s Codes are not supported" %(Gnum)) + READ_MSG.append("Warning: G%s Codes are not supported ( G-Code File Line: %d )" %(Gnum,line_number)) + #return READ_MSG + + elif Gnum == "38.2": + READ_MSG.append("Warning: G%s Codes are not supported ( G-Code File Line: %d )" %(Gnum,line_number)) + #READ_MSG.append("Aborting G-Code Reading: G%s Codes are not supported" %(Gnum)) + #return READ_MSG + + else: + passthru = passthru + "%s%s " %(com[0],com[1]) + + elif com[0] == "X": + if mode_pos == "absolute": + POS[xind] = float(com[1])*scale + else: + POS[xind] = float(com[1])*scale + POS_LAST[xind] + mv_flag = 1 + + elif com[0] == "Y": + if mode_pos == "absolute": + POS[yind] = float(com[1])*scale + else: + POS[yind] = float(com[1])*scale + POS_LAST[yind] + mv_flag = 1 + + elif com[0] == "Z": + if mode_pos == "absolute": + POS[zind] = float(com[1])*scale + else: + POS[zind] = float(com[1])*scale + POS_LAST[zind] + mv_flag = 1 + + ################### + elif com[0] == "I": + if mode_arc == "absolute": + CENTER[xind] = float(com[1])*scale + else: + CENTER[xind] = float(com[1])*scale + POS_LAST[xind] + if (mvtype==2 or mvtype==3): + mv_flag = 1 + + elif com[0] == "J": + if mode_arc == "absolute": + CENTER[yind] = float(com[1])*scale + else: + CENTER[yind] = float(com[1])*scale + POS_LAST[yind] + if (mvtype==2 or mvtype==3): + mv_flag = 1 + elif com[0] == "K": + if mode_arc == "absolute": + CENTER[zind] = float(com[1])*scale + else: + CENTER[zind] = float(com[1])*scale + POS_LAST[zind] + if (mvtype==2 or mvtype==3): + mv_flag = 1 + + elif com[0] == "R": + Rin= float(com[1])*scale + CENTER = self.get_center(POS,POS_LAST,Rin,mvtype,plane) + + ################### + elif com[0] == "F": + feed = float(com[1]) * scale + + elif com[0] == "S": + spindle = float(com[1]) + + elif com[0] == ";": + passthru = passthru + "%s " %(com[1]) + + elif com[0] == "P" and mv_flag == 1 and mvtype > 1: + READ_MSG.append("Aborting G-Code Reading: P word specifying the number of full or partial turns of arc are not supported") + return READ_MSG + + elif com[0] == "M": + Mnum = "%g" %(float(com[1])) + if Mnum == "2": + self.g_code_data.append([ "M2", "(END PROGRAM)" ]) + passthru = passthru + "%s%s " %(com[0],com[1]) + + elif com[0] == "N": + pass + #print "Ignoring Line Number %g" %(float(com[1])) + + else: + passthru = passthru + "%s%s " %(com[0],com[1]) + + pos = POS[:] + pos_last = POS_LAST[:] + center = CENTER[:] + + # Most command on a line are executed prior to a move so + # we will write the passthru commands on the line before we found them + # only "M0, M1, M2, M30 and M60" are executed after the move commands + # there is a risk that one of these commands could stop the program before + # the move is completed + + if passthru != '': + self.g_code_data.append("%s" %(passthru)) + + + ############################################################################### + if mv_flag == 1: + if mvtype == 0: + self.g_code_data.append([mvtype,pos_last[:],pos[:]]) + if mvtype == 1: + self.g_code_data.append([mvtype,pos_last[:],pos[:],feed,spindle]) + if mvtype == 2 or mvtype == 3: + if plane == "17": + if XYarc2line == False: + self.g_code_data.append([mvtype,pos_last[:],pos[:],center[:],feed,spindle]) + else: + data = self.arc2lines(pos_last[:],pos[:],center[:], mvtype, plane) + + for line in data: + XY=line + self.g_code_data.append([1,XY[:3],XY[3:],feed,spindle]) + + elif plane == "18": + data = self.arc2lines(pos_last[:],pos[:],center[:], mvtype, plane) + for line in data: + XY=line + self.g_code_data.append([1,XY[:3],XY[3:],feed,spindle]) + + elif plane == "19": + data = self.arc2lines(pos_last[:],pos[:],center[:], mvtype, plane) + for line in data: + XY=line + self.g_code_data.append([1,XY[:3],XY[3:],feed,spindle]) + ############################################################################### + ################################# + fin.close() + + ## Post process the g-code data to remove complex numbers + cnt = 0 + firstx = complex(0,1) + firsty = complex(0,1) + firstz = complex(0,1) + first_sum = firstx + firsty + firstz + while ((cnt < len(self.g_code_data)) and (isinstance(first_sum, complex))): + line = self.g_code_data[cnt] + if line[0] == 0 or line[0] == 1 or line[0] == 2 or line[0] == 3: + if (isinstance(firstx, complex)): firstx = line[2][0] + if (isinstance(firsty, complex)): firsty = line[2][1] + if (isinstance(firstz, complex)): firstz = line[2][2] + cnt=cnt+1 + first_sum = firstx + firsty + firstz + max_cnt = cnt + cnt = 0 + ambiguousX = False + ambiguousY = False + ambiguousZ = False + while (cnt < max_cnt): + line = self.g_code_data[cnt] + if line[0] == 1 or line[0] == 2 or line[0] == 3: + # X Values + if (isinstance(line[1][0], complex)): + line[1][0] = firstx + ambiguousX = True + if (isinstance(line[2][0], complex)): + line[2][0] = firstx + ambiguousX = True + # Y values + if (isinstance(line[1][1], complex)): + line[1][1] = firsty + ambiguousY = True + if (isinstance(line[2][1], complex)): + line[2][1] = firsty + ambiguousY = True + # Z values + if (isinstance(line[1][2], complex)): + line[1][2] = firstz + ambiguousZ = True + if (isinstance(line[2][2], complex)): + line[2][2] = firstz + ambiguousZ = True + cnt=cnt+1 + #if (ambiguousX or ambiguousY or ambiguousZ): + # MSG = "Ambiguous G-Code start location:\n" + # if (ambiguousX): MSG = MSG + "X position is not set by a G0(rapid) move prior to a G1,G2 or G3 move.\n" + # if (ambiguousY): MSG = MSG + "Y position is not set by a G0(rapid) move prior to a G1,G2 or G3 move.\n" + # if (ambiguousZ): MSG = MSG + "Z position is not set by a G0(rapid) move prior to a G1,G2 or G3 move.\n" + # MSG = MSG + "!! Review output files carefully !!" + # READ_MSG.append(MSG) + + return READ_MSG + + def get_center(self,POS,POS_LAST,Rin,mvtype,plane="17"): + if plane == "18": + xind=2 + yind=0 + zind=1 + elif plane == "19": + xind=1 + yind=2 + zind=0 + elif plane == "17": + xind=0 + yind=1 + zind=2 + + CENTER=["","",""] + cord = sqrt( (POS[xind]-POS_LAST[xind])**2 + (POS[yind]-POS_LAST[yind])**2 ) + v1 = cord/2.0 + + #print "rin=%f v1=%f (Rin**2 - v1**2)=%f" %(Rin,v1,(Rin**2 - v1**2)) + v2_sq = Rin**2 - v1**2 + if v2_sq<0.0: + v2_sq = 0.0 + v2 = sqrt( v2_sq ) + + theta = self.Get_Angle2(POS[xind]-POS_LAST[xind],POS[yind]-POS_LAST[yind]) + + if mvtype == 3: + dxc,dyc = self.Transform(-v2,v1,radians(theta-90)) + elif mvtype == 2: + dxc,dyc = self.Transform(v2,v1,radians(theta-90)) + else: + return "Center Error" + + xcenter = POS_LAST[xind] + dxc + ycenter = POS_LAST[yind] + dyc + + CENTER[xind] = xcenter + CENTER[yind] = ycenter + CENTER[zind] = POS_LAST[zind] + + return CENTER + + ####################################### + def split_code(self,code2split,shift=[0,0,0],angle=0.0): + xsplit=0.0 + mvtype = -1 # G0 (Rapid), G1 (linear), G2 (clockwise arc) or G3 (counterclockwise arc). + + passthru = "" + POS =[0,0,0] + feed = 0 + spindle = 0 + self.right_side = [] + self.left_side = [] + + L = 0 + R = 1 + for line in code2split: + if line[0] == 1: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = ['','',''] + feed = line[3] + spindle = line[4] + + elif line[0] == 3 or line[0] == 2: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = line[3][:] + feed = line[4] + spindle = line[5] + + else: + mvtype = -1 + passthru = line + + ############################################################################### + if mvtype >= 1 and mvtype <= 3: + pos = self.coordop(POS,shift,angle) + pos_last = self.coordop(POS_LAST,shift,angle) + + if CENTER[0]!='' and CENTER[1]!='': + center = self.coordop(CENTER,shift,angle) + else: + center = CENTER + + this="" + other="" + + if pos_last[0] > xsplit+self.Zero: + flag_side = R + elif pos_last[0] < xsplit-self.Zero: + flag_side = L + else: + if mvtype == 1: + if pos[0] >= xsplit: + flag_side = R + else: + flag_side = L + + elif mvtype == 2: + + if abs(pos_last[1]-center[1]) < self.Zero: + if center[0] > xsplit: + flag_side = R + else: + flag_side = L + else: + if pos_last[1] >= center[1]: + flag_side = R + else: + flag_side = L + + else: #(mvtype == 3) + if abs(pos_last[1]-center[1]) < self.Zero: + if center[0] > xsplit: + flag_side = R + else: + flag_side = L + else: + if pos_last[1] >= center[1]: + flag_side = L + else: + flag_side = R + + if flag_side == R: + this = 1 + other = 0 + else: + this = 0 + other = 1 + + app=[self.apright, self.apleft] + + ############################# + if mvtype == 0: + pass + + if mvtype == 1: + A = self.coordunop(pos_last[:],shift,angle) + C = self.coordunop(pos[:] ,shift,angle) + cross = self.get_line_intersect(pos_last, pos, xsplit) + + if len(cross) > 0: ### Line crosses boundary ### + B = self.coordunop(cross[0] ,shift,angle) + app[this] ( [mvtype,A,B,feed,spindle] ) + app[other]( [mvtype,B,C,feed,spindle] ) + else: + app[this] ( [mvtype,A,C,feed,spindle] ) + + if mvtype == 2 or mvtype == 3: + A = self.coordunop(pos_last[:],shift,angle) + C = self.coordunop(pos[:] ,shift,angle) + D = self.coordunop(center ,shift,angle) + cross = self.get_arc_intersects(pos_last[:], pos[:], xsplit, center[:], "G%d" %(mvtype)) + + if len(cross) > 0: ### Arc crosses boundary at least once ### + B = self.coordunop(cross[0] ,shift,angle) + #Check length of arc before writing + if sqrt((A[0]-B[0])**2 + (A[1]-B[1])**2) > self.accuracy: + app[this]( [mvtype,A,B,D,feed,spindle]) + + if len(cross) == 1: ### Arc crosses boundary only once ### + #Check length of arc before writing + if sqrt((B[0]-C[0])**2 + (B[1]-C[1])**2) > self.accuracy: + app[other]([ mvtype,B,C,D, feed,spindle] ) + if len(cross) == 2: ### Arc crosses boundary twice ### + E = self.coordunop(cross[1],shift,angle) + #Check length of arc before writing + if sqrt((B[0]-E[0])**2 + (B[1]-E[1])**2) > self.accuracy: + app[other]([ mvtype,B,E,D, feed,spindle] ) + #Check length of arc before writing + if sqrt((E[0]-C[0])**2 + (E[1]-C[1])**2) > self.accuracy: + app[this] ([ mvtype,E,C,D, feed,spindle] ) + else: ### Arc does not cross boundary ### + app[this]([ mvtype,A,C,D, feed,spindle]) + + ############################################################################### + else: + if passthru != '': + self.apboth(passthru) + + ####################################### + def probe_code(self,code2probe,nX,nY,probe_istep,minx,miny,xPartitionLength,yPartitionLength): #,Xoffset,Yoffset): + #def probe_code(self,code2probe,nX,nY,probe_istep,minx,miny,xPartitionLength,yPartitionLength,Xoffset,Yoffset,Zoffset): + #print "nX,nY =",nX,nY + probe_coords = [] + BPN=500 + POINT_LIST = [False for i in range(int((nY)*(nX)))] + + if code2probe == []: + return + + mvtype = -1 # G0 (Rapid), G1 (linear), G2 (clockwise arc) or G3 (counterclockwise arc). + passthru = "" + POS = [0,0,0] + feed = 0 + spindle = 0 + out = [] + + min_length = min(xPartitionLength,yPartitionLength) / probe_istep + if (min_length < self.Zero): + min_length = max(xPartitionLength,yPartitionLength) / probe_istep + if (min_length < self.Zero): + min_length = 1 + + for line in code2probe: + if line[0] == 0 or line[0] == 1: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = ['','',''] + if line[0] == 1: + feed = line[3] + spindle = line[4] + + elif line[0] == 3 or line[0] == 2: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = line[3][:] + feed = line[4] + spindle = line[5] + else: + mvtype = -1 + passthru = line + + ############################################################################### + if mvtype >= 0 and mvtype <=3: + pos = POS[:] + pos_last = POS_LAST[:] + center = CENTER[:] + + ############################# + if mvtype == 0: + out.append( [mvtype,pos_last,pos] ) + + if mvtype == 1: + dx = pos[0]-pos_last[0] + dy = pos[1]-pos_last[1] + dz = pos[2]-pos_last[2] + length = sqrt(dx*dx + dy*dy) + if (length <= min_length): + out.append( [mvtype,pos_last,pos,feed,spindle] ) + else: + Lsteps = max(2,int(ceil(length / min_length))) + xstp0 = float(pos_last[0]) + ystp0 = float(pos_last[1]) + zstp0 = float(pos_last[2]) + for n in range(1,Lsteps+1): + xstp1 = n/float(Lsteps)*dx + pos_last[0] + ystp1 = n/float(Lsteps)*dy + pos_last[1] + zstp1 = n/float(Lsteps)*dz + pos_last[2] + out.append( [mvtype,[xstp0,ystp0,zstp0],[xstp1,ystp1,zstp1],feed,spindle] ) + xstp0 = float(xstp1) + ystp0 = float(ystp1) + zstp0 = float(zstp1) + + if mvtype == 2 or mvtype == 3: + out.append( [ mvtype,pos_last,pos,center, feed,spindle] ) + ############################################################################### + else: + if passthru != '': + out.append(passthru) + + ################################ + ## Loop through output to ## + ## find needed probe points ## + ################################ + for i in range(len(out)): + line = out[i] + if line[0] == 0 or line[0] == 1: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = ['','',''] + if line[0] == 1: + feed = line[3] + spindle = line[4] + + elif line[0] == 3 or line[0] == 2: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = line[3][:] + feed = line[4] + spindle = line[5] + else: + mvtype = -1 + passthru = line + + if mvtype >= 1 and mvtype <=3: + pos = POS[:] + pos_last = POS_LAST[:] + center = CENTER[:] + + + #### ADD ADDITIONAL DATA TO POS_LAST DATA #### + i_x,i_y = self.get_ix_iy((pos_last[0]-minx),(pos_last[1]-miny),xPartitionLength,yPartitionLength) + #i_x = i_x+Xoffset + #i_y = i_y+Yoffset + if i_x < 0: + i_x=0 + if i_y < 0: + i_y=0 + if (i_x+1 >= nX): + i_x = nX-2 + #i_x = i_x-1 #commented 02/22 + #print "adjust i_x POS_LAST" + i_x2 = i_x+1 + if (i_y+1 >= nY): + i_y = nY-2 + #i_y = i_y-1 #commented 02/22 + #print "adjust i_y POS_LAST" + i_y2 = i_y+1 + + p_index_A = int(i_y* nX + i_x ) + p_index_B = int(i_y2*nX + i_x ) + p_index_C = int(i_y *nX + i_x2) + p_index_D = int(i_y2*nX + i_x2) + + Xfraction=((pos_last[0]-minx)-(i_x*xPartitionLength))/xPartitionLength + Yfraction=((pos_last[1]-miny)-(i_y*yPartitionLength))/yPartitionLength + + if Xfraction>1.0: + #print "ERROR POS_LAST: Xfraction = ", Xfraction + Xfraction = 1.0 + if Xfraction <0.0: + #print "ERROR POS_LAST: Xfraction = ", Xfraction + Xfraction = 0.0 + if Yfraction > 1.0: + #print "ERROR POS_LAST: Yfraction = ", Yfraction + Yfraction = 1.0 + if Yfraction<0.0: + #print "ERROR POS_LAST: Yfraction = ", Yfraction + Yfraction = 0.0 + + BPN=500 + out[i][1].append(p_index_A+BPN) + out[i][1].append(p_index_B+BPN) + out[i][1].append(p_index_C+BPN) + out[i][1].append(p_index_D+BPN) + out[i][1].append(Xfraction) + out[i][1].append(Yfraction) + + try: + POINT_LIST[p_index_A ] = True + POINT_LIST[p_index_B ] = True + POINT_LIST[p_index_C ] = True + POINT_LIST[p_index_D ] = True + except: + pass + #### ADD ADDITIONAL DATA TO POS_LAST DATA #### + i_x,i_y = self.get_ix_iy((pos[0]-minx),(pos[1]-miny),xPartitionLength,yPartitionLength) + #i_x = i_x+Xoffset + #i_y = i_y+Yoffset + if i_x < 0: + i_x=0 + if i_y < 0: + i_y=0 + if (i_x+1 >= nX): + i_x = nX-2 + #i_x = i_x-1 #commented 02/22 + #print "adjust i_x POS" + i_x2 = i_x+1 + if (i_y+1 >= nY): + i_y = nY-2 + #i_y = i_y-1#commented 02/22 + #print "adjust i_y POS" + i_y2 = i_y+1 + + p_index_A = int(i_y* nX + i_x ) + p_index_B = int(i_y2*nX + i_x ) + p_index_C = int(i_y *nX + i_x2) + p_index_D = int(i_y2*nX + i_x2) + Xfraction=((pos[0]-minx)-(i_x*xPartitionLength))/xPartitionLength + Yfraction=((pos[1]-miny)-(i_y*yPartitionLength))/yPartitionLength + + if Xfraction>1.0: + Xfraction = 1.0 + #print "ERROR POS: Xfraction = ", Xfraction + if Xfraction <0.0: + Xfraction = 0.0 + #print "ERROR POS: Xfraction = ", Xfraction + if Yfraction > 1.0: + Yfraction = 1.0 + #print "ERROR POS: Yfraction = ", Yfraction + if Yfraction<0.0: + Yfraction = 0.0 + #print "ERROR POS: Yfraction = ", Yfraction + + out[i][2].append(p_index_A+BPN) + out[i][2].append(p_index_B+BPN) + out[i][2].append(p_index_C+BPN) + out[i][2].append(p_index_D+BPN) + out[i][2].append(Xfraction) + out[i][2].append(Yfraction) + try: + POINT_LIST[p_index_A ] = True + POINT_LIST[p_index_B ] = True + POINT_LIST[p_index_C ] = True + POINT_LIST[p_index_D ] = True + except: + pass + self.probe_gcode = out + #for line in out: + # print line + + ################################ + ## Generate Probing Code ## + ## For needed points ## + ################################ + + for i in range(len(POINT_LIST)): + i_x = i % nX + i_y = int(i / nX) + xp = i_x * xPartitionLength + minx + yp = i_y * yPartitionLength + miny + probe_coords.append([POINT_LIST[i],i+BPN,xp,yp]) + + self.probe_coords = probe_coords + return + + def get_ix_iy(self,x,y,xPartitionLength,yPartitionLength): + i_x=int(x/xPartitionLength) + i_y=int(y/yPartitionLength) + return i_x,i_y + + ####################################### + def scale_rotate_code(self,code2scale,scale=[1.0,1.0,1.0,1.0],angle=0.0): + if code2scale == []: + return code2scale,0,0,0,0,0,0 + minx = 99999 + maxx = -99999 + miny = 99999 + maxy = -99999 + minz = 99999 + maxz = -99999 + mvtype = -1 # G0 (Rapid), G1 (linear), G2 (clockwise arc) or G3 (counterclockwise arc). + + passthru = "" + POS =[0,0,0] + feed = 0 + spindle = 0 + out = [] + + L = 0 + R = 1 + flag_side = 1 + + for line in code2scale: + if line[0] == 0 or line[0] == 1: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = ['','',''] + if line[0] == 1: + feed = line[3] * scale[3] + spindle = line[4] + + elif line[0] == 3 or line[0] == 2: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = line[3][:] + feed = line[4] * scale[3] + spindle = line[5] + else: + mvtype = -1 + passthru = line + + ############################################################################### + if mvtype >= 0 and mvtype <=3: + + pos = self.scale_rot_coords(POS,scale,angle) + pos_last = self.scale_rot_coords(POS_LAST,scale,angle) + + + if CENTER[0]!='' and CENTER[1]!='': + center = self.scale_rot_coords(CENTER,scale,angle) + else: + center = CENTER + + ############################# + try: + minx = min( minx, min(pos[0],pos_last[0]) ) + maxx = max( maxx, max(pos[0],pos_last[0]) ) + except: + pass + try: + miny = min( miny, min(pos[1],pos_last[1]) ) + maxy = max( maxy, max(pos[1],pos_last[1]) ) + except: + pass + try: + minz = min( minz, min(pos[2],pos_last[2]) ) + maxz = max( maxz, max(pos[2],pos_last[2]) ) + except: + pass + + if mvtype == 0: + out.append( [mvtype,pos_last,pos] ) + + if mvtype == 1: + out.append( [mvtype,pos_last,pos,feed, spindle] ) + + if mvtype == 2 or mvtype == 3: + out.append( [ mvtype,pos_last,pos,center, feed, spindle] ) + + if mvtype == 3: + ang1 = self.Get_Angle2(pos_last[0]-center[0],pos_last[1]-center[1]) + xtmp,ytmp = self.Transform(pos[0]-center[0],pos[1]-center[1],radians(-ang1)) + ang2 = self.Get_Angle2(xtmp,ytmp) + + else: + ang1 = self.Get_Angle2(pos[0]-center[0],pos[1]-center[1]) + xtmp,ytmp = self.Transform(pos_last[0]-center[0],pos_last[1]-center[1],radians(-ang1)) + ang2 = self.Get_Angle2(xtmp,ytmp) + + if ang2 == 0: + ang2=359.999 + + Radius = sqrt( (pos[0]-center[0])**2 +(pos[1]-center[1])**2 ) + + if ang1 > 270: + da = 270 + elif ang1 > 180: + da = 180 + elif ang1 > 90: + da = 90 + else: + da = 0 + for side in [90,180,270,360]: + spd = side + da + if ang2 > (spd-ang1): + if spd > 360: + spd=spd-360 + if spd==90: + maxy = max( maxy, center[1]+Radius ) + if spd==180: + minx = min( minx, center[0]-Radius) + if spd==270: + miny = min( miny, center[1]-Radius ) + if spd==360: + maxx = max( maxx, center[0]+Radius ) + ############################################################################### + else: + if passthru != '': + out.append(passthru) + + return out,minx,maxx,miny,maxy,minz,maxz + + + ####################################### + def scale_translate(self,code2translate,translate=[0.0,0.0,0.0]): + + if translate[0]==0 and translate[1]==0 and translate[2]==0: + return code2translate + + mvtype = -1 # G0 (Rapid), G1 (linear), G2 (clockwise arc) or G3 (counterclockwise arc). + passthru = "" + POS =[0,0,0] + pos =[0,0,0] + pos_last=[0,0,0] + feed = 0 + spindle = 0 + out = [] + + L = 0 + R = 1 + flag_side = 1 + + for line in code2translate: + if line[0] == 1 or line[0] == 0: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = ['','',''] + if line[0] == 1: + feed = line[3] + spindle = line[4] + + elif line[0] == 3 or line[0] == 2: + mvtype = line[0] + POS_LAST = line[1][:] + POS = line[2][:] + CENTER = line[3][:] + feed = line[4] + spindle = line[5] + else: + mvtype = -1 + passthru = line + + ############################################################################### + if mvtype >= 0 and mvtype <=3: + pos = self.scale_trans_coords(POS,translate) + pos_last = self.scale_trans_coords(POS_LAST,translate) + if CENTER[0]!='' and CENTER[1]!='': + center = self.scale_trans_coords(CENTER,translate) + else: + center = CENTER[:] + + ############################# + if mvtype == 0: + out.append( [mvtype,pos_last,pos] ) + + if mvtype == 1: + out.append( [mvtype,pos_last,pos,feed,spindle] ) + + if mvtype == 2 or mvtype == 3: + out.append( [ mvtype,pos_last,pos,center, feed,spindle] ) + ############################################################################### + else: + if passthru != '': + out.append(passthru) + return out + + def scale_trans_coords(self,coords,trans): + x = coords[0] - trans[0] + y = coords[1] - trans[1] + z = coords[2] - trans[2] + return [x,y,z] + + def scale_rot_coords(self,coords,scale,rot): + x = coords[0] * scale[0] + y = coords[1] * scale[1] + z = coords[2] * scale[2] + + x,y = self.Transform(x,y, radians(rot) ) + return [x,y,z] + + def generategcode(self,side,z_safe=.5, + plunge_feed=10.0, + no_variables=False, + Rstock=0.0, + Wrap="XYZ", + preamble="", + postamble="", + gen_rapids=False, + PLACES_L=4, + PLACES_R=3, + PLACES_F=1, + WriteAll=False, + FSCALE="Scale-Rotary", + Reverse_Rotary = False, + NoComments=False): + + g_code = [] + + sign = 1 + if Reverse_Rotary: + sign = -1 + + self.MODAL_VAL={'X':" ", 'Y':" ", 'Z':" ", 'F':" ", 'A':" ", 'B':" ", 'I':" ", 'J':" "} + LASTX = 0 + LASTY = 0 + LASTZ = z_safe + + g_code.append("( G-Code Modified by G-Code Ripper )") + g_code.append("( by Scorch - 2017 www.scorchworks.com )") + if Wrap == "XYZ": + AXIS=["X" , "Y" , "Z" ] + DECP=[PLACES_L, PLACES_L, PLACES_L] + elif Wrap == "Y2A": + AXIS=["X" , "A" , "Z" ] + DECP=[PLACES_L, PLACES_R, PLACES_L] + WriteAll=False + g_code.append("(G-Code Ripper has mapped the Y-Axis to the A-Axis )") + elif Wrap == "X2B": + AXIS=["B" , "Y" , "Z" ] + DECP=[PLACES_R, PLACES_L, PLACES_L] + WriteAll=False + g_code.append("(G-Code Ripper has mapped the X-Axis to the B-Axis )") + elif Wrap == "Y2B": + AXIS=["X" , "B" , "Z" ] + DECP=[PLACES_L, PLACES_R, PLACES_L] + WriteAll=False + g_code.append("(G-Code Ripper has mapped the Y-Axis to the B-Axis )") + elif Wrap == "X2A": + AXIS=["A" , "Y" , "Z" ] + DECP=[PLACES_R, PLACES_L, PLACES_L] + WriteAll=False + g_code.append("(G-Code Ripper has mapped the X-Axis to the A-Axis )") + + if Wrap != "XYZ": + g_code.append("(A nominal stock radius of %f was used. )" %(Rstock)) + g_code.append("(Z-axis zero position is the surface of the round stock. )") + g_code.append("(---------------------------------------------------------)") + + g_code.append("G90 (set absolute distance mode)") + g_code.append("G90.1 (set absolute distance mode for arc centers)") + g_code.append("G17 (set active plane to XY)") + + if self.units == "in": + g_code.append("G20 (set units to inches)") + else: + g_code.append("G21 (set units to mm)") + + if no_variables==False: + g_code.append("# = % 5.3f " %(z_safe)) + g_code.append("# = % 5.0f " %(plunge_feed)) + + for line in preamble.split('|'): + g_code.append(line) + + g_code.append("(---------------------------------------------------------)") + ################### + ## GCODE WRITING ## + ################### + for line in side: + if line[0] == 1 or line[0] == 2 or line[0] == 3 or (line[0] == 0 and gen_rapids == False): + D0 = line[2][0]-line[1][0] + D1 = line[2][1]-line[1][1] + D2 = line[2][2]-line[1][2] + D012 = sqrt((D0+0j).real**2+(D1+0j).real**2+(D2+0j).real**2) + + coordA=[ line[1][0], line[1][1], line[1][2] ] + coordB=[ line[2][0], line[2][1], line[2][2] ] + if Wrap == "Y2A" or Wrap == "Y2B": + #if line[1][1].imag == 0: + if (not isinstance(line[1][1], complex)): + coordA[1]=sign*degrees(line[1][1]/Rstock) + #if line[2][1].imag == 0: + if (not isinstance(line[2][1], complex)): + coordB[1]=sign*degrees(line[2][1]/Rstock) + elif Wrap == "X2B" or Wrap == "X2A": + #if line[1][0].imag == 0: + if (not isinstance(line[1][0], complex)): + coordA[0]=sign*degrees(line[1][0]/Rstock) + #if line[2][0].imag == 0: + if (not isinstance(line[2][0], complex)): + coordB[0]=sign*degrees(line[2][0]/Rstock) + + dx = coordA[0]-LASTX + dy = coordA[1]-LASTY + dz = coordA[2]-LASTZ + + # Check if next point is coincident with the + # current point withing the set accuracy + if sqrt((dx+0j).real**2 + (dy+0j).real**2 + (dz+0j).real**2) > self.accuracy and gen_rapids == True: + ### Move tool to safe height (z_safe) ### + if no_variables==False: + g_code.append("G0 %c # " %(AXIS[2]) ) + self.MODAL_VAL[AXIS[2]] = z_safe + else: + LINE = "G0" + LINE = self.app_gcode_line(LINE,AXIS[2],z_safe,DECP[2],WriteAll) + if len(LINE) > 2: g_code.append(LINE) + + ### Move tool to coordinates of next cut ### + LINE = "G0" + LINE = self.app_gcode_line(LINE,AXIS[0],coordA[0],DECP[0],WriteAll) + LINE = self.app_gcode_line(LINE,AXIS[1],coordA[1],DECP[1],WriteAll) + if len(LINE) > 2: g_code.append(LINE) + + if float(coordA[2]) < float(z_safe): + if no_variables==False: + LINE = "G1" + LINE = self.app_gcode_line(LINE,AXIS[2],coordA[2],DECP[2],WriteAll) + LINE = LINE + " F #" + self.MODAL_VAL["F"] = plunge_feed + if len(LINE) > 2: g_code.append(LINE) + else: + LINE = "G1" + LINE = self.app_gcode_line(LINE,AXIS[2],coordA[2] ,DECP[2] , WriteAll) + LINE = self.app_gcode_line(LINE,"F" ,plunge_feed,PLACES_F, WriteAll) + if len(LINE) > 2: g_code.append(LINE) + + LASTX = coordB[0] + LASTY = coordB[1] + LASTZ = coordB[2] + + if (line[0] == 1) or (line[0] == 2) or (line[0] == 3) or (line[0] == 0 and (gen_rapids == False)): + try: LAST0 = float(self.MODAL_VAL[AXIS[0]]) + except: LAST0 = coordB[0] + try: LAST1 = float(self.MODAL_VAL[AXIS[1]]) + except: LAST1 = coordB[1] + try: LAST2 = float(self.MODAL_VAL[AXIS[2]]) + except: LAST2 = coordB[2] + + LINE = "G%d" %(line[0]) + LINE = self.app_gcode_line(LINE,AXIS[0],coordB[0],DECP[0],WriteAll) + LINE = self.app_gcode_line(LINE,AXIS[1],coordB[1],DECP[1],WriteAll) + LINE = self.app_gcode_line(LINE,AXIS[2],coordB[2],DECP[2],WriteAll) + if (line[0] == 1): + if ((LINE.find("A") > -1) or (LINE.find("B") > -1)) and (FSCALE == "Scale-Rotary") and (D012>self.Zero): + if (LINE.find("X") > -1) or (LINE.find("Y") > -1) or (LINE.find("Z") > -1): + if Wrap == "Y2A" or Wrap == "Y2B": + Df = sqrt(D0**2 + D2**2) + elif Wrap == "X2B" or Wrap == "X2A": + Df = sqrt(D1**2 + D2**2) + Feed_adj = abs(Df / (D012/line[3])) + else: + if Wrap == "Y2A" or Wrap == "Y2B": + DAf = coordB[1]-LAST1 + Feed_adj = abs(DAf / (D012/line[3])) + elif Wrap == "X2B" or Wrap == "X2A": + DAf = coordB[0]-LAST0 + Feed_adj = abs(DAf / (D012/line[3])) + else: + Feed_adj = line[3] + LINE = self.app_gcode_line(LINE,"F",Feed_adj ,PLACES_F,WriteAll) + + elif (line[0] == 2) or (line[0] == 3): + LINE = self.app_gcode_line(LINE,"I",line[3][0],DECP[0] ,WriteAll) + LINE = self.app_gcode_line(LINE,"J",line[3][1],DECP[1] ,WriteAll) + LINE = self.app_gcode_line(LINE,"F",Feed_adj ,PLACES_F,WriteAll) + if len(LINE) > 2: g_code.append(LINE) + + elif (line[0] == 0 and gen_rapids == True): + pass + + elif line[0] == ";": + if not NoComments: + g_code.append("%s" %(line[1])) + + elif line[0] == "M2": + if gen_rapids == True: + if no_variables==False: + g_code.append("G0 %c # " %(AXIS[2]) ) + self.MODAL_VAL[AXIS[2]] = z_safe + else: + LINE = "G0" + LINE = self.app_gcode_line(LINE,AXIS[2],z_safe,DECP[2],WriteAll) + g_code.append(LINE) + + for entry in postamble.split('|'): + g_code.append(entry) + #g_code.append(line[0]) + else: + g_code.append(line) + ######################## + ## END G-CODE WRITING ## + ######################## + return g_code + + + def app_gcode_line(self,LINE,CODE,VALUE,PLACES,WriteAll): + if isinstance(VALUE, complex): + return LINE + #if VALUE.imag != 0: + # return LINE + + if CODE == "F": + if (VALUE*10**PLACES) < 1: + # Fix Zero feed rate + VALUE = 1.0/(10**PLACES) + + if PLACES > 0: + FORMAT="%% .%df" %(PLACES) + else: + FORMAT="%d" + VAL = FORMAT %(VALUE) + + if ( VAL != self.MODAL_VAL[CODE] )\ + or ( CODE=="I" ) \ + or ( CODE=="J" ) \ + or (WriteAll): + LINE = LINE + " %s%s" %(CODE, VAL) + self.MODAL_VAL[CODE] = VAL + + return LINE + + def get_arc_intersects(self, p1, p2, xsplit, cent, code): + xcross1= xsplit + xcross2= xsplit + + R = sqrt( (cent[0]-p1[0])**2 + (cent[1]-p1[1])**2 ) + Rt= sqrt( (cent[0]-p2[0])**2 + (cent[1]-p2[1])**2 ) + if abs(R-Rt) > self.accuracy: self.fmessage("Radius Warning: R1=%f R2=%f"%(R,Rt)) + + val = R**2 - (xsplit - cent[0])**2 + if val >= 0.0: + root = sqrt( val ) + ycross1 = cent[1] - root + ycross2 = cent[1] + root + else: + return [] + + theta = self.Get_Angle2(p1[0]-cent[0],p1[1]-cent[1]) + + xbeta,ybeta = self.Transform(p2[0]-cent[0],p2[1]-cent[1],radians(-theta)) + beta = self.Get_Angle2(xbeta,ybeta,code) + + if abs(beta) <= self.Zero: beta = 360.0 + + xt,yt = self.Transform(xsplit-cent[0],ycross1-cent[1],radians(-theta)) + gt1 = self.Get_Angle2(xt,yt,code) + + xt,yt = self.Transform(xsplit-cent[0],ycross2-cent[1],radians(-theta)) + gt2 = self.Get_Angle2(xt,yt,code) + + if gt1 < gt2: + gamma1 = gt1 + gamma2 = gt2 + else: + gamma1 = gt2 + gamma2 = gt1 + temp = ycross1 + ycross1 = ycross2 + ycross2 = temp + + dz = p2[2] - p1[2] + da = beta + mz = dz/da + zcross1 = p1[2] + gamma1 * mz + zcross2 = p1[2] + gamma2 * mz + + output=[] + if gamma1 < beta and gamma1 > self.Zero and gamma1 < beta-self.Zero: + output.append([xcross1,ycross1,zcross1]) + if gamma2 < beta and gamma1 > self.Zero and gamma2 < beta-self.Zero: + output.append([xcross2,ycross2,zcross2]) + + #print(" start: x1 =%5.2f y1=%5.2f z1=%5.2f" %(p1[0], p1[1], p1[2])) + #print(" end: x2 =%5.2f y2=%5.2f z2=%5.2f" %(p2[0], p2[1], p2[2])) + #print("center: xc =%5.2f yc=%5.2f xsplit=%5.2f code=%s" %(cent[0],cent[1],xsplit,code)) + #print("R = %f" %(R)) + #print("theta =%5.2f" %(theta)) + #print("beta =%5.2f gamma1=%5.2f gamma2=%5.2f\n" %(beta,gamma1,gamma2)) + #cnt=0 + #for line in output: + # cnt=cnt+1 + # print("arc cross%d: %5.2f, %5.2f, %5.2f" %(cnt, line[0], line[1], line[2])) + #print(output) + #print("----------------------------------------------\n") + + return output + + def arc2lines(self, p1, p2, cent, code, plane="17"): + if plane == "18": + xind=2 + yind=0 + zind=1 + elif plane == "19": + xind=1 + yind=2 + zind=0 + elif plane == "17": + xind=0 + yind=1 + zind=2 + + R = sqrt( (cent[xind]-p1[xind])**2 + (cent[yind]-p1[yind])**2 ) + Rt= sqrt( (cent[xind]-p2[xind])**2 + (cent[yind]-p2[yind])**2 ) + if abs(R-Rt) > self.accuracy: self.fmessage("Radius Warning: R1=%f R2=%f "%(R,Rt)) + + if code == 3: + theta = self.Get_Angle2(p1[xind]-cent[xind],p1[yind]-cent[yind]) + xbeta,ybeta = self.Transform(p2[xind]-cent[xind],p2[yind]-cent[yind],radians(-theta)) + X1 = p1[xind] + Y1 = p1[yind] + Z1 = p1[zind] + zstart = Z1 + zend = p2[zind] + if code == 2: + theta = self.Get_Angle2(p2[xind]-cent[xind],p2[yind]-cent[yind]) + xbeta,ybeta = self.Transform(p1[xind]-cent[xind],p1[yind]-cent[yind],radians(-theta)) + X1 = p2[xind] + Y1 = p2[yind] + Z1 = p2[zind] + zstart = Z1 + zend = p1[zind] + + beta = self.Get_Angle2(xbeta,ybeta) #,code) + + if abs(beta) <= self.Zero: beta = 360.0 + ########################################## + arc_step=self.arc_angle + + my_range=[] + + at=arc_step + while at < beta: + my_range.append(at) + at = at+arc_step + my_range.append(beta) + + + + new_lines=[] + for at in my_range: + xt,yt = self.Transform(R,0,radians(theta+at)) + + X2 = cent[xind] + xt + Y2 = cent[yind] + yt + #Z2 = p1[zind] + at*(p2[zind]-p1[zind])/beta + Z2 = zstart + at*(zend-zstart)/beta + data = ["","","","","",""] + + + if code == 3: + data[xind]=X1 + data[yind]=Y1 + data[zind]=Z1 + data[3+xind]=X2 + data[3+yind]=Y2 + data[3+zind]=Z2 + new_lines.append(data) + else: + data[xind]=X2 + data[yind]=Y2 + data[zind]=Z2 + data[3+xind]=X1 + data[3+yind]=Y1 + data[3+zind]=Z1 + new_lines.insert(0, data) + + X1=X2 + Y1=Y2 + Z1=Z2 + at = at+arc_step + + return new_lines + + def get_line_intersect(self,p1, p2, xsplit): + dx = p2[0] - p1[0] + dy = p2[1] - p1[1] + dz = p2[2] - p1[2] + + xcross = xsplit + try: + my = dy/dx + by = p1[1] - my * p1[0] + ycross = my*xsplit + by + except: + ycross = p1[1] + try: + mz = dz/dx + bz = p1[2] - mz * p1[0] + zcross = mz*xsplit + bz + except: + zcross = p1[2] + + output=[] + if xcross > min(p1[0],p2[0])+self.Zero and xcross < max(p1[0],p2[0])-self.Zero: + output.append([xcross,ycross,zcross]) + return output + + + def apleft(self, gtext): + self.left_side.append(gtext) + + + def apright(self, gtext): + self.right_side.append(gtext) + + + def apboth(self, gtext): + self.left_side.append(gtext) + self.right_side.append(gtext) + + + def rm_text(self,line,s,e): + if e == -1: + e = len(line) + temp1 = line[0:s] + temp2 = line[e+1:len(line)] + return temp1+temp2 + + + def insert_text(self,line,itext,s): + temp1 = line[0:s] + temp2 = line[s:len(line)] + return temp1+itext+temp2 + + + def coordop(self,coords,offset,rot): + x = coords[0] + y = coords[1] + z = coords[2] + x = x - offset[0] + y = y - offset[1] + z = z - offset[2] + x,y = self.Transform(x,y, radians(rot) ) + return [x,y,z] + + + def coordunop(self,coords,offset,rot): + x = coords[0] + y = coords[1] + z = coords[2] + x,y = self.Transform(x,y, radians(-rot) ) + x = x + offset[0] + y = y + offset[1] + z = z + offset[2] + return [x,y,z] + + ####################################### ####################################### + ####################################### ####################################### + ####################################### ####################################### + ####################################### ####################################### + + def FUNCTION_EVAL(self,list): + #list consists of [name,val1,val2] + name = list[0] + val1 = float(list[1]) + fval = float(val1) + ############################################# + ########### G-CODE FUNCTIONS ################ + ############################################# + # ATAN[Y]/[X] Four quadrant inverse tangent # + # ABS[arg] Absolute value # + # ACOS[arg] Inverse cosine # + # ASIN[arg] Inverse sine # + # COS[arg] Cosine # + # EXP[arg] e raised to the given power # + # FIX[arg] Round down to integer # + # FUP[arg] Round up to integer # + # ROUND[arg] Round to nearest integer # + # LN[arg] Base-e logarithm # + # SIN[arg] Sine # + # SQRT[arg] Square Root # + # TAN[arg] Tangent # + # EXISTS[arg] Check named Parameter # + ############################################# + if name == "ATAN": + fval2 = float(list[2]) + atan2(fval1,fval2) + if name == "ABS": + return abs(fval) + if name == "ACOS": + return degrees(acos(fval)) + if name == "ASIN": + return degrees(asin(fval)) + if name == "COS": + return cos(radians(fval)) + if name == "EXP": + return exp(fval) + if name == "FIX": + return floor(fval) + if name == "FUP": + return ceil(fval) + if name == "ROUND": + return round(fval) + if name == "LN": + return log(fval) + if name == "SIN": + return sin(radians(fval)) + if name == "SQRT": + return sqrt(fval) + if name == "TAN": + return tan(radians(fval)) + if name == "EXISTS": + pass + + def EXPRESSION_EVAL(self,line): + ################################################### + ### EVALUATE MATH IN REGION ### + ################################################### + line_in = line + P = 0 + if P==1: self.fmessage("line=%s" %(line)) + + if len(line)<2: + MSG = "ERROR EXP-1: Unable to evaluate expression: %s\n" %(line_in) + raise ValueError(MSG) + + + line = line.replace(" ","") + + ################################################# + ### G-CODE OPPERATORS ### + ### In Precedence Order ### + ################################################# + ## ** # + ## * / MOD # + ## + - # + ## EQ NE GT GE LT LE # + ## AND OR XOR # + ################################################# + + ################################################# + ### Replace multiple +/- with single operator ### + ################################################# + cnt = 1 + while cnt > 0: + if (not self.cmp_new(line[cnt],'+')) or (not self.cmp_new(line[cnt],'-')): + sign = 1 + span = 0 + FLAG = True + while FLAG: + if not self.cmp_new(line[cnt+span],'+'): + span = span + 1 + elif not self.cmp_new(line[cnt+span],'-'): + sign = -sign + span = span + 1 + else: + FLAG = False + tmp1=line[:(cnt)] + tmp2=line[(cnt+span):] + if sign > 0: + line = tmp1+'+'+tmp2 + else: + line = tmp1+'-'+tmp2 + cnt=cnt + 1 + if cnt >= len(line): + cnt = -1 + + ######################################### + ### Replace multi-character operators ### + ### with single character identifiers ### + ######################################### + line = line.replace("XOR","|") + line = line.replace("AND","&") + line = line.replace("LE","l") + line = line.replace("LT","<") + line = line.replace("GE","g") + line = line.replace("GT",">") + line = line.replace("NE","!") + line = line.replace("EQ","=") + line = line.replace("**","^") + + ######################################### + ### Split the text into a list ### + ######################################### + line = re.split( "([\[,\],\^,\*,\/,\%,\+,\-,\| ,\& ,\l ,\< ,\g ,\> ,\! ,\= ])", line) + + ######################################### + ### Remove empty items from the list ### + ######################################### + for i in range(line.count('')): line.remove('') + + ######################################### + ### Find the last "[" in the list ### + ######################################### + s=-1 + for cnt in range(s+1,len(line)): + if line[cnt] == '[': + s = cnt + if s == -1: + MSG = "ERROR EXP-2: Unable to evaluate expression: %s" %(line_in) + #self.fmessage(MSG) + raise ValueError(MSG) + + ################################################################# + ### While there are still brackets "[...]" keep processing ### + ################################################################# + while s != -1: + ############################################################## + ### Find the first occurance of "]" after the current "[" ### + ############################################################## + e=-1 + for cnt in range(len(line)-1,s,-1): + if line[cnt] == ']': + e = cnt + + ############################################# + ### Find the items between the brackets ### + ############################################# + temp = line[s+1:e] + + ############################################################## + ### Fix Some Special Cases ### + ############################################################## + ### **- *- MOD- ### + ############################################################## + for cnt in range(0,len(temp)): + if (not self.cmp_new(temp[cnt],'^')) or \ + (not self.cmp_new(temp[cnt],'*')) or \ + (not self.cmp_new(temp[cnt],'%')): + if not self.cmp_new(temp[cnt+1],'-'): + temp[cnt+1]='' + temp[cnt+2]= -float(temp[cnt+2]) + elif not self.cmp_new(temp[cnt+1],'+'): + temp[cnt+1]='' + temp[cnt+2]= float(temp[cnt+2]) + for i in range(temp.count('')): temp.remove('') + + ##################################### + XOR_operation = self.list_split(temp,"|") #XOR + for iXOR in range(0,len(XOR_operation)): + ##################################### + AND_operation = self.list_split(XOR_operation[iXOR],"&") #AND + for iAND in range(0,len(AND_operation)): + ##################################### + LE_operation = self.list_split(AND_operation[iAND],"l") #LE + for iLE in range(0,len(LE_operation)): + ##################################### + LT_operation = self.list_split(LE_operation[iLE],"<") #LT + for iLT in range(0,len(LT_operation)): + ##################################### + GE_operation = self.list_split(LT_operation[iLT],"g") #GE + for iGE in range(0,len(GE_operation)): + ##################################### + GT_operation = self.list_split(GE_operation[iGE],">") #GT + for iGT in range(0,len(GT_operation)): + ##################################### + NE_operation = self.list_split(GT_operation[iGT],"!") #NE + for iNE in range(0,len(NE_operation)): + ##################################### + EQ_operation = self.list_split(NE_operation[iNE],"=") #EQ + for iEQ in range(0,len(EQ_operation)): + ##################################### + add = self.list_split(EQ_operation[iEQ],"+") + for cnt in range(1,len(add)): + if add[cnt-1]==[]: + add[cnt-1] = '' + for i in range(add.count('')): add.remove('') + for iadd in range(0,len(add)): + ##################################### + subtract = self.list_split(add[iadd],"-") + for cnt in range(1,len(subtract)): + if subtract[cnt-1]==[]: + subtract[cnt-1] = '' + subtract[cnt][0] = -float(subtract[cnt][0]) + for i in range(subtract.count('')): subtract.remove('') + for isub in range(0,len(subtract)): + ##################################### + multiply = self.list_split(subtract[isub],"*") + for imult in range(0,len(multiply)): + ##################################### + divide = self.list_split(multiply[imult],"/") + for idiv in range(0,len(divide)): + ##################################### + mod = self.list_split(divide[idiv],"%") + for imod in range(0,len(mod)): + ##################################### + power = self.list_split(mod[imod],"^") + for ipow in range(0,len(power)): + if power[ipow]==[]: + MSG = "ERROR EXP-3: Unable to evaluate expression: %s" %(line_in) + raise ValueError(MSG) + + if type(power[0]) is list: + power_len = len(power[0]) + else: + power_len = 1 + if power_len > 1: + power[ipow] = self.FUNCTION_EVAL(power[0]) + else: + power[ipow] = float(power[ipow][0]) + ##################################### + res_power=power[0] + for k in range(1,len(power)): + res_power = res_power**power[k] + if P==True: self.fmessage(" POWER"+str(power)+"="+str(res_power)) + mod[imod]=res_power + ##################################### + #REVERSE MOD + res_mod=mod[len(mod)-1] + for k in range(len(mod),1,-1): + res_mod = mod[k-2]%res_mod + self.fmessage(" MOD"+str(mod)+"="+str(res_mod)) + divide[idiv]=res_mod + ##################################### + res_divide=divide[0] + for k in range(1,len(divide),1): + res_divide = res_divide/divide[k] + if P==True: self.fmessage(" DIVIDE"+str(divide)+"="+str(res_divide)) + multiply[imult]=res_divide + ##################################### + res_multiply=multiply[0] + for k in range(1,len(multiply)): + res_multiply = res_multiply*multiply[k] + if P==True: self.fmessage("MULTIPLY"+str(multiply)+"="+str(res_multiply)) + subtract[isub]=res_multiply + ##################################### + res_subtract=subtract[0] + for k in range(1,len(subtract)): + res_subtract = res_subtract-subtract[k] + if P==True: self.fmessage("SUBTRACT"+str(subtract)+"="+str(res_subtract)) + add[iadd]=res_subtract + ##################################### + res_add=add[len(add)-1] + for k in range(len(add),1,-1): + res_add = add[k-2]+res_add + if P==True: self.fmessage(" ADD"+str(add)+"="+str(res_add)) + EQ_operation[iEQ]=res_add + ##################### + res_EQ=EQ_operation[0] + for k in range(1,len(EQ_operation),1): + if res_EQ == EQ_operation[k]: + res_EQ = 1 + else: + res_EQ = 0 + if P==True: self.fmessage(" EQ"+str(EQ_operation)+"="+str(res_EQ)) + NE_operation[iNE]=res_EQ + ##################### + res_NE=NE_operation[0] + for k in range(1,len(NE_operation),1): + if res_NE != NE_operation[k]: + res_NE = 1 + else: + res_NE = 0 + if P==True: self.fmessage(" NE"+str(NE_operation)+"="+str(res_NE)) + GT_operation[iGT]=res_NE + ##################### + res_GT=GT_operation[0] + for k in range(1,len(GT_operation),1): + if res_GT > GT_operation[k]: + res_GT = 1 + else: + res_GT = 0 + if P==True: self.fmessage(" GT"+str(GT_operation),"="+str(res_GT)) + GE_operation[iGE]=res_GT + ##################### + res_GE=GE_operation[0] + for k in range(1,len(GE_operation),1): + if res_GE >= GE_operation[k]: + res_GE = 1 + else: + res_GE = 0 + if P==True: self.fmessage(" GE"+str(GE_operation)+"="+str(res_GE)) + LT_operation[iLT]=res_GE + ##################### + res_LT=LT_operation[0] + for k in range(1,len(LT_operation),1): + if res_LT < LT_operation[k]: + res_LT = 1 + else: + res_LT = 0 + if P==True: self.fmessage(" LT"+str(LT_operation)+"="+str(res_LT)) + LE_operation[iLE]=res_LT + ##################### + res_LE=LE_operation[0] + for k in range(1,len(LE_operation),1): + if res_LE <= LE_operation[k]: + res_LE = 1 + else: + res_LE = 0 + if P==True: self.fmessage(" LE"+str(LE_operation)+"="+str(res_LE)) + AND_operation[iAND]=res_LE + ##################### + res_AND=AND_operation[0] + for k in range(1,len(AND_operation),1): + if res_AND and AND_operation[k]: + res_AND = 1 + else: + res_AND = 0 + if P==True: self.fmessage(" AND"+str(AND_operation)+"="+str(res_AND)) + XOR_operation[iXOR]=res_AND + ##################### + res_XOR=XOR_operation[0] + for k in range(1,len(XOR_operation),1): + if bool(res_XOR) ^ bool(XOR_operation[k]): + res_XOR = 1 + else: + res_XOR = 0 + if P==True: self.fmessage(" XOR"+str(XOR_operation)+"="+str(res_XOR)) + + ##################################### + ### Return NEW VALUE to the list ### + ##################################### + for i in range(e,s-1,-1): line.pop(i) + line.insert(int(s),res_XOR) + + ############################# + # Find Last "[" in the list # + ############################# + s=-1 + for cnt in range(s+1,len(line)): + if line[cnt] == '[': + s = cnt + ################################################################# + ### END of While there are still brackets "[...]" ### + ################################################################# + + if len(line) > 1: + MSG = "ERROR EXP-5: Unable to evaluate expression: %s" %(line_in) + raise ValueError(MSG) + return "%.4f" %(line[0]) + + + def list_split(self,lst,obj): + loc=[] + index = -1 + for cnt in range(0,len(lst)): + if not self.cmp_new(lst[cnt],obj): + loc.append( lst[index+1:cnt] ) + index=cnt + loc.append( lst[index+1:len(lst)]) + return loc + + + #Define self.cmp_new, cmp replacement for Python 3 compatability + def cmp_new(self,A,B): + if A==B: + return False + else: + return True + + ############################################################################ + # routine takes an x and a y coords and does a cordinate transformation # + # to a new coordinate system at angle from the initial coordinate system # + # Returns new x,y tuple # + ############################################################################ + def Transform(self,x,y,angle): + newx = x * cos(angle) - y * sin(angle) + newy = x * sin(angle) + y * cos(angle) + return newx,newy + + ############################################################################ + # routine takes an sin and cos and returnss the angle (between 0 and 360) # + ############################################################################ + def Get_Angle2(self,x,y,code=""): + angle = 90.0-degrees(atan2(x,y)) + if angle < 0: + angle = 360 + angle + if code == "G2": + return (360.0 - angle) + else: + return angle + + + ################################################## + ### Generate paths for Laser cutter ### + ################################################## + def generate_laser_paths(self,side,Rapids=True): + ecoords = [] + ################################# + xlast = -99999 + ylast = -99999 + loop = 1 + for line in side: + #print line + if line[0] == 1: + feed = line[3] + spindle = line[4] + x1=(line[1][0]+0j).real + y1=(line[1][1]+0j).real + z1=(line[1][2]+0j).real + + x2=(line[2][0]+0j).real + y2=(line[2][1]+0j).real + z2=(line[2][2]+0j).real + + if xlast!=x1 or ylast!=y1: + loop = loop+1 + ecoords.append([x1,y1,loop,feed,spindle]) + + if x2!=x1 or y2!=y1: + ecoords.append([x2,y2,loop,feed,spindle]) + + xlast = x2 + ylast = y2 + ###################################### + return ecoords + ################################################## + ### End Generate paths for Laser cutter ### + ################################################## + + + +if __name__ == "__main__": + g_rip = G_Code_Rip() + filename="Z:\\text.ngc" + MSG = g_rip.Read_G_Code(filename, XYarc2line = True, arc_angle=2, units="in", Accuracy="") + print(MSG) + ecoords = g_rip.generate_laser_paths(g_rip.g_code_data) + for line in ecoords: + print(line) diff --git a/gpl-3.0.txt b/gpl-3.0.txt new file mode 100644 index 0000000..818433e --- /dev/null +++ b/gpl-3.0.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/inkex.py b/inkex.py new file mode 100644 index 0000000..27ebabe --- /dev/null +++ b/inkex.py @@ -0,0 +1,403 @@ +""" +inkex.py +A helper module for creating Inkscape extensions + +Copyright (C) 2005,2010 Aaron Spike and contributors + +Contributors: + Aurelio A. Heckert + Bulia Byak + Nicolas Dufour, nicoduf@yahoo.fr + Peter J. R. Moulder + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +import copy +import gettext +import optparse +import os +import random +import re +import sys +from math import * +from lxml import etree + +# a dictionary of all of the xmlns prefixes in a standard inkscape doc +NSS = { +u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', +u'cc' :u'http://creativecommons.org/ns#', +u'ccOLD' :u'http://web.resource.org/cc/', +u'svg' :u'http://www.w3.org/2000/svg', +u'dc' :u'http://purl.org/dc/elements/1.1/', +u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', +u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape', +u'xlink' :u'http://www.w3.org/1999/xlink', +u'xml' :u'http://www.w3.org/XML/1998/namespace' +} + + +def localize(): + domain = 'inkscape' + if sys.platform.startswith('win'): + import locale + current_locale, encoding = locale.getdefaultlocale() + os.environ['LANG'] = current_locale + try: + localdir = os.environ['INKSCAPE_LOCALEDIR'] + trans = gettext.translation(domain, localdir, [current_locale], fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + elif sys.platform.startswith('darwin'): + try: + localdir = os.environ['INKSCAPE_LOCALEDIR'] + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + try: + localdir = os.environ['PACKAGE_LOCALE_DIR'] + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + else: + try: + localdir = os.environ['PACKAGE_LOCALE_DIR'] + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + #sys.stderr.write(str(localdir) + "\n") + trans.install() + + +def debug(what): + sys.stderr.write(str(what) + "\n") + return what + + +def errormsg(msg): + """Intended for end-user-visible error messages. + + (Currently just writes to stderr with an appended newline, but could do + something better in future: e.g. could add markup to distinguish error + messages from status messages or debugging output.) + + Note that this should always be combined with translation: + + import inkex + ... + inkex.errormsg(_("This extension requires two selected paths.")) + """ + #if isinstance(msg, unicode): + # sys.stderr.write(msg.encode("utf-8") + "\n") + #else: + # sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("utf-8")) + print(msg) + +def are_near_relative(a, b, eps): + return (a-b <= a*eps) and (a-b >= -a*eps) + + +def check_inkbool(option, opt, value): + if str(value).capitalize() == 'True': + return True + elif str(value).capitalize() == 'False': + return False + else: + raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value)) + + +def addNS(tag, ns=None): + val = tag + if ns is not None and len(ns) > 0 and ns in NSS and len(tag) > 0 and tag[0] != '{': + val = "{%s}%s" % (NSS[ns], tag) + return val + + +class InkOption(optparse.Option): + TYPES = optparse.Option.TYPES + ("inkbool",) + TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER) + TYPE_CHECKER["inkbool"] = check_inkbool + + +class Effect: + """A class for creating Inkscape SVG Effects""" + + def __init__(self, *args, **kwargs): + self.document = None + self.original_document = None + self.ctx = None + self.selected = {} + self.doc_ids = {} + self.options = None + self.args = None + self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile", + option_class=InkOption) + self.OptionParser.add_option("--id", + action="append", type="string", dest="ids", default=[], + help="id attribute of object to manipulate") + self.OptionParser.add_option("--selected-nodes", + action="append", type="string", dest="selected_nodes", default=[], + help="id:subpath:position of selected nodes, if any") + # TODO write a parser for this + + def effect(self): + """Apply some effects on the document. Extensions subclassing Effect + must override this function and define the transformations + in it.""" + pass + + def getoptions(self,args=sys.argv[1:]): + """Collect command line arguments""" + self.options, self.args = self.OptionParser.parse_args(args) + + def parse(self, filename=None, encoding=None): + """Parse document in specified file or on stdin""" + + # First try to open the file from the function argument + if filename is not None: + try: + stream = open(filename, 'r') + except IOError: + errormsg(_("Unable to open specified file: %s") % filename) + sys.exit() + + # If it wasn't specified, try to open the file specified as + # an object member + elif self.svg_file is not None: + try: + stream = open(self.svg_file, 'r') + except IOError: + errormsg(_("Unable to open object member file: %s") % self.svg_file) + sys.exit() + + # Finally, if the filename was not specified anywhere, use + # standard input stream + else: + stream = sys.stdin + + if encoding == None: + p = etree.XMLParser(huge_tree=True, recover=True) + else: + p = etree.XMLParser(huge_tree=True, recover=True, encoding=encoding) + + self.document = etree.parse(stream, parser=p) + self.original_document = copy.deepcopy(self.document) + stream.close() + + # defines view_center in terms of document units + def getposinlayer(self): + #defaults + self.current_layer = self.document.getroot() + self.view_center = (0.0, 0.0) + + layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS) + if layerattr: + layername = layerattr[0] + layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS) + if layer: + self.current_layer = layer[0] + + xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS) + yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS) + if xattr and yattr: + x = self.unittouu(xattr[0] + 'px') + y = self.unittouu(yattr[0] + 'px') + doc_height = self.unittouu(self.getDocumentHeight()) + if x and y: + self.view_center = (float(x), doc_height - float(y)) + # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape + + def getselected(self): + """Collect selected nodes""" + for i in self.options.ids: + path = '//*[@id="%s"]' % i + for node in self.document.xpath(path, namespaces=NSS): + self.selected[i] = node + + def getElementById(self, id): + path = '//*[@id="%s"]' % id + el_list = self.document.xpath(path, namespaces=NSS) + if el_list: + return el_list[0] + else: + return None + + def getParentNode(self, node): + for parent in self.document.getiterator(): + if node in parent.getchildren(): + return parent + + def getdocids(self): + docIdNodes = self.document.xpath('//@id', namespaces=NSS) + for m in docIdNodes: + self.doc_ids[m] = 1 + + def getNamedView(self): + return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0] + + def createGuide(self, posX, posY, angle): + atts = { + 'position': str(posX)+','+str(posY), + 'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle))) + } + guide = etree.SubElement( + self.getNamedView(), + addNS('guide','sodipodi'), atts) + return guide + + def output(self): + """Serialize document into XML on stdout""" + original = etree.tostring(self.original_document) + result = etree.tostring(self.document) + if original != result: + self.document.write(sys.stdout) + + def affect(self, args=sys.argv[1:], output=True): + """Affect an SVG document with a callback effect""" + self.svg_file = args[-1] + localize() + self.getoptions(args) + self.parse() + self.getposinlayer() + self.getselected() + self.getdocids() + self.effect() + if output: + self.output() + + def uniqueId(self, old_id, make_new_id=True): + new_id = old_id + if make_new_id: + while new_id in self.doc_ids: + new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + self.doc_ids[new_id] = 1 + return new_id + + def xpathSingle(self, path): + try: + retval = self.document.xpath(path, namespaces=NSS)[0] + except: + errormsg(_("No matching node for expression: %s") % path) + retval = None + return retval + + # a dictionary of unit to user unit conversion factors + __uuconv = {'in': 96.0, 'pt': 1.33333333333, 'px': 1.0, 'mm': 3.77952755913, 'cm': 37.7952755913, + 'm': 3779.52755913, 'km': 3779527.55913, 'pc': 16.0, 'yd': 3456.0, 'ft': 1152.0} + + # Fault tolerance for lazily defined SVG + def getDocumentWidth(self): + width = self.document.getroot().get('width') + if width: + return width + else: + viewbox = self.document.getroot().get('viewBox') + if viewbox: + return viewbox.split()[2] + else: + return '0' + + # Fault tolerance for lazily defined SVG + def getDocumentHeight(self): + """Returns a string corresponding to the height of the document, as + defined in the SVG file. If it is not defined, returns the height + as defined by the viewBox attribute. If viewBox is not defined, + returns the string '0'.""" + height = self.document.getroot().get('height') + if height: + return height + else: + viewbox = self.document.getroot().get('viewBox') + if viewbox: + return viewbox.split()[3] + else: + return '0' + + def getDocumentUnit(self): + """Returns the unit used for in the SVG document. + In the case the SVG document lacks an attribute that explicitly + defines what units are used for SVG coordinates, it tries to calculate + the unit from the SVG width and viewBox attributes. + Defaults to 'px' units.""" + svgunit = 'px' # default to pixels + + svgwidth = self.getDocumentWidth() + viewboxstr = self.document.getroot().get('viewBox') + if viewboxstr: + unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + p = param.match(svgwidth) + u = unitmatch.search(svgwidth) + + width = 100 # default + viewboxwidth = 100 # default + svgwidthunit = 'px' # default assume 'px' unit + if p: + width = float(p.string[p.start():p.end()]) + else: + errormsg(_("SVG Width not set correctly! Assuming width = 100")) + if u: + svgwidthunit = u.string[u.start():u.end()] + + viewboxnumbers = [] + for t in viewboxstr.split(): + try: + viewboxnumbers.append(float(t)) + except ValueError: + pass + if len(viewboxnumbers) == 4: # check for correct number of numbers + viewboxwidth = viewboxnumbers[2] + + svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth + + # try to find the svgunitfactor in the list of units known. If we don't find something, ... + eps = 0.01 # allow 1% error in factor + for key in self.__uuconv: + if are_near_relative(self.__uuconv[key], svgunitfactor, eps): + # found match! + svgunit = key + + return svgunit + + def unittouu(self, string): + """Returns userunits given a string representation of units in another system""" + unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + p = param.match(string) + u = unit.search(string) + if p: + retval = float(p.string[p.start():p.end()]) + else: + retval = 0.0 + if u: + try: + return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()]) + except KeyError: + pass + else: # default assume 'px' unit + return retval / self.__uuconv[self.getDocumentUnit()] + + return retval + + def uutounit(self, val, unit): + return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()]) + + def addDocumentUnit(self, value): + """Add document unit when no unit is specified in the string """ + try: + float(value) + return value + self.getDocumentUnit() + except ValueError: + return value diff --git a/interface.html b/interface.html new file mode 100644 index 0000000..b9b71cb --- /dev/null +++ b/interface.html @@ -0,0 +1,77 @@ + + + + K40 Whisperer Web Interface + + + + + + +
+
test
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + \ No newline at end of file diff --git a/interpolate.py b/interpolate.py new file mode 100644 index 0000000..166e14d --- /dev/null +++ b/interpolate.py @@ -0,0 +1 @@ +# # This class was lifted from Stack Overflow # https://stackoverflow.com/questions/7343697/linear-interpolation-python # from bisect import bisect_left class interpolate(object): def __init__(self, x_list, y_list): if any([y - x <= 0 for x, y in zip(x_list, x_list[1:])]): raise ValueError("x_list must be in strictly ascending order!") x_list = self.x_list = list(map(float, x_list)) y_list = self.y_list = list(map(float, y_list)) intervals = list(zip(x_list, x_list[1:], y_list, y_list[1:])) self.slopes = [(y2 - y1)/(x2 - x1) for x1, x2, y1, y2 in intervals] def __getitem__(self, x): if x <= self.x_list[0]: return self.y_list[0] elif x >= self.x_list[-1]: return self.y_list[-1] else: i = bisect_left(self.x_list, x) - 1 return self.y_list[i] + self.slopes[i] * (x - self.x_list[i]) \ No newline at end of file diff --git a/k40_whisperer.py b/k40_whisperer.py new file mode 100644 index 0000000..4cc4b4d --- /dev/null +++ b/k40_whisperer.py @@ -0,0 +1,5990 @@ +#!/usr/bin/python +""" + K40 Whisperer + + Copyright (C) <2017-2023> + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +""" +version = '0.64 TURBO' +title_text = "K40 Whisperer V"+version + +import sys +from math import * +from egv import egv +from nano_library import K40_CLASS +from dxf import DXF_CLASS +from svg_reader import SVG_READER +from svg_reader import SVG_TEXT_EXCEPTION +from svg_reader import SVG_PXPI_EXCEPTION +from g_code_library import G_Code_Rip +from interpolate import interpolate +from ecoords import ECoord +from convex_hull import hull2D +from embedded_images import K40_Whisperer_Images +from http.server import (HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer, SimpleHTTPRequestHandler) +import threading +import json + +import inkex +import simplestyle +import simpletransform +import cubicsuperpath +import cspsubdiv +import traceback +import struct + +DEBUG = False + +if DEBUG: + import inspect + +VERSION = sys.version_info[0] +LOAD_MSG = "" + +if VERSION == 3: + from tkinter import * + from tkinter.filedialog import * + import tkinter.messagebox + MAXINT = sys.maxsize + +else: + from Tkinter import * + from tkFileDialog import * + import tkMessageBox + MAXINT = sys.maxint + +if VERSION < 3 and sys.version_info[1] < 6: + def next(item): + #return item.next() + return item.__next__() + +try: + import psyco + psyco.full() + LOAD_MSG = LOAD_MSG+"\nPsyco Loaded\n" +except: + pass + +import math +from time import time +import os +import re +import binascii +import getopt +import operator +import webbrowser +from PIL import Image +from PIL import ImageOps +from PIL import ImageFilter + +try: + Image.warnings.simplefilter('ignore', Image.DecompressionBombWarning) +except: + pass +try: + from PIL import ImageTk + from PIL import _imaging +except: + pass #Don't worry everything will still work + +PYCLIPPER=True +try: + import pyclipper +except: + print("Unable to load Pyclipper library (Offset trace outline will not work without it)") + PYCLIPPER = False + +try: + os.chdir(os.path.dirname(__file__)) +except: + pass + +QUIET = False + +################################################################################ +class Application(Frame): + def __init__(self, master): + self.trace_window = toplevel_dummy() + Frame.__init__(self, master) + self.w = 780 + self.h = 490 + frame = Frame(master, width= self.w, height=self.h) + self.master = master + self.x = -1 + self.y = -1 + self.createWidgets() + self.micro = False + + + def resetPath(self): + self.RengData = ECoord() + self.VengData = ECoord() + self.VcutData = ECoord() + self.GcodeData = ECoord() + self.SCALE = 1 + self.Design_bounds = (0,0,0,0) + self.UI_image = None + #if self.HomeUR.get(): + self.move_head_window_temporary([0.0,0.0]) + #else: + # self.move_head_window_temporary([0.0,0.0]) + + self.pos_offset=[0.0,0.0] + + def createWidgets(self): + self.initComplete = 0 + self.stop=[True] + + self.k40 = None + self.run_time = 0 + + self.master.bind("", self.Master_Configure) + self.master.bind('', self.bindConfigure) + self.master.bind('', self.KEY_F1) + self.master.bind('', self.KEY_F2) + self.master.bind('', self.KEY_F3) + self.master.bind('', self.KEY_F4) + self.master.bind('', self.KEY_F5) + self.master.bind('', self.KEY_F6) + self.master.bind('', self.Home) + + #self.master.bind('', self.Raster_Eng) + #self.master.bind('', self.Vector_Eng) + #self.master.bind('', self.Vector_Cut) + #self.master.bind('', self.Gcode_Cut) + + self.master.bind('' , self.Move_Left) + self.master.bind('' , self.Move_Right) + self.master.bind('' , self.Move_Up) + self.master.bind('' , self.Move_Down) + + self.master.bind('' , self.Move_UL) + self.master.bind('' , self.Move_UR) + self.master.bind('' , self.Move_LR) + self.master.bind('' , self.Move_LL) + self.master.bind('' , self.Move_CC) + + self.master.bind('' , self.Move_Left) + self.master.bind('' , self.Move_Right) + self.master.bind('' , self.Move_Up) + self.master.bind('' , self.Move_Down) + + self.master.bind('' , self.Move_UL) + self.master.bind('' , self.Move_UR) + self.master.bind('' , self.Move_LR) + self.master.bind('' , self.Move_LL) + self.master.bind('' , self.Move_CC) + + ##### + + self.master.bind('' , self.Move_Arb_Left) + self.master.bind('', self.Move_Arb_Right) + self.master.bind('' , self.Move_Arb_Up) + self.master.bind('' , self.Move_Arb_Down) + + self.master.bind('', self.Move_Arb_Left) + self.master.bind('' , self.Move_Arb_Right) + self.master.bind('' , self.Move_Arb_Up) + self.master.bind('', self.Move_Arb_Down) + + + self.master.bind('' , self.Move_Arb_Left) + self.master.bind('', self.Move_Arb_Right) + self.master.bind('' , self.Move_Arb_Up) + self.master.bind('' , self.Move_Arb_Down) + + self.master.bind('', self.Move_Arb_Left) + self.master.bind('' , self.Move_Arb_Right) + self.master.bind('' , self.Move_Arb_Up) + self.master.bind('', self.Move_Arb_Down) + + ##### + self.master.bind('' , self.Initialize_Laser) + self.master.bind('' , self.Unfreeze_Laser) + self.master.bind('' , self.menu_File_Open_Design) + self.master.bind('' , self.menu_Reload_Design) + self.master.bind('' , self.Home) + self.master.bind('' , self.Unlock) + self.master.bind('' , self.Stop) + self.master.bind('' , self.TRACE_Settings_Window) + + self.master.bind('', self.Toggle_Fullscreen) + + self.include_Reng = BooleanVar() + self.include_Rpth = BooleanVar() + self.include_Veng = BooleanVar() + self.include_Vcut = BooleanVar() + self.include_Gcde = BooleanVar() + self.include_Time = BooleanVar() + + self.advanced = BooleanVar() + + self.halftone = BooleanVar() + self.mirror = BooleanVar() + self.rotate = BooleanVar() + self.negate = BooleanVar() + self.inputCSYS = BooleanVar() + self.HomeUR = BooleanVar() + self.engraveUP = BooleanVar() + self.init_home = BooleanVar() + self.post_home = BooleanVar() + self.post_beep = BooleanVar() + self.post_disp = BooleanVar() + self.post_exec = BooleanVar() + + self.pre_pr_crc = BooleanVar() + self.inside_first = BooleanVar() + self.rotary = BooleanVar() + self.reduced_mem = BooleanVar() + self.wait = BooleanVar() + + + self.ht_size = StringVar() + self.Reng_feed = StringVar() + self.Veng_feed = StringVar() + self.Vcut_feed = StringVar() + + self.Reng_passes = StringVar() + self.Veng_passes = StringVar() + self.Vcut_passes = StringVar() + self.Gcde_passes = StringVar() + + + self.board_name = StringVar() + self.units = StringVar() + self.jog_step = StringVar() + self.rast_step = StringVar() + self.funits = StringVar() + + + self.bezier_M1 = StringVar() + self.bezier_M2 = StringVar() + self.bezier_weight = StringVar() + +## self.unsharp_flag = BooleanVar() +## self.unsharp_r = StringVar() +## self.unsharp_p = StringVar() +## self.unsharp_t = StringVar() +## self.unsharp_flag.set(False) +## self.unsharp_r.set("40") +## self.unsharp_p.set("350") +## self.unsharp_t.set("3") + + self.LaserXsize = StringVar() + self.LaserYsize = StringVar() + + self.LaserXscale = StringVar() + self.LaserYscale = StringVar() + self.LaserRscale = StringVar() + + self.rapid_feed = StringVar() + + self.gotoX = StringVar() + self.gotoY = StringVar() + + self.n_egv_passes = StringVar() + + self.inkscape_path = StringVar() + self.batch_path = StringVar() + self.ink_timeout = StringVar() + + self.t_timeout = StringVar() + self.n_timeouts = StringVar() + + self.Reng_time = StringVar() + self.Veng_time = StringVar() + self.Vcut_time = StringVar() + self.Gcde_time = StringVar() + + self.comb_engrave = BooleanVar() + self.comb_vector = BooleanVar() + self.zoom2image = BooleanVar() + + self.trace_w_laser = BooleanVar() + self.trace_gap = StringVar() + self.trace_speed = StringVar() + + ########################################################################### + # INITILIZE VARIABLES # + # if you want to change a default setting this is the place to do it # + ########################################################################### + self.include_Reng.set(1) + self.include_Rpth.set(0) + self.include_Veng.set(1) + self.include_Vcut.set(1) + self.include_Gcde.set(1) + self.include_Time.set(0) + self.advanced.set(0) + + self.halftone.set(1) + self.mirror.set(0) + self.rotate.set(0) + self.negate.set(0) + self.inputCSYS.set(0) + self.HomeUR.set(0) + self.engraveUP.set(0) + self.init_home.set(1) + self.post_home.set(0) + self.post_beep.set(0) + self.post_disp.set(0) + self.post_exec.set(0) + + self.pre_pr_crc.set(1) + self.inside_first.set(1) + self.rotary.set(0) + self.reduced_mem.set(0) + self.wait.set(1) + + self.ht_size.set(500) + + self.Reng_feed.set("100") + self.Veng_feed.set("20") + self.Vcut_feed.set("10") + self.Reng_passes.set("1") + self.Veng_passes.set("1") + self.Vcut_passes.set("1") + self.Gcde_passes.set("1") + + + self.jog_step.set("1.0") + self.rast_step.set("0.002") + + self.bezier_weight.set("3.5") + self.bezier_M1.set("2.5") + self.bezier_M2.set("0.50") + + self.bezier_weight_default = float(self.bezier_weight.get()) + self.bezier_M1_default = float(self.bezier_M1.get()) + self.bezier_M2_default = float(self.bezier_M2.get()) + + + self.board_name.set("LASER-M2") # Options are + # "LASER-M2", + # "LASER-M1", + # "LASER-M", + # "LASER-B2", + # "LASER-B1", + # "LASER-B", + # "LASER-A" + + + self.units.set("mm") # Options are "in" and "mm" + + self.ink_timeout.set("3") + self.t_timeout.set("200") + self.n_timeouts.set("30") + + self.HOME_DIR = os.path.expanduser("~") + + if not os.path.isdir(self.HOME_DIR): + self.HOME_DIR = "" + + self.DESIGN_FILE = (self.HOME_DIR+"/None") + self.EGV_FILE = None + + self.aspect_ratio = 0 + self.segID = [] + + self.LaserXsize.set("325") + self.LaserYsize.set("220") + + self.LaserXscale.set("1.000") + self.LaserYscale.set("1.000") + self.LaserRscale.set("1.000") + + self.rapid_feed.set("0.0") + + self.gotoX.set("0.0") + self.gotoY.set("0.0") + + self.n_egv_passes.set("1") + + self.comb_engrave.set(0) + self.comb_vector.set(0) + self.zoom2image.set(0) + + + self.trace_w_laser.set(0) + self.trace_gap.set(0) + self.trace_speed.set(50) + + self.laserX = 0.0 + self.laserY = 0.0 + self.PlotScale = 1.0 + self.GUI_Disabled = False + + # PAN and ZOOM STUFF + self.panx = 0 + self.panx = 0 + self.lastx = 0 + self.lasty = 0 + self.move_start_x = 0 + self.move_start_y = 0 + + + self.RengData = ECoord() + self.VengData = ECoord() + self.VcutData = ECoord() + self.GcodeData = ECoord() + self.SCALE = 1 + self.Design_bounds = (0,0,0,0) + self.UI_image = None + self.pos_offset=[0.0,0.0] + self.inkscape_warning = False + + # Derived variables + if self.units.get() == 'in': + self.funits.set('in/min') + self.units_scale = 1.0 + else: + self.units.set('mm') + self.funits.set('mm/s') + self.units_scale = 25.4 + + self.statusMessage = StringVar() + self.statusMessage.set("Welcome to K40 Whisperer") + + + self.Reng_time.set("0") + self.Veng_time.set("0") + self.Vcut_time.set("0") + self.Gcde_time.set("0") + + self.min_vector_speed = 1.1 #in/min + self.min_raster_speed = 12 #in/min + + ########################################################################## + ### END INITILIZING VARIABLES ### + ########################################################################## + + # make a Status Bar + self.statusbar = Label(self.master, textvariable=self.statusMessage, \ + bd=1, relief=SUNKEN , height=1) + self.statusbar.pack(anchor=SW, fill=X, side=BOTTOM) + + + # Canvas + lbframe = Frame( self.master ) + self.PreviewCanvas_frame = lbframe + self.PreviewCanvas = Canvas(lbframe, width=self.w-(220+20), height=self.h-200, background="grey75") + self.PreviewCanvas.pack(side=LEFT, fill=BOTH, expand=1) + self.PreviewCanvas_frame.place(x=230, y=10) + + self.PreviewCanvas.tag_bind('LaserTag',"<1>" , self.mousePanStart) + self.PreviewCanvas.tag_bind('LaserTag',"" , self.mousePan) + self.PreviewCanvas.tag_bind('LaserTag',"", self.mousePanStop) + + self.PreviewCanvas.tag_bind('LaserDot',"<3>" , self.right_mousePanStart) + self.PreviewCanvas.tag_bind('LaserDot',"" , self.right_mousePan) + self.PreviewCanvas.tag_bind('LaserDot',"", self.right_mousePanStop) + + # Left Column # + self.separator1 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + self.separator2 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + self.separator3 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + self.separator4 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + + self.Label_Reng_feed_u = Label(self.master,textvariable=self.funits, anchor=W) + self.Entry_Reng_feed = Entry(self.master,width="15") + self.Entry_Reng_feed.configure(textvariable=self.Reng_feed,justify='center',fg="black") + self.Reng_feed.trace_variable("w", self.Entry_Reng_feed_Callback) + self.NormalColor = self.Entry_Reng_feed.cget('bg') + + self.Label_Veng_feed_u = Label(self.master,textvariable=self.funits, anchor=W) + self.Entry_Veng_feed = Entry(self.master,width="15") + self.Entry_Veng_feed.configure(textvariable=self.Veng_feed,justify='center',fg="blue") + self.Veng_feed.trace_variable("w", self.Entry_Veng_feed_Callback) + self.NormalColor = self.Entry_Veng_feed.cget('bg') + + self.Label_Vcut_feed_u = Label(self.master,textvariable=self.funits, anchor=W) + self.Entry_Vcut_feed = Entry(self.master,width="15") + self.Entry_Vcut_feed.configure(textvariable=self.Vcut_feed,justify='center',fg="red") + self.Vcut_feed.trace_variable("w", self.Entry_Vcut_feed_Callback) + self.NormalColor = self.Entry_Vcut_feed.cget('bg') + + # Buttons + self.Reng_Button = Button(self.master,text="Raster Engrave", command=self.Raster_Eng) + self.Veng_Button = Button(self.master,text="Vector Engrave", command=self.Vector_Eng) + self.Vcut_Button = Button(self.master,text="Vector Cut" , command=self.Vector_Cut) + self.Grun_Button = Button(self.master,text="Run G-Code" , command=self.Gcode_Cut) + + + self.Reng_Veng_Button = Button(self.master,text="Raster and\nVector Engrave", command=self.Raster_Vector_Eng) + self.Veng_Vcut_Button = Button(self.master,text="Vector Engrave\nand Cut", command=self.Vector_Eng_Cut) + self.Reng_Veng_Vcut_Button = Button(self.master,text="Raster Engrave\nVector Engrave\nand\nVector Cut", command=self.Raster_Vector_Cut) + + self.Label_Position_Control = Label(self.master,text="Position Controls:", anchor=W) + + self.Initialize_Button = Button(self.master,text="Initialize Laser Cutter", command=self.Initialize_Laser) + + self.Open_Button = Button(self.master,text="Open\nDesign File", command=self.menu_File_Open_Design) + self.Reload_Button = Button(self.master,text="Reload\nDesign File", command=self.menu_Reload_Design) + + self.Home_Button = Button(self.master,text="Home", command=self.Home) + self.UnLock_Button = Button(self.master,text="Unlock Rail", command=self.Unlock) + self.Stop_Button = Button(self.master,text="Pause/Stop", command=self.Stop) + + try: + self.left_image = PhotoImage(data=K40_Whisperer_Images.left_B64, format='gif') + self.right_image = PhotoImage(data=K40_Whisperer_Images.right_B64, format='gif') + self.up_image = PhotoImage(data=K40_Whisperer_Images.up_B64, format='gif') + self.down_image = PhotoImage(data=K40_Whisperer_Images.down_B64, format='gif') + + self.Right_Button = Button(self.master,image=self.right_image, command=self.Move_Right, bg='#efffba') + self.Right_Button_10 = Button(self.master,image=self.right_image, command=self.Move_Right_10, bg='#ffe9ba') + self.Right_Button_100 = Button(self.master,image=self.right_image, command=self.Move_Right_100, bg='#ffbaba') + self.Left_Button = Button(self.master,image=self.left_image, command=self.Move_Left, bg='#efffba') + self.Left_Button_10 = Button(self.master,image=self.left_image, command=self.Move_Left_10, bg='#ffe9ba') + self.Left_Button_100 = Button(self.master,image=self.left_image, command=self.Move_Left_100, bg='#ffbaba') + self.Up_Button = Button(self.master,image=self.up_image, command=self.Move_Up, bg='#efffba') + self.Up_Button_10 = Button(self.master,image=self.up_image, command=self.Move_Up_10, bg='#ffe9ba') + self.Up_Button_100 = Button(self.master,image=self.up_image, command=self.Move_Up_100, bg='#ffbaba') + self.Down_Button = Button(self.master,image=self.down_image, command=self.Move_Down, bg='#efffba') + self.Down_Button_10 = Button(self.master,image=self.down_image, command=self.Move_Down_10, bg='#ffe9ba') + self.Down_Button_100 = Button(self.master,image=self.down_image, command=self.Move_Down_100, bg='#ffbaba') + + self.UL_image = PhotoImage(data=K40_Whisperer_Images.UL_B64, format='gif') + self.UR_image = PhotoImage(data=K40_Whisperer_Images.UR_B64, format='gif') + self.LR_image = PhotoImage(data=K40_Whisperer_Images.LR_B64, format='gif') + self.LL_image = PhotoImage(data=K40_Whisperer_Images.LL_B64, format='gif') + self.CC_image = PhotoImage(data=K40_Whisperer_Images.CC_B64, format='gif') + + self.UL_Button = Button(self.master,image=self.UL_image, command=self.Move_UL, bg='#bad7ff') + self.UR_Button = Button(self.master,image=self.UR_image, command=self.Move_UR, bg='#bad7ff') + self.LR_Button = Button(self.master,image=self.LR_image, command=self.Move_LR, bg='#bad7ff') + self.LL_Button = Button(self.master,image=self.LL_image, command=self.Move_LL, bg='#bad7ff') + self.CC_Button = Button(self.master,image=self.CC_image, command=self.Move_CC, bg='#bad7ff') + + except: + self.Right_Button = Button(self.master,text=">", command=self.Move_Right) + self.Right_Button_10 = Button(self.master,text=">>", command=self.Move_Right_10) + self.Right_Button_100 = Button(self.master,text=">>>", command=self.Move_Right_100) + self.Left_Button = Button(self.master,text="<", command=self.Move_Left) + self.Left_Button_10 = Button(self.master,text="<<", command=self.Move_Left_10) + self.Left_Button_100 = Button(self.master,text="<<<", command=self.Move_Left_100) + self.Up_Button = Button(self.master,text="^", command=self.Move_Up) + self.Up_Button_10 = Button(self.master,text="^^", command=self.Move_Up_10) + self.Up_Button_100 = Button(self.master,text="^^^", command=self.Move_Up_100) + self.Down_Button = Button(self.master,text="v", command=self.Move_Down) + self.Down_Button_10 = Button(self.master,text="vv", command=self.Move_Down_10) + self.Down_Button_100 = Button(self.master,text="vvv", command=self.Move_Down_100) + + self.UL_Button = Button(self.master,text=" ", command=self.Move_UL) + self.UR_Button = Button(self.master,text=" ", command=self.Move_UR) + self.LR_Button = Button(self.master,text=" ", command=self.Move_LR) + self.LL_Button = Button(self.master,text=" ", command=self.Move_LL) + self.CC_Button = Button(self.master,text=" ", command=self.Move_CC) + + self.Label_Step = Label(self.master,text="Jog Step", anchor=CENTER ) + self.Label_Step_u = Label(self.master,textvariable=self.units, anchor=W) + self.Entry_Step = Entry(self.master,width="15") + self.Entry_Step.configure(textvariable=self.jog_step, justify='center') + self.jog_step.trace_variable("w", self.Entry_Step_Callback) + + ########################################################################### + self.GoTo_Button = Button(self.master,text="Move To", command=self.GoTo) + + self.Entry_GoToX = Entry(self.master,width="15",justify='center') + self.Entry_GoToX.configure(textvariable=self.gotoX) + self.gotoX.trace_variable("w", self.Entry_GoToX_Callback) + self.Entry_GoToY = Entry(self.master,width="15",justify='center') + self.Entry_GoToY.configure(textvariable=self.gotoY) + self.gotoY.trace_variable("w", self.Entry_GoToY_Callback) + + self.Label_GoToX = Label(self.master,text="X", anchor=CENTER ) + self.Label_GoToY = Label(self.master,text="Y", anchor=CENTER ) + ########################################################################### + # End Left Column # + + # Advanced Column # + self.separator_vert = Frame(self.master, height=2, bd=1, relief=SUNKEN) + self.Label_Advanced_column = Label(self.master,text="Advanced Settings",anchor=CENTER) + self.separator_adv = Frame(self.master, height=2, bd=1, relief=SUNKEN) + + self.Label_Halftone_adv = Label(self.master,text="Halftone (Dither)") + self.Checkbutton_Halftone_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Halftone_adv.configure(variable=self.halftone) + self.halftone.trace_variable("w", self.View_Refresh_and_Reset_RasterPath) #self.menu_View_Refresh_Callback + + self.Label_Negate_adv = Label(self.master,text="Invert Raster Color") + self.Checkbutton_Negate_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Negate_adv.configure(variable=self.negate) + self.negate.trace_variable("w", self.View_Refresh_and_Reset_RasterPath) + + self.separator_adv2 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + + self.Label_Mirror_adv = Label(self.master,text="Mirror Design") + self.Checkbutton_Mirror_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Mirror_adv.configure(variable=self.mirror) + self.mirror.trace_variable("w", self.View_Refresh_and_Reset_RasterPath) + + self.Label_Rotate_adv = Label(self.master,text="Rotate Design") + self.Checkbutton_Rotate_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Rotate_adv.configure(variable=self.rotate) + self.rotate.trace_variable("w", self.View_Refresh_and_Reset_RasterPath) + + self.separator_adv3 = Frame(self.master, height=2, bd=1, relief=SUNKEN) + + self.Label_inputCSYS_adv = Label(self.master,text="Use Input CSYS") + self.Checkbutton_inputCSYS_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_inputCSYS_adv.configure(variable=self.inputCSYS) + self.inputCSYS.trace_variable("w", self.menu_View_inputCSYS_Refresh_Callback) + + self.Label_Inside_First_adv = Label(self.master,text="Cut Inside First") + self.Checkbutton_Inside_First_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Inside_First_adv.configure(variable=self.inside_first) + self.inside_first.trace_variable("w", self.menu_Inside_First_Callback) + + self.Label_Inside_First_adv = Label(self.master,text="Cut Inside First") + self.Checkbutton_Inside_First_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Inside_First_adv.configure(variable=self.inside_first) + + self.Label_Rotary_Enable_adv = Label(self.master,text="Use Rotary Settings") + self.Checkbutton_Rotary_Enable_adv = Checkbutton(self.master,text="") + self.Checkbutton_Rotary_Enable_adv.configure(variable=self.rotary) + self.rotary.trace_variable("w", self.Reset_RasterPath_and_Update_Time) + + + ##### + self.separator_comb = Frame(self.master, height=2, bd=1, relief=SUNKEN) + + self.Label_Comb_Engrave_adv = Label(self.master,text="Group Engrave Tasks") + self.Checkbutton_Comb_Engrave_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Comb_Engrave_adv.configure(variable=self.comb_engrave) + self.comb_engrave.trace_variable("w", self.menu_View_Refresh_Callback) + + self.Label_Comb_Vector_adv = Label(self.master,text="Group Vector Tasks") + self.Checkbutton_Comb_Vector_adv = Checkbutton(self.master,text=" ", anchor=W) + self.Checkbutton_Comb_Vector_adv.configure(variable=self.comb_vector) + self.comb_vector.trace_variable("w", self.menu_View_Refresh_Callback) + ##### + + self.Label_Reng_passes = Label(self.master,text="Raster Eng. Passes") + self.Entry_Reng_passes = Entry(self.master,width="15") + self.Entry_Reng_passes.configure(textvariable=self.Reng_passes,justify='center',fg="black") + self.Reng_passes.trace_variable("w", self.Entry_Reng_passes_Callback) + self.NormalColor = self.Entry_Reng_passes.cget('bg') + + self.Label_Veng_passes = Label(self.master,text="Vector Eng. Passes") + self.Entry_Veng_passes = Entry(self.master,width="15") + self.Entry_Veng_passes.configure(textvariable=self.Veng_passes,justify='center',fg="blue") + self.Veng_passes.trace_variable("w", self.Entry_Veng_passes_Callback) + self.NormalColor = self.Entry_Veng_passes.cget('bg') + + self.Label_Vcut_passes = Label(self.master,text="Vector Cut Passes") + self.Entry_Vcut_passes = Entry(self.master,width="15") + self.Entry_Vcut_passes.configure(textvariable=self.Vcut_passes,justify='center',fg="red") + self.Vcut_passes.trace_variable("w", self.Entry_Vcut_passes_Callback) + self.NormalColor = self.Entry_Vcut_passes.cget('bg') + + self.Label_Gcde_passes = Label(self.master,text="G-Code Passes") + self.Entry_Gcde_passes = Entry(self.master,width="15") + self.Entry_Gcde_passes.configure(textvariable=self.Gcde_passes,justify='center',fg="black") + self.Gcde_passes.trace_variable("w", self.Entry_Gcde_passes_Callback) + self.NormalColor = self.Entry_Gcde_passes.cget('bg') + + + self.Hide_Adv_Button = Button(self.master,text="Hide Advanced", command=self.Hide_Advanced) + + # End Right Column # + self.calc_button = Button(self.master,text="Calculate Raster Time", command=self.menu_Calc_Raster_Time) + + #GEN Setting Window Entry initializations + self.Entry_Sspeed = Entry() + self.Entry_BoxGap = Entry() + self.Entry_ContAngle = Entry() + + # Make Menu Bar + self.menuBar = Menu(self.master, relief = "raised", bd=2) + + + + + top_File = Menu(self.menuBar, tearoff=0) + top_File.add("command", label = "Save Settings File", command = self.menu_File_Save) + top_File.add("command", label = "Read Settings File", command = self.menu_File_Open_Settings_File) + + top_File.add_separator() + top_File.add("command", label = "Open Design (SVG/DXF/G-Code)" , command = self.menu_File_Open_Design) + top_File.add("command", label = "Reload Design" , command = self.menu_Reload_Design) + + top_File.add_separator() + top_File.add("command", label = "Send EGV File to Laser" , command = self.menu_File_Open_EGV) + + SaveEGVmenu = Menu(self.master, relief = "raised", bd=2, tearoff=0) + top_File.add_cascade(label="Save EGV File", menu=SaveEGVmenu) + SaveEGVmenu.add("command", label = "Raster Engrave" , command = self.menu_File_Raster_Engrave) + SaveEGVmenu.add("command", label = "Vector Engrave" , command = self.menu_File_Vector_Engrave) + SaveEGVmenu.add("command", label = "Vector Cut" , command = self.menu_File_Vector_Cut) + SaveEGVmenu.add("command", label = "G-Code Operations" , command = self.menu_File_G_Code) + SaveEGVmenu.add_separator() + SaveEGVmenu.add("command", label = "Raster and Vector Engrave" , command = self.menu_File_Raster_Vector_Engrave) + SaveEGVmenu.add("command", label = "Vector Engrave and Cut" , command = self.menu_File_Vector_Engrave_Cut) + SaveEGVmenu.add("command", label = "Raster, Vector Engrave and Vector Cut" , command = self.menu_File_Raster_Vector_Cut) + + + top_File.add_separator() + top_File.add("command", label = "Exit" , command = self.menu_File_Quit) + + self.menuBar.add("cascade", label="File", menu=top_File) + + #top_Edit = Menu(self.menuBar, tearoff=0) + #self.menuBar.add("cascade", label="Edit", menu=top_Edit) + + top_View = Menu(self.menuBar, tearoff=0) + top_View.add("command", label = "Refresh ", command = self.menu_View_Refresh) + top_View.add_separator() + top_View.add_checkbutton(label = "Show Raster Image" , variable=self.include_Reng ,command= self.menu_View_Refresh) + if DEBUG: + top_View.add_checkbutton(label = "Show Raster Paths" ,variable=self.include_Rpth ,command= self.menu_View_Refresh) + + top_View.add_checkbutton(label = "Show Vector Engrave", variable=self.include_Veng ,command= self.menu_View_Refresh) + top_View.add_checkbutton(label = "Show Vector Cut" , variable=self.include_Vcut ,command= self.menu_View_Refresh) + top_View.add_checkbutton(label = "Show G-Code Paths" , variable=self.include_Gcde ,command= self.menu_View_Refresh) + top_View.add_separator() + top_View.add_checkbutton(label = "Show Time Estimates", variable=self.include_Time ,command= self.menu_View_Refresh) + top_View.add_checkbutton(label = "Zoom to Design Size", variable=self.zoom2image ,command= self.menu_View_Refresh) + + #top_View.add_separator() + #top_View.add("command", label = "computeAccurateReng",command= self.computeAccurateReng) + #top_View.add("command", label = "computeAccurateVeng",command= self.computeAccurateVeng) + #top_View.add("command", label = "computeAccurateVcut",command= self.computeAccurateVcut) + + self.menuBar.add("cascade", label="View", menu=top_View) + + top_Tools = Menu(self.menuBar, tearoff=0) + self.menuBar.add("cascade", label="Tools", menu=top_Tools) + USBmenu = Menu(self.master, relief = "raised", bd=2, tearoff=0) + + top_Tools.add("command", label = "Calculate Raster Time", command = self.menu_Calc_Raster_Time) + top_Tools.add("command", label = "Trace Design Boundary ", command = self.TRACE_Settings_Window) + top_Tools.add_separator() + top_Tools.add("command", label = "Initialize Laser ", command = self.Initialize_Laser) + top_Tools.add("command", label = "Unfreeze Laser " , command = self.Unfreeze_Laser) + top_Tools.add_cascade(label="USB", menu=USBmenu) + USBmenu.add("command", label = "Reset USB", command = self.Reset) + USBmenu.add("command", label = "Release USB", command = self.Release_USB) + + + + #top_USB = Menu(self.menuBar, tearoff=0) + #top_USB.add("command", label = "Reset USB", command = self.Reset) + #top_USB.add("command", label = "Release USB", command = self.Release_USB) + #top_USB.add("command", label = "Initialize Laser", command = self.Initialize_Laser) + #self.menuBar.add("cascade", label="USB", menu=top_USB) + + + top_Settings = Menu(self.menuBar, tearoff=0) + top_Settings.add("command", label = "General Settings ", command = self.GEN_Settings_Window) + top_Settings.add("command", label = "Raster Settings ", command = self.RASTER_Settings_Window) + top_Settings.add("command", label = "Rotary Settings ", command = self.ROTARY_Settings_Window) + top_Settings.add_separator() + top_Settings.add_checkbutton(label = "Advanced Settings ", variable=self.advanced ,command= self.menu_View_Refresh) + + self.menuBar.add("cascade", label="Settings", menu=top_Settings) + + top_Help = Menu(self.menuBar, tearoff=0) + top_Help.add("command", label = "About (e-mail)", command = self.menu_Help_About) + top_Help.add("command", label = "K40 Whisperer Web Page", command = self.menu_Help_Web) + top_Help.add("command", label = "Manual (Web Page)", command = self.menu_Help_Manual) + self.menuBar.add("cascade", label="Help", menu=top_Help) + + self.master.config(menu=self.menuBar) + + ########################################################################## + # Config File and command line options # + ########################################################################## + config_file = "k40_whisperer.txt" + home_config1 = self.HOME_DIR + "/" + config_file + if ( os.path.isfile(config_file) ): + self.Open_Settings_File(config_file) + elif ( os.path.isfile(home_config1) ): + self.Open_Settings_File(home_config1) + + +# opts, args = None, None +# try: +# opts, args = getopt.getopt(sys.argv[1:], "ho:",["help", "other_option"]) +# except: +# debug_message('Unable interpret command line options') +# sys.exit() +# for option, value in opts: +## if option in ('-h','--help'): +## fmessage(' ') +## fmessage('Usage: python .py [-g file]') +## fmessage('-o : unknown other option (also --other_option)') +## fmessage('-h : print this help (also --help)\n') +## sys.exit() +# if option in ('-m','--micro'): +# self.micro = True + + ########################################################################## + +################################################################################ + def entry_set(self, val2, calc_flag=0, new=0): + if calc_flag == 0 and new==0: + try: + self.statusbar.configure( bg = 'yellow' ) + val2.configure( bg = 'yellow' ) + self.statusMessage.set(" Recalculation required.") + except: + pass + elif calc_flag == 3: + try: + val2.configure( bg = 'red' ) + self.statusbar.configure( bg = 'red' ) + self.statusMessage.set(" Value should be a number. ") + except: + pass + elif calc_flag == 2: + try: + self.statusbar.configure( bg = 'red' ) + val2.configure( bg = 'red' ) + except: + pass + elif (calc_flag == 0 or calc_flag == 1) and new==1 : + try: + self.statusbar.configure( bg = 'white' ) + self.statusMessage.set(" ") + val2.configure( bg = 'white' ) + except: + pass + elif (calc_flag == 1) and new==0 : + try: + self.statusbar.configure( bg = 'white' ) + self.statusMessage.set(" ") + val2.configure( bg = 'white' ) + except: + pass + + elif (calc_flag == 0 or calc_flag == 1) and new==2: + return 0 + return 1 + +################################################################################ + def Write_Config_File(self, event): + + config_data = self.WriteConfig() + config_file = "k40_whisperer.txt" + configname_full = self.HOME_DIR + "/" + config_file + + current_name = event.widget.winfo_parent() + win_id = event.widget.nametowidget(current_name) + + if ( os.path.isfile(configname_full) ): + try: + win_id.withdraw() + except: + pass + + if not message_ask_ok_cancel("Replace", "Replace Exiting Configuration File?\n"+configname_full): + try: + win_id.deiconify() + except: + pass + return + try: + fout = open(configname_full,'w') + except: + self.statusMessage.set("Unable to open file for writing: %s" %(configname_full)) + self.statusbar.configure( bg = 'red' ) + return + for line in config_data: + try: + fout.write(line+'\n') + except: + fout.write('(skipping line)\n') + fout.close + self.statusMessage.set("Configuration File Saved: %s" %(configname_full)) + self.statusbar.configure( bg = 'white' ) + try: + win_id.deiconify() + except: + pass + + ################################################################################ + def WriteConfig(self): + global Zero + header = [] + header.append('( K40 Whisperer Settings: '+version+' )') + header.append('( by Scorch - 2019 )') + header.append("(=========================================================)") + # BOOL + header.append('(k40_whisperer_set include_Reng %s )' %( int(self.include_Reng.get()) )) + header.append('(k40_whisperer_set include_Veng %s )' %( int(self.include_Veng.get()) )) + header.append('(k40_whisperer_set include_Vcut %s )' %( int(self.include_Vcut.get()) )) + header.append('(k40_whisperer_set include_Gcde %s )' %( int(self.include_Gcde.get()) )) + header.append('(k40_whisperer_set include_Time %s )' %( int(self.include_Time.get()) )) + + header.append('(k40_whisperer_set halftone %s )' %( int(self.halftone.get()) )) + header.append('(k40_whisperer_set HomeUR %s )' %( int(self.HomeUR.get()) )) + header.append('(k40_whisperer_set inputCSYS %s )' %( int(self.inputCSYS.get()) )) + header.append('(k40_whisperer_set advanced %s )' %( int(self.advanced.get()) )) + header.append('(k40_whisperer_set mirror %s )' %( int(self.mirror.get()) )) + header.append('(k40_whisperer_set rotate %s )' %( int(self.rotate.get()) )) + header.append('(k40_whisperer_set negate %s )' %( int(self.negate.get()) )) + + header.append('(k40_whisperer_set engraveUP %s )' %( int(self.engraveUP.get()) )) + header.append('(k40_whisperer_set init_home %s )' %( int(self.init_home.get()) )) + header.append('(k40_whisperer_set post_home %s )' %( int(self.post_home.get()) )) + header.append('(k40_whisperer_set post_beep %s )' %( int(self.post_beep.get()) )) + header.append('(k40_whisperer_set post_disp %s )' %( int(self.post_disp.get()) )) + header.append('(k40_whisperer_set post_exec %s )' %( int(self.post_exec.get()) )) + + header.append('(k40_whisperer_set pre_pr_crc %s )' %( int(self.pre_pr_crc.get()) )) + header.append('(k40_whisperer_set inside_first %s )' %( int(self.inside_first.get()) )) + + header.append('(k40_whisperer_set comb_engrave %s )' %( int(self.comb_engrave.get()) )) + header.append('(k40_whisperer_set comb_vector %s )' %( int(self.comb_vector.get()) )) + header.append('(k40_whisperer_set zoom2image %s )' %( int(self.zoom2image.get()) )) + header.append('(k40_whisperer_set rotary %s )' %( int(self.rotary.get()) )) + header.append('(k40_whisperer_set reduced_mem %s )' %( int(self.reduced_mem.get()) )) + header.append('(k40_whisperer_set wait %s )' %( int(self.wait.get()) )) + + header.append('(k40_whisperer_set trace_w_laser %s )' %( int(self.trace_w_laser.get()) )) + + # STRING.get() + header.append('(k40_whisperer_set board_name %s )' %( self.board_name.get() )) + header.append('(k40_whisperer_set units %s )' %( self.units.get() )) + header.append('(k40_whisperer_set Reng_feed %s )' %( self.Reng_feed.get() )) + header.append('(k40_whisperer_set Veng_feed %s )' %( self.Veng_feed.get() )) + header.append('(k40_whisperer_set Vcut_feed %s )' %( self.Vcut_feed.get() )) + header.append('(k40_whisperer_set jog_step %s )' %( self.jog_step.get() )) + + header.append('(k40_whisperer_set Reng_passes %s )' %( self.Reng_passes.get() )) + header.append('(k40_whisperer_set Veng_passes %s )' %( self.Veng_passes.get() )) + header.append('(k40_whisperer_set Vcut_passes %s )' %( self.Vcut_passes.get() )) + header.append('(k40_whisperer_set Gcde_passes %s )' %( self.Gcde_passes.get() )) + + header.append('(k40_whisperer_set rast_step %s )' %( self.rast_step.get() )) + header.append('(k40_whisperer_set ht_size %s )' %( self.ht_size.get() )) + + header.append('(k40_whisperer_set LaserXsize %s )' %( self.LaserXsize.get() )) + header.append('(k40_whisperer_set LaserYsize %s )' %( self.LaserYsize.get() )) + header.append('(k40_whisperer_set LaserXscale %s )' %( self.LaserXscale.get() )) + header.append('(k40_whisperer_set LaserYscale %s )' %( self.LaserYscale.get() )) + header.append('(k40_whisperer_set LaserRscale %s )' %( self.LaserRscale.get() )) + header.append('(k40_whisperer_set rapid_feed %s )' %( self.rapid_feed.get() )) + + header.append('(k40_whisperer_set gotoX %s )' %( self.gotoX.get() )) + header.append('(k40_whisperer_set gotoY %s )' %( self.gotoY.get() )) + + header.append('(k40_whisperer_set bezier_M1 %s )' %( self.bezier_M1.get() )) + header.append('(k40_whisperer_set bezier_M2 %s )' %( self.bezier_M2.get() )) + header.append('(k40_whisperer_set bezier_weight %s )' %( self.bezier_weight.get() )) + + header.append('(k40_whisperer_set trace_gap %s )' %( self.trace_gap.get() )) + header.append('(k40_whisperer_set trace_speed %s )' %( self.trace_speed.get() )) + +## header.append('(k40_whisperer_set unsharp_flag %s )' %( int(self.unsharp_flag.get()) )) +## header.append('(k40_whisperer_set unsharp_r %s )' %( self.unsharp_r.get() )) +## header.append('(k40_whisperer_set unsharp_p %s )' %( self.unsharp_p.get() )) +## header.append('(k40_whisperer_set unsharp_t %s )' %( self.unsharp_t.get() )) + + header.append('(k40_whisperer_set t_timeout %s )' %( self.t_timeout.get() )) + header.append('(k40_whisperer_set n_timeouts %s )' %( self.n_timeouts.get() )) + + header.append('(k40_whisperer_set ink_timeout %s )' %( self.ink_timeout.get() )) + + + header.append('(k40_whisperer_set designfile \042%s\042 )' %( self.DESIGN_FILE )) + header.append('(k40_whisperer_set inkscape_path \042%s\042 )' %( self.inkscape_path.get() )) + header.append('(k40_whisperer_set batch_path \042%s\042 )' %( self.batch_path.get() )) + + + self.jog_step + header.append("(=========================================================)") + + return header + ###################################################### + + def Quit_Click(self, event): + self.statusMessage.set("Exiting!") + self.Release_USB + root.destroy() + + def mousePanStart(self,event): + self.panx = event.x + self.pany = event.y + self.move_start_x = event.x + self.move_start_y = event.y + + def mousePan(self,event): + all = self.PreviewCanvas.find_all() + dx = event.x-self.panx + dy = event.y-self.pany + + self.PreviewCanvas.move('LaserTag', dx, dy) + self.lastx = self.lastx + dx + self.lasty = self.lasty + dy + self.panx = event.x + self.pany = event.y + + def mousePanStop(self,event): + Xold = round(self.laserX,3) + Yold = round(self.laserY,3) + + can_dx = event.x-self.move_start_x + can_dy = -(event.y-self.move_start_y) + + dx = can_dx*self.PlotScale + dy = can_dy*self.PlotScale + if self.HomeUR.get(): + dx = -dx + self.laserX,self.laserY = self.XY_in_bounds(dx,dy) + DXmils = round((self.laserX - Xold)*1000.0,0) + DYmils = round((self.laserY - Yold)*1000.0,0) + + if self.Send_Rapid_Move(DXmils,DYmils): + self.menu_View_Refresh() + + def right_mousePanStart(self,event): + self.s_panx = event.x + self.s_pany = event.y + self.s_move_start_x = event.x + self.s_move_start_y = event.y + + def right_mousePan(self,event): + all = self.PreviewCanvas.find_all() + dx = event.x-self.s_panx + dy = event.y-self.s_pany + + self.PreviewCanvas.move('LaserDot', dx, dy) + self.s_lastx = self.lastx + dx + self.s_lasty = self.lasty + dy + self.s_panx = event.x + self.s_pany = event.y + + def right_mousePanStop(self,event): + Xold = round(self.laserX,3) + Yold = round(self.laserY,3) + can_dx = event.x-self.s_move_start_x + can_dy = -(event.y-self.s_move_start_y) + + dx = can_dx*self.PlotScale + dy = can_dy*self.PlotScale + + DX = round(dx*1000) + DY = round(dy*1000) + self.Move_Arbitrary(DX,DY) + self.menu_View_Refresh() + + def LASER_Size(self): + MINX = 0.0 + MAXY = 0.0 + if self.units.get()=="in": + MAXX = float(self.LaserXsize.get()) + MINY = -float(self.LaserYsize.get()) + else: + MAXX = float(self.LaserXsize.get())/25.4 + MINY = -float(self.LaserYsize.get())/25.4 + + return (MAXX-MINX,MAXY-MINY) + + + def XY_in_bounds(self,dx_inches,dy_inches, no_size=False): + MINX = 0.0 + MAXY = 0.0 + if self.units.get()=="in": + MAXX = float(self.LaserXsize.get()) + MINY = -float(self.LaserYsize.get()) + else: + MAXX = float(self.LaserXsize.get())/25.4 + MINY = -float(self.LaserYsize.get())/25.4 + + if (self.inputCSYS.get() and self.RengData.image == None) or no_size: + xmin,xmax,ymin,ymax = 0.0,0.0,0.0,0.0 + else: + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + + X = self.laserX + dx_inches + Y = self.laserY + dy_inches + ################ + dx=xmax-xmin + dy=ymax-ymin + if X < MINX: + X = MINX + if X+dx > MAXX: + X = MAXX-dx + + if Y-dy < MINY: + Y = MINY+dy + if Y > MAXY: + Y = MAXY + ################ + if not no_size: + XOFF = self.pos_offset[0]/1000.0 + YOFF = self.pos_offset[1]/1000.0 + if X+XOFF < MINX: + X= X +(MINX-(X+XOFF)) + if X+XOFF > MAXX: + X= X -((X+XOFF)-MAXX) + if Y+YOFF < MINY: + Y= Y + (MINY-(Y+YOFF)) + if Y+YOFF > MAXY: + Y= Y -((Y+YOFF)-MAXY) + ################ + X = round(X,3) + Y = round(Y,3) + return X,Y + +## def computeAccurateVeng(self): +## self.update_gui("Optimize vector engrave.") +## self.VengData.set_ecoords(self.optimize_paths(self.VengData.ecoords),data_sorted=True) +## self.refreshTime() +## +## def computeAccurateVcut(self): +## self.update_gui("Optimize vector cut.") +## self.VcutData.set_ecoords(self.optimize_paths(self.VcutData.ecoords),data_sorted=True) +## self.refreshTime() +## +## def computeAccurateReng(self): +## self.update_gui("Calculating Raster engrave.") +## if self.RengData.image != None: +## if self.RengData.ecoords == []: +## self.make_raster_coords() +## self.RengData.sorted = True +## self.refreshTime() + + + def format_time(self,time_in_seconds): + # format the duration from seconds to something human readable + if time_in_seconds !=None and time_in_seconds >=0 : + s = round(time_in_seconds) + m,s=divmod(s,60) + h,m=divmod(m,60) + res = "" + if h > 0: + res = "%dh " %(h) + if m > 0: + res += "%dm " %(m) + if h == 0: + res += "%ds " %(s) + #L=len(res) + #for i in range(L,8): + # res = res+" " + return res + else : + return "?" + + def refreshTime(self): + if not self.include_Time.get(): + return + if self.units.get() == 'in': + factor = 60.0 + else : + factor = 25.4 + + Raster_eng_feed = float(self.Reng_feed.get()) / factor + Vector_eng_feed = float(self.Veng_feed.get()) / factor + Vector_cut_feed = float(self.Vcut_feed.get()) / factor + + Raster_eng_passes = float(self.Reng_passes.get()) + Vector_eng_passes = float(self.Veng_passes.get()) + Vector_cut_passes = float(self.Vcut_passes.get()) + Gcode_passes = float(self.Gcde_passes.get()) + + rapid_feed = 100.0 / 25.4 # 100 mm/s move feed to be confirmed + + if self.RengData.rpaths: + Reng_time=0 + else: + Reng_time = None + Veng_time = 0 + Vcut_time = 0 + + if self.RengData.len!=None: + # these equations are a terrible hack based on measured raster engraving times + # to be fixed someday + if Raster_eng_feed*60.0 <= 300: + accel_time=8.3264*(Raster_eng_feed*60.0)**(-0.7451) + else: + accel_time=2.5913*(Raster_eng_feed*60.0)**(-0.4795) + + t_accel = self.RengData.n_scanlines * accel_time + Reng_time = ( (self.RengData.len)/Raster_eng_feed ) * Raster_eng_passes + t_accel + if self.VengData.len!=None: + Veng_time = (self.VengData.len / Vector_eng_feed + self.VengData.move / rapid_feed) * Vector_eng_passes + if self.VcutData.len!=None: + Vcut_time = (self.VcutData.len / Vector_cut_feed + self.VcutData.move / rapid_feed) * Vector_cut_passes + + Gcode_time = self.GcodeData.gcode_time * Gcode_passes + + self.Reng_time.set("Raster Engrave: %s" %(self.format_time(Reng_time))) + self.Veng_time.set("Vector Engrave: %s" %(self.format_time(Veng_time))) + self.Vcut_time.set(" Vector Cut: %s" %(self.format_time(Vcut_time))) + self.Gcde_time.set(" Gcode: %s" %(self.format_time(Gcode_time))) + + ########################################## + cszw = int(self.PreviewCanvas.cget("width")) + cszh = int(self.PreviewCanvas.cget("height")) + HUD_vspace = 15 + HUD_X = cszw-5 + HUD_Y = cszh-5 + + w = int(self.master.winfo_width()) + h = int(self.master.winfo_height()) + HUD_X2 = w-20 + HUD_Y2 = h-75 + + self.PreviewCanvas.delete("HUD") + self.calc_button.place_forget() + + if self.GcodeData.ecoords == []: + self.PreviewCanvas.create_text(HUD_X, HUD_Y , fill = "red" ,text =self.Vcut_time.get(), anchor="se",tags="HUD") + self.PreviewCanvas.create_text(HUD_X, HUD_Y-HUD_vspace , fill = "blue" ,text =self.Veng_time.get(), anchor="se",tags="HUD") + + if (Reng_time==None): + #try: + # self.calc_button.place_forget() + #except: + # pass + #self.calc_button = Button(self.master,text="Calculate Raster Time", command=self.menu_Calc_Raster_Time) + self.calc_button.place(x=HUD_X2, y=HUD_Y2, width=120+20, height=17, anchor="se") + else: + self.calc_button.place_forget() + self.PreviewCanvas.create_text(HUD_X, HUD_Y-HUD_vspace*2, fill = "black", + text =self.Reng_time.get(), anchor="se",tags="HUD") + else: + self.PreviewCanvas.create_text(HUD_X, HUD_Y, fill = "black",text =self.Gcde_time.get(), anchor="se",tags="HUD") + ########################################## + + + def Settings_ReLoad_Click(self, event): + win_id=self.grab_current() + + def Close_Current_Window_Click(self,event=None): + current_name = event.widget.winfo_parent() + win_id = event.widget.nametowidget(current_name) + win_id.destroy() + + # Left Column # + ############################# + def Entry_Reng_feed_Check(self): + try: + value = float(self.Reng_feed.get()) + vfactor=(25.4/60.0)/self.feed_factor() + low_limit = self.min_raster_speed*vfactor + if value < low_limit: + self.statusMessage.set(" Feed Rate should be greater than or equal to %f " %(low_limit)) + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Reng_feed_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Reng_feed, self.Entry_Reng_feed_Check(), new=1) + ############################# + def Entry_Veng_feed_Check(self): + try: + value = float(self.Veng_feed.get()) + vfactor=(25.4/60.0)/self.feed_factor() + low_limit = self.min_vector_speed*vfactor + if value < low_limit: + self.statusMessage.set(" Feed Rate should be greater than or equal to %f " %(low_limit)) + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Veng_feed_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Veng_feed, self.Entry_Veng_feed_Check(), new=1) + ############################# + def Entry_Vcut_feed_Check(self): + try: + value = float(self.Vcut_feed.get()) + vfactor=(25.4/60.0)/self.feed_factor() + low_limit = self.min_vector_speed*vfactor + if value < low_limit: + self.statusMessage.set(" Feed Rate should be greater than or equal to %f " %(low_limit)) + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Vcut_feed_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Vcut_feed, self.Entry_Vcut_feed_Check(), new=1) + + ############################# + def Entry_Step_Check(self): + try: + value = float(self.jog_step.get()) + if value <= 0.0: + self.statusMessage.set(" Step should be greater than 0.0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Step_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Step, self.Entry_Step_Check(), new=1) + + + ############################# + def Entry_GoToX_Check(self): + try: + value = float(self.gotoX.get()) + if (value < 0.0) and (not self.HomeUR.get()): + self.statusMessage.set(" Value should be greater than 0.0 ") + return 2 # Value is invalid number + elif (value > 0.0) and self.HomeUR.get(): + self.statusMessage.set(" Value should be less than 0.0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_GoToX_Callback(self, varName, index, mode): + self.entry_set(self.Entry_GoToX, self.Entry_GoToX_Check(), new=1) + + ############################# + def Entry_GoToY_Check(self): + try: + value = float(self.gotoY.get()) + if value > 0.0: + self.statusMessage.set(" Value should be less than 0.0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_GoToY_Callback(self, varName, index, mode): + self.entry_set(self.Entry_GoToY, self.Entry_GoToY_Check(), new=1) + + ############################# + def Entry_Rstep_Check(self): + try: + value = self.get_raster_step_1000in() + if value <= 0 or value > 63: + self.statusMessage.set(" Step should be between 0.001 and 0.063 in") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Rstep_Callback(self, varName, index, mode): + self.RengData.reset_path() + self.refreshTime() + self.entry_set(self.Entry_Rstep, self.Entry_Rstep_Check(), new=1) + +## ############################# +## def Entry_Unsharp_Radius_Check(self): +## try: +## value = float(self.unsharp_r.get()) +## if value <= 0: +## self.statusMessage.set(" Radius should be greater than zero.") +## return 2 # Value is invalid number +## except: +## return 3 # Value not a number +## self.menu_View_Refresh_Callback() +## return 0 # Value is a valid number +## def Entry_Unsharp_Radius_Callback(self, varName, index, mode): +## self.entry_set(self.Entry_Unsharp_Radius, self.Entry_Unsharp_Radius_Check(), new=1) +## +## +## ############################# +## def Entry_Unsharp_Percent_Check(self): +## try: +## value = float(self.unsharp_p.get()) +## if value <= 0: +## self.statusMessage.set(" Percent should be greater than zero.") +## return 2 # Value is invalid number +## except: +## return 3 # Value not a number +## self.menu_View_Refresh_Callback() +## return 0 # Value is a valid number +## def Entry_Unsharp_Percent_Callback(self, varName, index, mode): +## self.entry_set(self.Entry_Unsharp_Percent, self.Entry_Unsharp_Percent_Check(), new=1) +## +## ############################# +## def Entry_Unsharp_Threshold_Check(self): +## try: +## value = float(self.unsharp_t.get()) +## if value < 0: +## self.statusMessage.set(" Threshold should be greater than or equal to zero.") +## return 2 # Value is invalid number +## except: +## return 3 # Value not a number +## self.menu_View_Refresh_Callback() +## return 0 # Value is a valid number +## def Entry_Unsharp_Threshold_Callback(self, varName, index, mode): +## self.entry_set(self.Entry_Unsharp_Threshold, self.Entry_Unsharp_Threshold_Check(), new=1) + + ############################# + # End Left Column # + ############################# + def bezier_weight_Callback(self, varName=None, index=None, mode=None): + self.Reset_RasterPath_and_Update_Time() + self.bezier_plot() + + def bezier_M1_Callback(self, varName=None, index=None, mode=None): + self.Reset_RasterPath_and_Update_Time() + self.bezier_plot() + + def bezier_M2_Callback(self, varName=None, index=None, mode=None): + self.Reset_RasterPath_and_Update_Time() + self.bezier_plot() + + def bezier_plot(self): + self.BezierCanvas.delete('bez') + + #self.BezierCanvas.create_line( 5,260-0,260,260-255,fill="black", capstyle="round", width = 2, tags='bez') + M1 = float(self.bezier_M1.get()) + M2 = float(self.bezier_M2.get()) + w = float(self.bezier_weight.get()) + num = 10 + x,y = self.generate_bezier(M1,M2,w,n=num) + for i in range(0,num): + self.BezierCanvas.create_line( 5+x[i],260-y[i],5+x[i+1],260-y[i+1],fill="black", \ + capstyle="round", width = 2, tags='bez') + self.BezierCanvas.create_text(128, 0, text="Output Level vs. Input Level",anchor="n", tags='bez') + + + ############################# + def Entry_Ink_Timeout_Check(self): + try: + value = float(self.ink_timeout.get()) + if value < 0.0: + self.statusMessage.set(" Timeout should be 0 or greater") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Ink_Timeout_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Ink_Timeout,self.Entry_Ink_Timeout_Check(), new=1) + + + ############################# + def Entry_Timeout_Check(self): + try: + value = float(self.t_timeout.get()) + if value <= 0.0: + self.statusMessage.set(" Timeout should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Timeout_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Timeout,self.Entry_Timeout_Check(), new=1) + + ############################# + def Entry_N_Timeouts_Check(self): + try: + value = float(self.n_timeouts.get()) + if value <= 0.0: + self.statusMessage.set(" N_Timeouts should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_N_Timeouts_Callback(self, varName, index, mode): + self.entry_set(self.Entry_N_Timeouts,self.Entry_N_Timeouts_Check(), new=1) + + ############################# + def Entry_N_EGV_Passes_Check(self): + try: + value = int(self.n_egv_passes.get()) + if value < 1: + self.statusMessage.set(" EGV passes should be 1 or higher") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_N_EGV_Passes_Callback(self, varName, index, mode): + self.entry_set(self.Entry_N_EGV_Passes,self.Entry_N_EGV_Passes_Check(), new=1) + + ############################# + def Entry_Laser_Area_Width_Check(self): + try: + value = float(self.LaserXsize.get()) + if value <= 0.0: + self.statusMessage.set(" Width should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Laser_Area_Width_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_Area_Width,self.Entry_Laser_Area_Width_Check(), new=1) + + ############################# + def Entry_Laser_Area_Height_Check(self): + try: + value = float(self.LaserYsize.get()) + if value <= 0.0: + self.statusMessage.set(" Height should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Laser_Area_Height_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_Area_Height,self.Entry_Laser_Area_Height_Check(), new=1) + + + ############################# + def Entry_Laser_X_Scale_Check(self): + try: + value = float(self.LaserXscale.get()) + if value <= 0.0: + self.statusMessage.set(" X scale factor should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.Reset_RasterPath_and_Update_Time() + return 0 # Value is a valid number + def Entry_Laser_X_Scale_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_X_Scale,self.Entry_Laser_X_Scale_Check(), new=1) + ############################# + def Entry_Laser_Y_Scale_Check(self): + try: + value = float(self.LaserYscale.get()) + if value <= 0.0: + self.statusMessage.set(" Y scale factor should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.Reset_RasterPath_and_Update_Time() + return 0 # Value is a valid number + def Entry_Laser_Y_Scale_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_Y_Scale,self.Entry_Laser_Y_Scale_Check(), new=1) + + ############################# + def Entry_Laser_R_Scale_Check(self): + try: + value = float(self.LaserRscale.get()) + if value <= 0.0: + self.statusMessage.set(" Rotary scale factor should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.Reset_RasterPath_and_Update_Time() + return 0 # Value is a valid number + def Entry_Laser_R_Scale_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_R_Scale,self.Entry_Laser_R_Scale_Check(), new=1) + + ############################# + def Entry_Laser_Rapid_Feed_Check(self): + try: + value = float(self.rapid_feed.get()) + vfactor=(25.4/60.0)/self.feed_factor() + low_limit = 1.0*vfactor + if value !=0 and value < low_limit: + self.statusMessage.set(" Rapid feed should be greater than or equal to %f (or 0 for default speed) " %(low_limit)) + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Laser_Rapid_Feed_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Laser_Rapid_Feed,self.Entry_Laser_Rapid_Feed_Check(), new=1) + + # Advanced Column # + ############################# + def Entry_Reng_passes_Check(self): + try: + value = int(self.Reng_passes.get()) + if value < 1: + self.statusMessage.set(" Number of passes should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Reng_passes_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Reng_passes, self.Entry_Reng_passes_Check(), new=1) + ############################# + def Entry_Veng_passes_Check(self): + try: + value = int(self.Veng_passes.get()) + if value < 1: + self.statusMessage.set(" Number of passes should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Veng_passes_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Veng_passes, self.Entry_Veng_passes_Check(), new=1) + ############################# + def Entry_Vcut_passes_Check(self): + try: + value = int(self.Vcut_passes.get()) + if value < 1: + self.statusMessage.set(" Number of passes should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Vcut_passes_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Vcut_passes, self.Entry_Vcut_passes_Check(), new=1) + + ############################# + def Entry_Gcde_passes_Check(self): + try: + value = int(self.Gcde_passes.get()) + if value < 1: + self.statusMessage.set(" Number of passes should be greater than 0 ") + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Gcde_passes_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Gcde_passes, self.Entry_Gcde_passes_Check(), new=1) + + ############################# + + def Entry_Trace_Gap_Check(self): + try: + value = float(self.trace_gap.get()) + except: + return 3 # Value not a number + self.menu_View_Refresh() + return 0 # Value is a valid number + def Entry_Trace_Gap_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Trace_Gap, self.Entry_Trace_Gap_Check(), new=1) + + ############################# + + def Entry_Trace_Speed_Check(self): + try: + value = float(self.trace_speed.get()) + vfactor=(25.4/60.0)/self.feed_factor() + low_limit = self.min_vector_speed*vfactor + if value < low_limit: + self.statusMessage.set(" Feed Rate should be greater than or equal to %f " %(low_limit)) + return 2 # Value is invalid number + except: + return 3 # Value not a number + self.refreshTime() + return 0 # Value is a valid number + def Entry_Trace_Speed_Callback(self, varName, index, mode): + self.entry_set(self.Entry_Trace_Speed, self.Entry_Trace_Speed_Check(), new=1) + + ############################# + def Inkscape_Path_Click(self, event): + self.Inkscape_Path_Message() + win_id=self.grab_current() + newfontdir = askopenfilename(filetypes=[("Executable Files",("inkscape.exe","*inkscape*")),\ + ("All Files","*")],\ + initialdir=self.inkscape_path.get()) + if newfontdir != "" and newfontdir != (): + if type(newfontdir) is not str: + newfontdir = newfontdir.encode("utf-8") + self.inkscape_path.set(newfontdir) + + try: + win_id.withdraw() + win_id.deiconify() + except: + pass + + def Inkscape_Path_Message(self, event=None): + if self.inkscape_warning == False: + self.inkscape_warning = True + msg1 = "Beware:" + msg2 = "Most people should leave the 'Inkscape Executable' entry field blank. " + msg3 = "K40 Whisperer will find Inkscape in one of the the standard locations after you install Inkscape." + message_box(msg1, msg2+msg3) + + + def Entry_units_var_Callback(self): + if (self.units.get() == 'in') and (self.funits.get()=='mm/s'): + self.funits.set('in/min') + self.Scale_Linear_Inputs('in') + elif (self.units.get() == 'mm') and (self.funits.get()=='in/min'): + self.funits.set('mm/s') + self.Scale_Linear_Inputs('mm') + + def Scale_Linear_Inputs(self, new_units=None): + if new_units=='in': + self.units_scale = 1.0 + factor = 1/25.4 + vfactor = 60.0/25.4 + elif new_units=='mm': + factor = 25.4 + vfactor = 25.4/60.0 + self.units_scale = 25.4 + else: + return + self.LaserXsize.set ( self.Scale_Text_Value('%.2f',self.LaserXsize.get() ,factor ) ) + self.LaserYsize.set ( self.Scale_Text_Value('%.2f',self.LaserYsize.get() ,factor ) ) + self.jog_step.set ( self.Scale_Text_Value('%.3f',self.jog_step.get() ,factor ) ) + self.gotoX.set ( self.Scale_Text_Value('%.3f',self.gotoX.get() ,factor ) ) + self.gotoY.set ( self.Scale_Text_Value('%.3f',self.gotoY.get() ,factor ) ) + self.Reng_feed.set ( self.Scale_Text_Value('%.1f',self.Reng_feed.get() ,vfactor) ) + self.Veng_feed.set ( self.Scale_Text_Value('%.1f',self.Veng_feed.get() ,vfactor) ) + self.Vcut_feed.set ( self.Scale_Text_Value('%.1f',self.Vcut_feed.get() ,vfactor) ) + self.trace_speed.set( self.Scale_Text_Value('%.1f',self.trace_speed.get() ,vfactor) ) + self.rapid_feed.set ( self.Scale_Text_Value('%.1f',self.rapid_feed.get() ,vfactor) ) + + def Scale_Text_Value(self,format_txt,Text_Value,factor): + try: + return format_txt %(float(Text_Value)*factor ) + except: + return '' + + def menu_File_Open_Settings_File(self,event=None): + init_dir = os.path.dirname(self.DESIGN_FILE) + if ( not os.path.isdir(init_dir) ): + init_dir = self.HOME_DIR + fileselect = askopenfilename(filetypes=[("Settings Files","*.txt"),\ + ("All Files","*")],\ + initialdir=init_dir) + if fileselect != '' and fileselect != (): + self.Open_Settings_File(fileselect) + + def Reduced_Memory_Callback(self, varName, index, mode): + if self.RengData.image != None: + self.menu_Reload_Design() + #print("Reload_Design") + + def menu_Reload_Design(self,event=None): + if self.GUI_Disabled: + return + file_full = self.DESIGN_FILE + file_name = os.path.basename(file_full) + if ( os.path.isfile(file_full) ): + filename = file_full + elif ( os.path.isfile( file_name ) ): + filename = file_name + elif ( os.path.isfile( self.HOME_DIR+"/"+file_name ) ): + filename = self.HOME_DIR+"/"+file_name + else: + self.statusMessage.set("file not found: %s" %(os.path.basename(file_full)) ) + self.statusbar.configure( bg = 'red' ) + return + + Name, fileExtension = os.path.splitext(filename) + TYPE=fileExtension.upper() + if TYPE=='.DXF': + self.Open_DXF(filename) + elif TYPE=='.SVG': + self.Open_SVG(filename) + elif TYPE=='.EGV': + self.EGV_Send_Window(filename) + else: + self.Open_G_Code(filename) + self.menu_View_Refresh() + + + + def menu_File_Open_Design(self,event=None): + if self.GUI_Disabled: + return + init_dir = os.path.dirname(self.DESIGN_FILE) + if ( not os.path.isdir(init_dir) ): + init_dir = self.HOME_DIR + + design_types = ("Design Files", ("*.svg","*.dxf")) + gcode_types = ("G-Code Files", ("*.ngc","*.gcode","*.g","*.tap")) + + Name, fileExtension = os.path.splitext(self.DESIGN_FILE) + TYPE=fileExtension.upper() + if TYPE != '.DXF' and TYPE!='.SVG' and TYPE!='.EGV' and TYPE!='': + default_types = gcode_types + else: + default_types = design_types + + fileselect = askopenfilename(filetypes=[default_types, + ("G-Code Files ", ("*.ngc","*.gcode","*.g","*.tap")),\ + ("DXF Files ","*.dxf"),\ + ("SVG Files ","*.svg"),\ + ("All Files ","*"),\ + ("Design Files ", ("*.svg","*.dxf"))],\ + initialdir=init_dir) + + if fileselect == () or (not os.path.isfile(fileselect)): + return + + Name, fileExtension = os.path.splitext(fileselect) + self.update_gui("Opening '%s'" % fileselect ) + TYPE=fileExtension.upper() + if TYPE=='.DXF': + self.Open_DXF(fileselect) + elif TYPE=='.SVG': + self.Open_SVG(fileselect) + else: + self.Open_G_Code(fileselect) + + + self.DESIGN_FILE = fileselect + self.menu_View_Refresh() + + def menu_File_Raster_Engrave(self): + self.menu_File_save_EGV(operation_type="Raster_Eng") + + def menu_File_Vector_Engrave(self): + self.menu_File_save_EGV(operation_type="Vector_Eng") + + def menu_File_Vector_Cut(self): + self.menu_File_save_EGV(operation_type="Vector_Cut") + + def menu_File_G_Code(self): + self.menu_File_save_EGV(operation_type="Gcode_Cut") + + def menu_File_Raster_Vector_Engrave(self): + self.menu_File_save_EGV(operation_type="Raster_Eng-Vector_Eng") + + def menu_File_Vector_Engrave_Cut(self): + self.menu_File_save_EGV(operation_type="Vector_Eng-Vector_Cut") + + def menu_File_Raster_Vector_Cut(self): + self.menu_File_save_EGV(operation_type="Raster_Eng-Vector_Eng-Vector_Cut") + + def menu_File_save_EGV(self,operation_type=None,default_name="out.EGV"): + self.stop[0]=False + if DEBUG: + start=time() + fileName, fileExtension = os.path.splitext(self.DESIGN_FILE) + init_file=os.path.basename(fileName) + default_name = init_file+"_"+operation_type + + if self.EGV_FILE != None: + init_dir = os.path.dirname(self.EGV_FILE) + else: + init_dir = os.path.dirname(self.DESIGN_FILE) + + if ( not os.path.isdir(init_dir) ): + init_dir = self.HOME_DIR + + fileName, fileExtension = os.path.splitext(default_name) + init_file=os.path.basename(fileName) + + filename = asksaveasfilename(defaultextension='.EGV', \ + filetypes=[("EGV File","*.EGV")],\ + initialdir=init_dir,\ + initialfile= init_file ) + + if filename != '' and filename != (): + + if operation_type.find("Raster_Eng") > -1: + self.make_raster_coords() + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No raster data to engrave") + + self.send_data(operation_type=operation_type, output_filename=filename) + self.EGV_FILE = filename + if DEBUG: + print("time = %d seconds" %(int(time()-start))) + self.stop[0]=True + + + + def menu_File_Open_EGV(self): + init_dir = os.path.dirname(self.DESIGN_FILE) + if ( not os.path.isdir(init_dir) ): + init_dir = self.HOME_DIR + fileselect = askopenfilename(filetypes=[("Engraver Files", ("*.egv","*.EGV")),\ + ("All Files","*")],\ + initialdir=init_dir) + if fileselect != '' and fileselect != (): + self.resetPath() + self.DESIGN_FILE = fileselect + self.EGV_Send_Window(fileselect) + + def Open_EGV(self,filemname,n_passes=1): + self.stop[0]=False + EGV_data=[] + value1 = "" + value2 = "" + value3 = "" + value4 = "" + data="" + #value1 and value2 are the absolute y and x starting positions + #value3 and value4 are the absolute y and x end positions + with open(filemname) as f: + while True: + ## Skip header + c = f.read(1) + while c!="%" and c: + c = f.read(1) + ## Read 1st Value + c = f.read(1) + while c!="%" and c: + value1 = value1 + c + c = f.read(1) + y_start_mils = int(value1) + ## Read 2nd Value + c = f.read(1) + while c!="%" and c: + value2 = value2 + c + c = f.read(1) + x_start_mils = int(value2) + ## Read 3rd Value + c = f.read(1) + while c!="%" and c: + value3 = value3 + c + c = f.read(1) + y_end_mils = int(value3) + ## Read 4th Value + c = f.read(1) + while c!="%" and c: + value4 = value4 + c + c = f.read(1) + x_end_mils = int(value4) + break + + ## Read Data + while True: + c = f.read(1) + if not c: + break + if c=='\n' or c==' ' or c=='\r': + pass + else: + data=data+"%c" %c + EGV_data.append(ord(c)) + + if ( (x_end_mils != 0) or (y_end_mils != 0) ): + n_passes=1 + else: + x_start_mils = 0 + y_start_mils = 0 + + try: + self.send_egv_data(EGV_data,n_passes) + except MemoryError as e: + msg1 = "Memory Error:" + msg2 = "Memory Error: Out of Memory." + self.statusMessage.set(msg2) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + except Exception as e: + msg1 = "Sending Data Stopped: " + msg2 = "%s" %(e) + if msg2 == "": + formatted_lines = traceback.format_exc().splitlines() + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + #rapid move back to starting position + dxmils = -(x_end_mils - x_start_mils) + dymils = y_end_mils - y_start_mils + self.Send_Rapid_Move(dxmils,dxmils) + self.stop[0]=True + + def Open_SVG(self,filemname): + self.resetPath() + self.SVG_FILE = filemname + if self.reduced_mem.get(): + self.input_dpi = 500.0 + else: + self.input_dpi = 1000.0 + svg_reader = SVG_READER() + svg_reader.image_dpi = self.input_dpi + svg_reader.set_inkscape_path(self.inkscape_path.get()) + svg_reader.timout = int(float( self.ink_timeout.get())*60.0) + dialog_pxpi = None + dialog_viewbox = None + try: + try: + try: + svg_reader.parse_svg(self.SVG_FILE) + svg_reader.make_paths() + except SVG_PXPI_EXCEPTION as e: + pxpi_dialog = pxpiDialog(root, + self.units.get(), + svg_reader.SVG_Size, + svg_reader.SVG_ViewBox, + svg_reader.SVG_inkscape_version) + + svg_reader = SVG_READER() + svg_reader.image_dpi = self.input_dpi + svg_reader.set_inkscape_path(self.inkscape_path.get()) + svg_reader.timout = int(float( self.ink_timeout.get())*60.0) + if pxpi_dialog.result == None: + return + + dialog_pxpi,dialog_viewbox = pxpi_dialog.result + svg_reader.parse_svg(self.SVG_FILE) + svg_reader.set_size(dialog_pxpi,dialog_viewbox) + svg_reader.make_paths() + + except SVG_TEXT_EXCEPTION as e: + svg_reader = SVG_READER() + svg_reader.image_dpi = self.input_dpi + svg_reader.set_inkscape_path(self.inkscape_path.get()) + svg_reader.timout = int(float( self.ink_timeout.get())*60.0) + self.statusMessage.set("Converting TEXT to PATHS.") + self.master.update() + svg_reader.parse_svg(self.SVG_FILE) + if dialog_pxpi != None and dialog_viewbox != None: + svg_reader.set_size(dialog_pxpi,dialog_viewbox) + svg_reader.make_paths(txt2paths=True) + + except Exception as e: + msg1 = "SVG Error: " + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + return + except: + self.statusMessage.set("Unable To open SVG File: %s" %(filemname)) + debug_message(traceback.format_exc()) + return + xmax = svg_reader.Xsize/25.4 + ymax = svg_reader.Ysize/25.4 + xmin = 0 + ymin = 0 + + self.Design_bounds = (xmin,xmax,ymin,ymax) + + ########################## + ### Create ECOORDS ### + ########################## + self.VcutData.make_ecoords(svg_reader.cut_lines,scale=1/25.4) + self.VengData.make_ecoords(svg_reader.eng_lines,scale=1/25.4) + + ########################## + ### Load Image ### + ########################## + self.RengData.set_image(svg_reader.raster_PIL) + + if (self.RengData.image != None): + self.wim, self.him = self.RengData.image.size + self.aspect_ratio = float(self.wim-1) / float(self.him-1) + #self.make_raster_coords() + self.refreshTime() + margin=0.0625 # A bit of margin to prevent the warningwindow for designs that are close to being within the bounds + if self.Design_bounds[0] > self.VengData.bounds[0]+margin or\ + self.Design_bounds[0] > self.VcutData.bounds[0]+margin or\ + self.Design_bounds[1] < self.VengData.bounds[1]-margin or\ + self.Design_bounds[1] < self.VcutData.bounds[1]-margin or\ + self.Design_bounds[2] > self.VengData.bounds[2]+margin or\ + self.Design_bounds[2] > self.VcutData.bounds[2]+margin or\ + self.Design_bounds[3] < self.VengData.bounds[3]-margin or\ + self.Design_bounds[3] < self.VcutData.bounds[3]-margin: + line1 = "Warning:\n" + line2 = "There is vector cut or vector engrave data located outside of the SVG page bounds.\n\n" + line3 = "K40 Whisperer will attempt to use all of the vector data. " + line4 = "Please verify that the vector data is not outside of your lasers working area before engraving." + message_box("Warning", line1+line2+line3+line4) + + + ##################################################################### + def make_raster_coords(self): + if self.RengData.rpaths: + return + try: + hcoords=[] + if (self.RengData.image != None and self.RengData.ecoords==[]): + ecoords=[] + cutoff=128 + image_temp = self.RengData.image.convert("L") +## if self.unsharp_flag.get(): +## from PIL import ImageFilter +## #image_temp = image_temp.filter(UnsharpMask(radius=self.unsharp_r, percent=self.unsharp_p, threshold=self.unsharp_t)) +## filter = ImageFilter.UnsharpMask() +## filter.radius = float(self.unsharp_r.get()) # radius 3-5 pixels +## filter.percent = int(float(self.unsharp_p.get())) # precent 500% +## filter.threshold = int(float(self.unsharp_t.get())) # Threshold 0 +## image_temp = image_temp.filter(filter) + + if self.negate.get(): + image_temp = ImageOps.invert(image_temp) + + if self.mirror.get(): + image_temp = ImageOps.mirror(image_temp) + + if self.rotate.get(): + #image_temp = image_temp.rotate(90,expand=True) + image_temp = self.rotate_raster(image_temp) + + Xscale = float(self.LaserXscale.get()) + Yscale = float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + + if Xscale != 1.0 or Yscale != 1.0: + wim,him = image_temp.size + nw = int(wim*Xscale) + nh = int(him*Yscale) + image_temp = image_temp.resize((nw,nh)) + + + if self.halftone.get(): + ht_size_mils = round( self.input_dpi / float(self.ht_size.get()) ,1) + npixels = int( round(ht_size_mils,1) ) + if npixels == 0: + return + wim,him = image_temp.size + # Convert to Halftoning and save + nw=int(wim / npixels) + nh=int(him / npixels) + image_temp = image_temp.resize((nw,nh)) + + image_temp = self.convert_halftoning(image_temp) + image_temp = image_temp.resize((wim,him)) + else: + image_temp = image_temp.point(lambda x: 0 if x<128 else 255, '1') + + if DEBUG: + image_name = os.path.expanduser("~")+"/IMAGE.png" + image_temp.save(image_name,"PNG") + + Reng_np = image_temp.load() + wim,him = image_temp.size + del image_temp + ####################################### + x=0 + y=0 + loop=1 + LENGTH=0 + n_scanlines = 0 + + my_hull = hull2D() + bignumber = 9999999; + Raster_step = int(self.get_raster_step_1000in()) + timestamp=0 + im_height_mils = int(him/self.input_dpi*1000.0) + for i_step in range(0,im_height_mils,Raster_step): + i=floor(i_step*self.input_dpi/1000.0) + #print(i_step,i) + stamp=int(3*time()) #update every 1/3 of a second + if (stamp != timestamp): + timestamp=stamp #interlock + self.statusMessage.set("Creating Scan Lines: %.1f %%" %( (100.0*i)/him ) ) + self.master.update() + if self.stop[0]==True: + raise Exception("Action stopped by User.") + line = [] + cnt=1 + LEFT = bignumber; + RIGHT =-bignumber; + for j in range(1,wim): + if (Reng_np[j,i] == Reng_np[j-1,i]): + cnt = cnt+1 + else: + if Reng_np[j-1,i]: + laser = "U" + else: + laser = "D" + LEFT = min(j-cnt,LEFT) + RIGHT = max(j,RIGHT) + + line.append((cnt,laser)) + cnt=1 + if Reng_np[j-1,i] > cutoff: + laser = "U" + else: + laser = "D" + LEFT = min(j-cnt,LEFT) + RIGHT = max(j,RIGHT) + + line.append((cnt,laser)) + if LEFT != bignumber and RIGHT != -bignumber: + LENGTH = LENGTH + (RIGHT - LEFT)/self.input_dpi + n_scanlines = n_scanlines + 1 + + y=(im_height_mils-i_step)/1000.0 + x=0 + if LEFT != bignumber: + hcoords.append([LEFT/self.input_dpi,y]) + if RIGHT != -bignumber: + hcoords.append([RIGHT/self.input_dpi,y]) + if hcoords!=[]: + hcoords = my_hull.convexHullecoords(hcoords) + + rng = list(range(0,len(line),1)) + + for i in rng: + seg = line[i] + delta = seg[0]/self.input_dpi + if seg[1]=="D": + loop=loop+1 + ecoords.append([x ,y,loop]) + ecoords.append([x+delta,y,loop]) + x = x + delta + self.RengData.set_ecoords(ecoords,data_sorted=True) + self.RengData.len=LENGTH + self.RengData.n_scanlines = n_scanlines + #Set Flag indicating raster paths have been calculated + self.RengData.rpaths = True + self.RengData.hull_coords = hcoords + + except MemoryError as e: + msg1 = "Memory Error:" + msg2 = "Memory Error: Out of Memory." + self.statusMessage.set(msg2) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + except Exception as e: + msg1 = "Making Raster Coords Stopped: " + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + ####################################################################### + + + def rotate_raster(self,image_in): + wim,him = image_in.size + im_rotated = Image.new("L", (him, wim), "white") + + image_in_np = image_in.load() + im_rotated_np = im_rotated.load() + + for i in range(1,him): + for j in range(1,wim): + im_rotated_np[i,wim-j] = image_in_np[j,i] + return im_rotated + + def get_raster_step_1000in(self): + val_in = float(self.rast_step.get()) + value = int(round(val_in*1000.0,1)) + return value + + + def generate_bezier(self,M1,M2,w,n=100): + if (M1==M2): + x1=0 + y1=0 + else: + x1 = 255*(1-M2)/(M1-M2) + y1 = M1*x1 + x=[] + y=[] + # Calculate Bezier Curve + for step in range(0,n+1): + t = float(step)/float(n) + Ct = 1 / ( pow(1-t,2)+2*(1-t)*t*w+pow(t,2) ) + x.append( Ct*( 2*(1-t)*t*w*x1+pow(t,2)*255) ) + y.append( Ct*( 2*(1-t)*t*w*y1+pow(t,2)*255) ) + return x,y + + '''This Example opens an Image and transform the image into halftone. -Isai B. Cicourel''' + # Create a Half-tone version of the image + def convert_halftoning(self,image): + image = image.convert('L') + x_lim, y_lim = image.size + pixel = image.load() + + M1 = float(self.bezier_M1.get()) + M2 = float(self.bezier_M2.get()) + w = float(self.bezier_weight.get()) + + if w > 0: + x,y = self.generate_bezier(M1,M2,w) + + interp = interpolate(x, y) # Set up interpolate class + val_map=[] + # Map Bezier Curve to values between 0 and 255 + for val in range(0,256): + val_out = int(round(interp[val])) # Get the interpolated value at each value + val_map.append(val_out) + # Adjust image + timestamp=0 + for y in range(1, y_lim): + stamp=int(3*time()) #update every 1/3 of a second + if (stamp != timestamp): + timestamp=stamp #interlock + self.statusMessage.set("Adjusting Image Darkness: %.1f %%" %( (100.0*y)/y_lim ) ) + self.master.update() + for x in range(1, x_lim): + pixel[x, y] = val_map[ pixel[x, y] ] + + self.statusMessage.set("Creating Halftone Image." ) + self.master.update() + image = image.convert('1') + return image + + ####################################################################### + + def gcode_error_message(self,message): + error_report = Toplevel(width=525,height=60) + error_report.title("G-Code Reading Errors/Warnings") + error_report.iconname("G-Code Errors") + error_report.grab_set() + return_value = StringVar() + return_value.set("none") + + + def Close_Click(event): + return_value.set("close") + error_report.destroy() + + #Text Box + Error_Frame = Frame(error_report) + scrollbar = Scrollbar(Error_Frame, orient=VERTICAL) + Error_Text = Text(Error_Frame, width="80", height="20",yscrollcommand=scrollbar.set,bg='white') + for line in message: + Error_Text.insert(END,line+"\n") + scrollbar.config(command=Error_Text.yview) + scrollbar.pack(side=RIGHT,fill=Y) + #End Text Box + + Button_Frame = Frame(error_report) + close_button = Button(Button_Frame,text=" Close ") + close_button.bind("", Close_Click) + close_button.pack(side=RIGHT,fill=X) + + Error_Text.pack(side=LEFT,fill=BOTH,expand=1) + Button_Frame.pack(side=BOTTOM) + Error_Frame.pack(side=LEFT,fill=BOTH,expand=1) + + root.wait_window(error_report) + return return_value.get() + + def Open_G_Code(self,filename): + self.resetPath() + + g_rip = G_Code_Rip() + try: + MSG = g_rip.Read_G_Code(filename, XYarc2line = True, arc_angle=2, units="in", Accuracy="") + Error_Text = "" + if MSG!=[]: + self.gcode_error_message(MSG) + + #except StandardError as e: + except Exception as e: + msg1 = "G-Code Load Failed: " + msg2 = "Filename: %s" %(filename) + msg3 = "%s" %(e) + self.statusMessage.set((msg1+msg3).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, "%s\n%s" %(msg2,msg3)) + debug_message(traceback.format_exc()) + + + ecoords= g_rip.generate_laser_paths(g_rip.g_code_data) + self.GcodeData.set_ecoords(ecoords,data_sorted=True) + self.Design_bounds = self.GcodeData.bounds + + + def Open_DXF(self,filemname): + self.resetPath() + + self.DXF_FILE = filemname + dxf_import=DXF_CLASS() + tolerance = .0005 + try: + fd = open(self.DXF_FILE) + dxf_import.GET_DXF_DATA(fd,lin_tol = tolerance,get_units=True,units=None) + fd.seek(0) + + dxf_units = dxf_import.units + if dxf_units=="Unitless": + d = UnitsDialog(root) + dxf_units = d.result + if dxf_units=="Inches": + dxf_scale = 1.0 + elif dxf_units=="Feet": + dxf_scale = 12.0 + elif dxf_units=="Miles": + dxf_scale = 5280.0*12.0 + elif dxf_units=="Millimeters": + dxf_scale = 1.0/25.4 + elif dxf_units=="Centimeters": + dxf_scale = 1.0/2.54 + elif dxf_units=="Meters": + dxf_scale = 1.0/254.0 + elif dxf_units=="Kilometers": + dxf_scale = 1.0/254000.0 + elif dxf_units=="Microinches": + dxf_scale = 1.0/1000000.0 + elif dxf_units=="Mils": + dxf_scale = 1.0/1000.0 + else: + return + + lin_tol = tolerance / dxf_scale + dxf_import.GET_DXF_DATA(fd,lin_tol=lin_tol,get_units=False,units=None) + fd.close() + #except StandardError as e: + except Exception as e: + msg1 = "DXF Load Failed:" + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + except: + fmessage("Unable To open Drawing Exchange File (DXF) file.") + debug_message(traceback.format_exc()) + return + + new_origin=False + dxf_engrave_coords = dxf_import.DXF_COORDS_GET_TYPE(engrave=True, new_origin=False) + dxf_cut_coords = dxf_import.DXF_COORDS_GET_TYPE(engrave=False,new_origin=False) +## if DEBUG: +## dxf_code = dxf_import.WriteDXF(close_loops=False) +## fout = open('Z:\\out.dxf','w') +## for line in dxf_code: +## fout.write(line+'\n') +## fout.close + + if dxf_import.dxf_messages != "": + msg_split=dxf_import.dxf_messages.split("\n") + msg_split.sort() + msg_split.append("") + mcnt=1 + msg_out = "" + for i in range(1,len(msg_split)): + if msg_split[i-1]==msg_split[i]: + mcnt=mcnt+1 + else: + if msg_split[i-1]!="": + msg_line = "%s (%d places)\n" %(msg_split[i-1],mcnt) + msg_out = msg_out + msg_line + mcnt=1 + message_box("DXF Import:",msg_out) + + ########################## + ### Create ECOORDS ### + ########################## + self.VcutData.make_ecoords(dxf_cut_coords ,scale=dxf_scale) + self.VengData.make_ecoords(dxf_engrave_coords,scale=dxf_scale) + + xmin = min(self.VcutData.bounds[0],self.VengData.bounds[0]) + xmax = max(self.VcutData.bounds[1],self.VengData.bounds[1]) + ymin = min(self.VcutData.bounds[2],self.VengData.bounds[2]) + ymax = max(self.VcutData.bounds[3],self.VengData.bounds[3]) + self.Design_bounds = (xmin,xmax,ymin,ymax) + + + def Open_Settings_File(self,filename): + try: + fin = open(filename,'r') + except: + fmessage("Unable to open file: %s" %(filename)) + return + + text_codes=[] + ident = "k40_whisperer_set" + for line in fin: + try: + if ident in line: + # BOOL + if "include_Reng" in line: + self.include_Reng.set(line[line.find("include_Reng"):].split()[1]) + elif "include_Veng" in line: + self.include_Veng.set(line[line.find("include_Veng"):].split()[1]) + elif "include_Vcut" in line: + self.include_Vcut.set(line[line.find("include_Vcut"):].split()[1]) + elif "include_Gcde" in line: + self.include_Gcde.set(line[line.find("include_Gcde"):].split()[1]) + elif "include_Time" in line: + self.include_Time.set(line[line.find("include_Time"):].split()[1]) + elif "halftone" in line: + self.halftone.set(line[line.find("halftone"):].split()[1]) + elif "negate" in line: + self.negate.set(line[line.find("negate"):].split()[1]) + elif "HomeUR" in line: + self.HomeUR.set(line[line.find("HomeUR"):].split()[1]) + elif "inputCSYS" in line: + self.inputCSYS.set(line[line.find("inputCSYS"):].split()[1]) + elif "advanced" in line: + self.advanced.set(line[line.find("advanced"):].split()[1]) + elif "mirror" in line: + self.mirror.set(line[line.find("mirror"):].split()[1]) + elif "rotate" in line: + self.rotate.set(line[line.find("rotate"):].split()[1]) + elif "engraveUP" in line: + self.engraveUP.set(line[line.find("engraveUP"):].split()[1]) + elif "init_home" in line: + self.init_home.set(line[line.find("init_home"):].split()[1]) + elif "post_home" in line: + self.post_home.set(line[line.find("post_home"):].split()[1]) + elif "post_beep" in line: + self.post_beep.set(line[line.find("post_beep"):].split()[1]) + elif "post_disp" in line: + self.post_disp.set(line[line.find("post_disp"):].split()[1]) + elif "post_exec" in line: + self.post_exec.set(line[line.find("post_exec"):].split()[1]) + + elif "pre_pr_crc" in line: + self.pre_pr_crc.set(line[line.find("pre_pr_crc"):].split()[1]) + elif "inside_first" in line: + self.inside_first.set(line[line.find("inside_first"):].split()[1]) + elif "comb_engrave" in line: + self.comb_engrave.set(line[line.find("comb_engrave"):].split()[1]) + elif "comb_vector" in line: + self.comb_vector.set(line[line.find("comb_vector"):].split()[1]) + elif "zoom2image" in line: + self.zoom2image.set(line[line.find("zoom2image"):].split()[1]) + + elif "rotary" in line: + self.rotary.set(line[line.find("rotary"):].split()[1]) + elif "reduced_mem" in line: + self.reduced_mem.set(line[line.find("reduced_mem"):].split()[1]) + elif "wait" in line: + self.wait.set(line[line.find("wait"):].split()[1]) + + elif "trace_w_laser" in line: + self.trace_w_laser.set(line[line.find("trace_w_laser"):].split()[1]) + + # STRING.set() + elif "board_name" in line: + self.board_name.set(line[line.find("board_name"):].split()[1]) + elif "units" in line: + self.units.set(line[line.find("units"):].split()[1]) + elif "Reng_feed" in line: + self.Reng_feed .set(line[line.find("Reng_feed"):].split()[1]) + elif "Veng_feed" in line: + self.Veng_feed .set(line[line.find("Veng_feed"):].split()[1]) + elif "Vcut_feed" in line: + self.Vcut_feed.set(line[line.find("Vcut_feed"):].split()[1]) + elif "jog_step" in line: + self.jog_step.set(line[line.find("jog_step"):].split()[1]) + + elif "Reng_passes" in line: + self.Reng_passes.set(line[line.find("Reng_passes"):].split()[1]) + elif "Veng_passes" in line: + self.Veng_passes.set(line[line.find("Veng_passes"):].split()[1]) + elif "Vcut_passes" in line: + self.Vcut_passes.set(line[line.find("Vcut_passes"):].split()[1]) + elif "Gcde_passes" in line: + self.Gcde_passes.set(line[line.find("Gcde_passes"):].split()[1]) + + elif "rast_step" in line: + self.rast_step.set(line[line.find("rast_step"):].split()[1]) + elif "ht_size" in line: + self.ht_size.set(line[line.find("ht_size"):].split()[1]) + + elif "LaserXsize" in line: + self.LaserXsize.set(line[line.find("LaserXsize"):].split()[1]) + elif "LaserYsize" in line: + self.LaserYsize.set(line[line.find("LaserYsize"):].split()[1]) + + elif "LaserXscale" in line: + self.LaserXscale.set(line[line.find("LaserXscale"):].split()[1]) + elif "LaserYscale" in line: + self.LaserYscale.set(line[line.find("LaserYscale"):].split()[1]) + elif "LaserRscale" in line: + self.LaserRscale.set(line[line.find("LaserRscale"):].split()[1]) + + elif "rapid_feed" in line: + self.rapid_feed.set(line[line.find("rapid_feed"):].split()[1]) + + elif "gotoX" in line: + self.gotoX.set(line[line.find("gotoX"):].split()[1]) + elif "gotoY" in line: + self.gotoY.set(line[line.find("gotoY"):].split()[1]) + + elif "bezier_M1" in line: + self.bezier_M1.set(line[line.find("bezier_M1"):].split()[1]) + elif "bezier_M2" in line: + self.bezier_M2.set(line[line.find("bezier_M2"):].split()[1]) + elif "bezier_weight" in line: + self.bezier_weight.set(line[line.find("bezier_weight"):].split()[1]) + elif "trace_gap" in line: + self.trace_gap.set(line[line.find("trace_gap"):].split()[1]) + elif "trace_speed" in line: + self.trace_speed.set(line[line.find("trace_speed"):].split()[1]) + + ## elif "unsharp_flag" in line: + ## self.unsharp_flag.set(line[line.find("unsharp_flag"):].split()[1]) + ## elif "unsharp_r" in line: + ## self.unsharp_r.set(line[line.find("unsharp_r"):].split()[1]) + ## elif "unsharp_p" in line: + ## self.unsharp_p.set(line[line.find("unsharp_p"):].split()[1]) + ## elif "unsharp_t" in line: + ## self.unsharp_t.set(line[line.find("unsharp_t"):].split()[1]) + + elif "t_timeout" in line: + self.t_timeout.set(line[line.find("t_timeout"):].split()[1]) + elif "n_timeouts" in line: + self.n_timeouts.set(line[line.find("n_timeouts"):].split()[1]) + + elif "ink_timeout" in line: + self.ink_timeout.set(line[line.find("ink_timeout"):].split()[1]) + + elif "designfile" in line: + self.DESIGN_FILE=(line[line.find("designfile"):].split("\042")[1]) + elif "inkscape_path" in line: + self.inkscape_path.set(line[line.find("inkscape_path"):].split("\042")[1]) + elif "batch_path" in line: + self.batch_path.set(line[line.find("batch_path"):].split("\042")[1]) + + + except: + #Ignoring exeptions during reading data from line + pass + + fin.close() + + fileName, fileExtension = os.path.splitext(self.DESIGN_FILE) + init_file=os.path.basename(fileName) + + if init_file != "None": + if ( os.path.isfile(self.DESIGN_FILE) ): + pass + else: + self.statusMessage.set("Image file not found: %s " %(self.DESIGN_FILE)) + + if self.units.get() == 'in': + self.funits.set('in/min') + self.units_scale = 1.0 + else: + self.units.set('mm') + self.funits.set('mm/s') + self.units_scale = 25.4 + + temp_name, fileExtension = os.path.splitext(filename) + file_base=os.path.basename(temp_name) + + if self.initComplete == 1: + self.menu_Mode_Change() + self.DESIGN_FILE = filename + + ########################################################################## + ########################################################################## + def menu_File_Save(self): + settings_data = self.WriteConfig() + init_dir = os.path.dirname(self.DESIGN_FILE) + if ( not os.path.isdir(init_dir) ): + init_dir = self.HOME_DIR + + fileName, fileExtension = os.path.splitext(self.DESIGN_FILE) + init_file=os.path.basename(fileName) + + filename = asksaveasfilename(defaultextension='.txt', \ + filetypes=[("Text File","*.txt")],\ + initialdir=init_dir,\ + initialfile= init_file ) + + if filename != '' and filename != (): + try: + fout = open(filename,'w') + except: + self.statusMessage.set("Unable to open file for writing: %s" %(filename)) + self.statusbar.configure( bg = 'red' ) + return + + for line in settings_data: + try: + fout.write(line+'\n') + except: + fout.write('(skipping line)\n') + debug_message(traceback.format_exc()) + fout.close + self.statusMessage.set("File Saved: %s" %(filename)) + self.statusbar.configure( bg = 'white' ) + + def Get_Design_Bounds(self): + if self.rotate.get(): + ymin = self.Design_bounds[0] + ymax = self.Design_bounds[1] + xmin = -self.Design_bounds[3] + xmax = -self.Design_bounds[2] + else: + xmin,xmax,ymin,ymax = self.Design_bounds + return (xmin,xmax,ymin,ymax) + + def Move_UL(self,dummy=None): + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + if self.HomeUR.get(): + Xnew = self.laserX + (xmax-xmin) + DX = round((xmax-xmin)*1000.0) + else: + Xnew = self.laserX + DX = 0 + + (Xsize,Ysize)=self.LASER_Size() + if Xnew <= Xsize+.001: + self.move_head_window_temporary([DX,0.0]) + else: + pass + + def Move_UR(self,dummy=None): + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + if self.HomeUR.get(): + Xnew = self.laserX + DX = 0 + else: + Xnew = self.laserX + (xmax-xmin) + DX = round((xmax-xmin)*1000.0) + + (Xsize,Ysize)=self.LASER_Size() + if Xnew <= Xsize+.001: + self.move_head_window_temporary([DX,0.0]) + else: + pass + + def Move_LR(self,dummy=None): + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + if self.HomeUR.get(): + Xnew = self.laserX + DX = 0 + else: + Xnew = self.laserX + (xmax-xmin) + DX = round((xmax-xmin)*1000.0) + + Ynew = self.laserY - (ymax-ymin) + (Xsize,Ysize)=self.LASER_Size() + if Xnew <= Xsize+.001 and Ynew >= -Ysize-.001: + DY = round((ymax-ymin)*1000.0) + self.move_head_window_temporary([DX,-DY]) + else: + pass + + def Move_LL(self,dummy=None): + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + if self.HomeUR.get(): + Xnew = self.laserX + (xmax-xmin) + DX = round((xmax-xmin)*1000.0) + else: + Xnew = self.laserX + DX = 0 + + Ynew = self.laserY - (ymax-ymin) + (Xsize,Ysize)=self.LASER_Size() + if Xnew <= Xsize+.001 and Ynew >= -Ysize-.001: + DY = round((ymax-ymin)*1000.0) + self.move_head_window_temporary([DX,-DY]) + else: + pass + + def Move_CC(self,dummy=None): + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + if self.HomeUR.get(): + Xnew = self.laserX + (xmax-xmin)/2.0 + DX = round((xmax-xmin)/2.0*1000.0) + else: + Xnew = self.laserX + (xmax-xmin)/2.0 + DX = round((xmax-xmin)/2.0*1000.0) + + + Ynew = self.laserY - (ymax-ymin)/2.0 + (Xsize,Ysize)=self.LASER_Size() + if Xnew <= Xsize+.001 and Ynew >= -Ysize-.001: + DY = round((ymax-ymin)/2.0*1000.0) + self.move_head_window_temporary([DX,-DY]) + else: + pass + + def Move_Arbitrary(self,MoveX,MoveY,dummy=None): + if self.GUI_Disabled: + return + if self.HomeUR.get(): + DX = -MoveX + else: + DX = MoveX + DY = MoveY + NewXpos = self.pos_offset[0]+DX + NewYpos = self.pos_offset[1]+DY + self.move_head_window_temporary([NewXpos,NewYpos]) + + def Move_Arb_Step(self,dx,dy): + if self.GUI_Disabled: + return + if self.units.get()=="in": + dx_inches = round(dx*1000) + dy_inches = round(dy*1000) + else: + dx_inches = round(dx/25.4*1000) + dy_inches = round(dy/25.4*1000) + self.Move_Arbitrary( dx_inches,dy_inches ) + + def Move_Arb_Right(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Move_Arb_Step( JOG_STEP,0 ) + + def Move_Arb_Left(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Move_Arb_Step( -JOG_STEP,0 ) + + def Move_Arb_Up(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Move_Arb_Step( 0,JOG_STEP ) + + def Move_Arb_Down(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Move_Arb_Step( 0,-JOG_STEP ) + + #################################################### + + def Move_Right(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Rapid_Move( JOG_STEP,0 ) + + def Move_Right_10(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 10.0 + self.Rapid_Move( JOG_STEP,0 ) + + def Move_Right_100(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 100.0 + self.Rapid_Move( JOG_STEP,0 ) + + def Move_Left(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Rapid_Move( -JOG_STEP,0 ) + + def Move_Left_10(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 10.0 + self.Rapid_Move( -JOG_STEP,0 ) + + def Move_Left_100(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 100.0 + self.Rapid_Move( -JOG_STEP,0 ) + + def Move_Up(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Rapid_Move( 0,JOG_STEP ) + + def Move_Up_10(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 10.0 + self.Rapid_Move( 0,JOG_STEP ) + + def Move_Up_100(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 100.0 + self.Rapid_Move( 0,JOG_STEP ) + + def Move_Down(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) + self.Rapid_Move( 0,-JOG_STEP ) + + def Move_Down_10(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 10.0 + self.Rapid_Move( 0,-JOG_STEP ) + + def Move_Down_100(self,dummy=None): + JOG_STEP = float( self.jog_step.get() ) * 100.0 + self.Rapid_Move( 0,-JOG_STEP ) + + def Rapid_Move(self,dx,dy): + if self.GUI_Disabled: + return + if self.units.get()=="in": + dx_inches = round(dx,3) + dy_inches = round(dy,3) + else: + dx_inches = round(dx/25.4,3) + dy_inches = round(dy/25.4,3) + + if (self.HomeUR.get()): + dx_inches = -dx_inches + + Xnew,Ynew = self.XY_in_bounds(dx_inches,dy_inches) + dxmils = (Xnew - self.laserX)*1000.0 + dymils = (Ynew - self.laserY)*1000.0 + + if self.k40 == None: + self.laserX = Xnew + self.laserY = Ynew + self.menu_View_Refresh() + elif self.Send_Rapid_Move(dxmils,dymils): + self.laserX = Xnew + self.laserY = Ynew + self.menu_View_Refresh() + + + def Send_Rapid_Move(self,dxmils,dymils): + try: + if self.k40 != None: + Xscale = float(self.LaserXscale.get()) + Yscale = float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + + if Xscale != 1.0 or Yscale != 1.0: + dxmils = int(round(dxmils *Xscale)) + dymils = int(round(dymils *Yscale)) + self.k40.n_timeouts = 10 + + if self.rotary.get() and float(self.rapid_feed.get()): + self.slow_jog(int(dxmils),int(dymils)) + else: + self.k40.rapid_move(int(dxmils),int(dymils)) + + + return True + else: + return True + #except StandardError as e: + except Exception as e: + msg1 = "Rapid Move Failed: " + msg2 = "%s" %(e) + if msg2 == "": + formatted_lines = traceback.format_exc().splitlines() + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + debug_message(traceback.format_exc()) + return False + + + def slow_jog(self,dxmils,dymils): + if int(dxmils)==0 and int(dymils)==0: + return + self.stop[0]=False + Rapid_data=[] + Rapid_inst = egv(target=lambda s:Rapid_data.append(s)) + Rapid_feed = float(self.rapid_feed.get())*self.feed_factor() + Rapid_inst.make_egv_rapid(dxmils,dymils,Feed=Rapid_feed,board_name=self.board_name.get()) + self.send_egv_data(Rapid_data, 1, None) + self.stop[0]=True + + def update_gui(self, message=None, bgcolor='white'): + if message!=None: + self.statusMessage.set(message) + self.statusbar.configure( bg = bgcolor ) + self.master.update() + return True + + def set_gui(self,new_state="normal"): + if new_state=="normal": + self.GUI_Disabled=False + else: + self.GUI_Disabled=True + + try: + self.menuBar.entryconfigure("File" , state=new_state) + self.menuBar.entryconfigure("View" , state=new_state) + self.menuBar.entryconfigure("Tools" , state=new_state) + self.menuBar.entryconfigure("Settings", state=new_state) + self.menuBar.entryconfigure("Help" , state=new_state) + self.PreviewCanvas.configure(state=new_state) + + for w in self.master.winfo_children(): + try: + w.configure(state=new_state) + except: + pass + self.Stop_Button.configure(state="normal") + self.statusbar.configure(state="normal") + self.master.update() + except: + if DEBUG: + debug_message(traceback.format_exc()) + + def Vector_Cut(self, output_filename=None): + self.Prepare_for_laser_run("Vector Cut: Processing Vector Data.") + if self.VcutData.ecoords!=[]: + self.send_data("Vector_Cut", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No vector data to cut") + self.Finish_Job() + + def Vector_Eng(self, output_filename=None): + self.Prepare_for_laser_run("Vector Engrave: Processing Vector Data.") + if self.VengData.ecoords!=[]: + self.send_data("Vector_Eng", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No vector data to engrave") + self.Finish_Job() + + def Trace_Eng(self, output_filename=None): + self.Prepare_for_laser_run("Boundary Trace: Processing Data.") + self.trace_coords = self.make_trace_path() + + if self.trace_coords!=[]: + self.send_data("Trace_Eng", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No trace data to follow") + self.Finish_Job() + + def Raster_Eng(self, output_filename=None): + self.Prepare_for_laser_run("Raster Engraving: Processing Image Data.") + try: + self.make_raster_coords() + if self.RengData.ecoords!=[]: + self.send_data("Raster_Eng", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No raster data to engrave") + + except MemoryError as e: + msg1 = "Memory Error:" + msg2 = "Memory Error: Out of Memory." + self.statusMessage.set(msg2) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + except Exception as e: + msg1 = "Making Raster Data Stopped: " + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + self.Finish_Job() + + def Raster_Vector_Eng(self, output_filename=None): + self.Prepare_for_laser_run("Raster Engraving: Processing Image and Vector Data.") + try: + self.make_raster_coords() + if self.RengData.ecoords!=[] or self.VengData.ecoords!=[]: + self.send_data("Raster_Eng+Vector_Eng", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No data to engrave") + except Exception as e: + msg1 = "Preparing Data Stopped: " + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + self.Finish_Job() + + def Vector_Eng_Cut(self, output_filename=None): + self.Prepare_for_laser_run("Vector Cut: Processing Vector Data.") + if self.VcutData.ecoords!=[] or self.VengData.ecoords!=[]: + self.send_data("Vector_Eng+Vector_Cut", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No vector data.") + self.Finish_Job() + + def Raster_Vector_Cut(self, output_filename=None): + self.Prepare_for_laser_run("Raster Engraving: Processing Image and Vector Data.") + try: + self.make_raster_coords() + if self.RengData.ecoords!=[] or self.VengData.ecoords!=[] or self.VcutData.ecoords!=[]: + self.send_data("Raster_Eng+Vector_Eng+Vector_Cut", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No data to engrave/cut") + except Exception as e: + msg1 = "Preparing Data Stopped: " + msg2 = "%s" %(e) + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + self.Finish_Job() + + def Gcode_Cut(self, output_filename=None): + self.Prepare_for_laser_run("G Code Cutting.") + if self.GcodeData.ecoords!=[]: + self.send_data("Gcode_Cut", output_filename) + else: + self.statusbar.configure( bg = 'yellow' ) + self.statusMessage.set("No g-code data to cut") + self.Finish_Job() + + def Prepare_for_laser_run(self,msg): + self.stop[0]=False + self.move_head_window_temporary([0,0]) + self.set_gui("disabled") + self.statusbar.configure( bg = 'green' ) + self.statusMessage.set(msg) + self.master.update() + + def Finish_Job(self, event=None): + self.set_gui("normal") + self.stop[0]=True + if self.post_home.get(): + self.Unlock() + + if self.post_beep.get(): + self.master.bell() + + stderr = '' + stdout = '' + if self.post_exec.get(): + cmd = [self.batch_path.get()] + from subprocess import Popen, PIPE + startupinfo=None + proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE, stdin=PIPE, startupinfo=startupinfo) + stdout,stderr = proc.communicate() + + if self.post_disp.get() or stderr != '': + msg1 = '' + minutes = floor(self.run_time / 60) + seconds = self.run_time - minutes*60 + msg2 = "Job Ended.\nRun Time = %02d:%02d" %(minutes,seconds) + if stdout != '': + msg2=msg2+'\n\nBatch File Output:\n'+stdout + if stderr != '': + msg2=msg2+'\n\nBatch File Errors:\n'+stderr + self.run_time = 0 + message_box(msg1, msg2) + + + def make_trace_path(self): + my_hull = hull2D() + if self.inputCSYS.get() and self.RengData.image == None: + xmin,xmax,ymin,ymax = 0.0,0.0,0.0,0.0 + else: + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + + startx = xmin + starty = ymax + + ####################################### + Vcut_coords = self.VcutData.ecoords + Veng_coords = self.VengData.ecoords + Gcode_coords= self.GcodeData.ecoords + if self.mirror.get() or self.rotate.get(): + Vcut_coords = self.mirror_rotate_vector_coords(Vcut_coords) + Veng_coords = self.mirror_rotate_vector_coords(Veng_coords) + Gcode_coords= self.mirror_rotate_vector_coords(Gcode_coords) + + ####################################### + if self.RengData.ecoords==[]: + if self.stop[0] == True: + self.stop[0]=False + self.make_raster_coords() + self.stop[0]=True + else: + self.make_raster_coords() + + RengHullCoords = [] + Xscale = 1/float(self.LaserXscale.get()) + Yscale = 1/float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = 1/float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + + for point in self.RengData.hull_coords: + RengHullCoords.append([point[0]*Xscale+xmin, point[1]*Yscale, point[2]]) + + all_coords = [] + all_coords.extend(Vcut_coords) + all_coords.extend(Veng_coords) + all_coords.extend(Gcode_coords) + all_coords.extend(RengHullCoords) + + trace_coords=[] + if all_coords != []: + trace_coords = my_hull.convexHullecoords(all_coords) + gap = float(self.trace_gap.get())/self.units_scale + trace_coords = self.offset_eccords(trace_coords,gap) + + trace_coords,startx,starty = self.scale_vector_coords(trace_coords,startx,starty) + return trace_coords + + + ################################################################################ + def Sort_Paths(self,ecoords,i_loop=2): + ########################## + ### find loop ends ### + ########################## + Lbeg=[] + Lend=[] + if len(ecoords)>0: + Lbeg.append(0) + loop_old=ecoords[0][i_loop] + for i in range(1,len(ecoords)): + loop = ecoords[i][i_loop] + if loop != loop_old: + Lbeg.append(i) + Lend.append(i-1) + loop_old=loop + Lend.append(i) + + ####################################################### + # Find new order based on distance to next beg or end # + ####################################################### + order_out = [] + use_beg=0 + if len(ecoords)>0: + order_out.append([Lbeg[0],Lend[0]]) + inext = 0 + total=len(Lbeg) + for i in range(total-1): + if use_beg==1: + ii=Lbeg.pop(inext) + Lend.pop(inext) + else: + ii=Lend.pop(inext) + Lbeg.pop(inext) + + Xcur = ecoords[ii][0] + Ycur = ecoords[ii][1] + + dx = Xcur - ecoords[ Lbeg[0] ][0] + dy = Ycur - ecoords[ Lbeg[0] ][1] + min_dist = dx*dx + dy*dy + + dxe = Xcur - ecoords[ Lend[0] ][0] + dye = Ycur - ecoords[ Lend[0] ][1] + min_diste = dxe*dxe + dye*dye + + inext=0 + inexte=0 + for j in range(1,len(Lbeg)): + dx = Xcur - ecoords[ Lbeg[j] ][0] + dy = Ycur - ecoords[ Lbeg[j] ][1] + dist = dx*dx + dy*dy + if dist < min_dist: + min_dist=dist + inext=j + ### + dxe = Xcur - ecoords[ Lend[j] ][0] + dye = Ycur - ecoords[ Lend[j] ][1] + diste = dxe*dxe + dye*dye + if diste < min_diste: + min_diste=diste + inexte=j + ### + if min_diste < min_dist: + inext=inexte + order_out.append([Lend[inexte],Lbeg[inexte]]) + use_beg=1 + else: + order_out.append([Lbeg[inext],Lend[inext]]) + use_beg=0 + ########################################################### + return order_out + + ##################################################### + # determine if a point is inside a given polygon or not + # Polygon is a list of (x,y) pairs. + # http://www.ariel.com.au/a/python-point-int-poly.html + ##################################################### + def point_inside_polygon(self,x,y,poly): + n = len(poly) + inside = -1 + p1x = poly[0][0] + p1y = poly[0][1] + for i in range(n+1): + p2x = poly[i%n][0] + p2y = poly[i%n][1] + if y > min(p1y,p2y): + if y <= max(p1y,p2y): + if x <= max(p1x,p2x): + if p1y != p2y: + xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x + if p1x == p2x or x <= xinters: + inside = inside * -1 + p1x,p1y = p2x,p2y + + return inside + + def optimize_paths(self,ecoords,inside_check=True): + order_out = self.Sort_Paths(ecoords) + lastx=-999 + lasty=-999 + Acc=0.004 + cuts=[] + + for line in order_out: + temp=line + if temp[0] > temp[1]: + step = -1 + else: + step = 1 + + loop_old = -1 + + for i in range(temp[0],temp[1]+step,step): + x1 = ecoords[i][0] + y1 = ecoords[i][1] + loop = ecoords[i][2] + # check and see if we need to move to a new discontinuous start point + if (loop != loop_old): + dx = x1-lastx + dy = y1-lasty + dist = sqrt(dx*dx + dy*dy) + if dist > Acc: + cuts.append([[x1,y1]]) + else: + cuts[-1].append([x1,y1]) + else: + cuts[-1].append([x1,y1]) + lastx = x1 + lasty = y1 + loop_old = loop + + if inside_check: + ##################################################### + # For each loop determine if other loops are inside # + ##################################################### + Nloops=len(cuts) + self.LoopTree=[] + for iloop in range(Nloops): + self.LoopTree.append([]) + ## CUR_PCT=float(iloop)/Nloops*100.0 + ## if (not self.batch.get()): + ## self.statusMessage.set('Determining Which Side of Loop to Cut: %d of %d' %(iloop+1,Nloops)) + ## self.master.update() + ipoly = cuts[iloop] + ## Check points in other loops (could just check one) ## + if ipoly != []: + for jloop in range(Nloops): + if jloop != iloop: + inside = 0 + inside = inside + self.point_inside_polygon(cuts[jloop][0][0],cuts[jloop][0][1],ipoly) + if inside > 0: + self.LoopTree[iloop].append(jloop) + ##################################################### + for i in range(Nloops): + lns=[] + lns.append(i) + self.remove_self_references(lns,self.LoopTree[i]) + + self.order=[] + self.loops = list(range(Nloops)) + for i in range(Nloops): + if self.LoopTree[i]!=[]: + self.addlist(self.LoopTree[i]) + self.LoopTree[i]=[] + if self.loops[i]!=[]: + self.order.append(self.loops[i]) + self.loops[i]=[] + #END inside_check + ecoords_out = [] + for i in self.order: + line = cuts[i] + for coord in line: + ecoords_out.append([coord[0],coord[1],i]) + #END inside_check + else: + ecoords_out = [] + for i in range(len(cuts)): + line = cuts[i] + for coord in line: + ecoords_out.append([coord[0],coord[1],i]) + + return ecoords_out + + def remove_self_references(self,loop_numbers,loops): + for i in range(0,len(loops)): + for j in range(0,len(loop_numbers)): + if loops[i]==loop_numbers[j]: + loops.pop(i) + return + if self.LoopTree[loops[i]]!=[]: + loop_numbers.append(loops[i]) + self.remove_self_references(loop_numbers,self.LoopTree[loops[i]]) + + def addlist(self,list): + for i in list: + try: #this try/except is a bad hack fix to a recursion error. It should be fixed properly later. + if self.LoopTree[i]!=[]: + self.addlist(self.LoopTree[i]) #too many recursions here causes cmp error + self.LoopTree[i]=[] + except: + pass + if self.loops[i]!=[]: + self.order.append(self.loops[i]) + self.loops[i]=[] + + + def mirror_rotate_vector_coords(self,coords): + xmin = self.Design_bounds[0] + xmax = self.Design_bounds[1] + coords_rotate_mirror=[] + + for i in range(len(coords)): + coords_rotate_mirror.append(coords[i][:]) + if self.mirror.get(): + if self.inputCSYS.get() and self.RengData.image == None: + coords_rotate_mirror[i][0]=-coords_rotate_mirror[i][0] + else: + coords_rotate_mirror[i][0]=xmin+xmax-coords_rotate_mirror[i][0] + + + if self.rotate.get(): + x = coords_rotate_mirror[i][0] + y = coords_rotate_mirror[i][1] + coords_rotate_mirror[i][0] = -y + coords_rotate_mirror[i][1] = x + + return coords_rotate_mirror + + def scale_vector_coords(self,coords,startx,starty): + + Xscale = float(self.LaserXscale.get()) + Yscale = float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + + coords_scale=[] + if Xscale != 1.0 or Yscale != 1.0: + for i in range(len(coords)): + coords_scale.append(coords[i][:]) + x = coords_scale[i][0] + y = coords_scale[i][1] + coords_scale[i][0] = x*Xscale + coords_scale[i][1] = y*Yscale + scaled_startx = startx*Xscale + scaled_starty = starty*Yscale + else: + coords_scale = coords + scaled_startx = startx + scaled_starty = starty + + return coords_scale,scaled_startx,scaled_starty + + + def feed_factor(self): + if self.units.get()=='in': + feed_factor = 25.4/60.0 + else: + feed_factor = 1.0 + return feed_factor + + def send_data(self,operation_type=None, output_filename=None): + num_passes=0 + if self.k40 == None and output_filename == None: + self.statusMessage.set("Laser Cutter is not Initialized...") + self.statusbar.configure( bg = 'red' ) + return + try: + feed_factor=self.feed_factor() + + if self.inputCSYS.get() and self.RengData.image == None: + xmin,xmax,ymin,ymax = 0.0,0.0,0.0,0.0 + else: + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + + startx = xmin + starty = ymax + + if self.HomeUR.get(): + Xscale = float(self.LaserXscale.get()) + FlipXoffset = Xscale*abs(xmax-xmin) + if self.rotate.get(): + startx = -xmin + else: + FlipXoffset = 0 + + if self.rotary.get(): + Rapid_Feed = float(self.rapid_feed.get())*feed_factor + else: + Rapid_Feed = 0.0 + + Raster_Eng_data=[] + Vector_Eng_data=[] + Trace_Eng_data=[] + Vector_Cut_data=[] + G_code_Cut_data=[] + + if (operation_type.find("Vector_Cut") > -1) and (self.VcutData.ecoords!=[]): + Feed_Rate = float(self.Vcut_feed.get())*feed_factor + self.statusMessage.set("Vector Cut: Determining Cut Order....") + self.master.update() + if not self.VcutData.sorted and self.inside_first.get(): + self.VcutData.set_ecoords(self.optimize_paths(self.VcutData.ecoords),data_sorted=True) + + +## DEBUG_PLOT=False +## test_ecoords=self.VcutData.ecoords +## if DEBUG_PLOT: +## import matplotlib.pyplot as plt +## plt.ion() +## plt.clf() +## X=[] +## Y=[] +## LOOP_OLD = test_ecoords[0][2] +## for i in range(len(test_ecoords)): +## LOOP = test_ecoords[i][2] +## if LOOP != LOOP_OLD: +## plt.plot(X,Y) +## plt.pause(.5) +## X=[] +## Y=[] +## LOOP_OLD=LOOP +## X.append(test_ecoords[i][0]) +## Y.append(test_ecoords[i][1]) +## plt.plot(X,Y) + + + self.statusMessage.set("Generating EGV data...") + self.master.update() + + Vcut_coords = self.VcutData.ecoords + if self.mirror.get() or self.rotate.get(): + Vcut_coords = self.mirror_rotate_vector_coords(Vcut_coords) + + Vcut_coords,startx,starty = self.scale_vector_coords(Vcut_coords,startx,starty) + Vector_Cut_egv_inst = egv(target=lambda s:Vector_Cut_data.append(s)) + Vector_Cut_egv_inst.make_egv_data( + Vcut_coords, \ + startX=startx, \ + startY=starty, \ + Feed = Feed_Rate, \ + board_name=self.board_name.get(), \ + Raster_step = 0, \ + update_gui=self.update_gui, \ + stop_calc=self.stop, \ + FlipXoffset=FlipXoffset, \ + Rapid_Feed_Rate = Rapid_Feed, \ + use_laser=True + ) + + if (operation_type.find("Vector_Eng") > -1) and (self.VengData.ecoords!=[]): + Feed_Rate = float(self.Veng_feed.get())*feed_factor + self.statusMessage.set("Vector Engrave: Determining Cut Order....") + self.master.update() + if not self.VengData.sorted and self.inside_first.get(): + self.VengData.set_ecoords(self.optimize_paths(self.VengData.ecoords,inside_check=False),data_sorted=True) + self.statusMessage.set("Generating EGV data...") + self.master.update() + + Veng_coords = self.VengData.ecoords + if self.mirror.get() or self.rotate.get(): + Veng_coords = self.mirror_rotate_vector_coords(Veng_coords) + + Veng_coords,startx,starty = self.scale_vector_coords(Veng_coords,startx,starty) + Vector_Eng_egv_inst = egv(target=lambda s:Vector_Eng_data.append(s)) + Vector_Eng_egv_inst.make_egv_data( + Veng_coords, \ + startX=startx, \ + startY=starty, \ + Feed = Feed_Rate, \ + board_name=self.board_name.get(), \ + Raster_step = 0, \ + update_gui=self.update_gui, \ + stop_calc=self.stop, \ + FlipXoffset=FlipXoffset, \ + Rapid_Feed_Rate = Rapid_Feed, \ + use_laser=True + ) + + + if (operation_type.find("Trace_Eng") > -1) and (self.trace_coords!=[]): + Feed_Rate = float(self.trace_speed.get())*feed_factor + laser_on = self.trace_w_laser.get() + self.statusMessage.set("Generating EGV data...") + self.master.update() + Trace_Eng_egv_inst = egv(target=lambda s:Trace_Eng_data.append(s)) + Trace_Eng_egv_inst.make_egv_data( + self.trace_coords, \ + startX=startx, \ + startY=starty, \ + Feed = Feed_Rate, \ + board_name=self.board_name.get(), \ + Raster_step = 0, \ + update_gui=self.update_gui, \ + stop_calc=self.stop, \ + FlipXoffset=FlipXoffset, \ + Rapid_Feed_Rate = Rapid_Feed, \ + use_laser=laser_on + ) + + + if (operation_type.find("Raster_Eng") > -1) and (self.RengData.ecoords!=[]): + Feed_Rate = float(self.Reng_feed.get())*feed_factor + Raster_step = self.get_raster_step_1000in() + if not self.engraveUP.get(): + Raster_step = -Raster_step + + raster_startx = 0 + + Yscale = float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + raster_starty = Yscale*starty + + self.statusMessage.set("Generating EGV data...") + self.master.update() + Raster_Eng_egv_inst = egv(target=lambda s:Raster_Eng_data.append(s)) + Raster_Eng_egv_inst.make_egv_data( + self.RengData.ecoords, \ + startX=raster_startx, \ + startY=raster_starty, \ + Feed = Feed_Rate, \ + board_name=self.board_name.get(), \ + Raster_step = Raster_step, \ + update_gui=self.update_gui, \ + stop_calc=self.stop, \ + FlipXoffset=FlipXoffset, \ + Rapid_Feed_Rate = Rapid_Feed, \ + use_laser=True + ) + #print(len(Raster_Eng_data)) + Raster_Eng_data=Raster_Eng_egv_inst.strip_redundant_codes(Raster_Eng_data) + #print(len(Raster_Eng_data)) + + if (operation_type.find("Gcode_Cut") > -1) and (self.GcodeData.ecoords!=[]): + self.statusMessage.set("Generating EGV data...") + self.master.update() + Gcode_coords = self.GcodeData.ecoords + if self.mirror.get() or self.rotate.get(): + Gcode_coords = self.mirror_rotate_vector_coords(Gcode_coords) + + Gcode_coords,startx,starty = self.scale_vector_coords(Gcode_coords,startx,starty) + G_code_Cut_egv_inst = egv(target=lambda s:G_code_Cut_data.append(s)) + G_code_Cut_egv_inst.make_egv_data( + Gcode_coords, \ + startX=startx, \ + startY=starty, \ + Feed = None, \ + board_name=self.board_name.get(), \ + Raster_step = 0, \ + update_gui=self.update_gui, \ + stop_calc=self.stop, \ + FlipXoffset=FlipXoffset, \ + Rapid_Feed_Rate = Rapid_Feed, \ + use_laser=True + ) + + ### Join Resulting Data together ### + data=[] + data.append(ord("I")) + if Trace_Eng_data!=[]: + trace_passes=1 + for k in range(trace_passes): + if len(data)> 4: + data[-4]=ord("@") + data.extend(Trace_Eng_data) + if Raster_Eng_data!=[]: + num_passes = int(float(self.Reng_passes.get())) + for k in range(num_passes): + if len(data)> 4: + data[-4]=ord("@") + data.extend(Raster_Eng_data) + if Vector_Eng_data!=[]: + num_passes = int(float(self.Veng_passes.get())) + for k in range(num_passes): + if len(data)> 4: + data[-4]=ord("@") + data.extend(Vector_Eng_data) + if Vector_Cut_data!=[]: + num_passes = int(float(self.Vcut_passes.get())) + for k in range(num_passes): + if len(data)> 4: + data[-4]=ord("@") + data.extend(Vector_Cut_data) + if G_code_Cut_data!=[]: + num_passes = int(float(self.Gcde_passes.get())) + for k in range(num_passes): + if len(data)> 4: + data[-4]=ord("@") + data.extend(G_code_Cut_data) + if len(data)< 4: + raise Exception("No laser data was generated.") + + self.master.update() + if output_filename != None: + self.write_egv_to_file(data,output_filename) + else: + self.send_egv_data(data, 1, output_filename) + self.menu_View_Refresh() + + except MemoryError as e: + msg1 = "Memory Error:" + msg2 = "Memory Error: Out of Memory." + self.statusMessage.set(msg2) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + except Exception as e: + msg1 = "Sending Data Stopped: " + msg2 = "%s" %(e) + if msg2 == "": + formatted_lines = traceback.format_exc().splitlines() + self.statusMessage.set((msg1+msg2).split("\n")[0] ) + self.statusbar.configure( bg = 'red' ) + message_box(msg1, msg2) + debug_message(traceback.format_exc()) + + def send_egv_data(self,data,num_passes=1,output_filename=None): + pre_process_CRC = self.pre_pr_crc.get() + if self.k40 != None: + self.k40.timeout = int(float( self.t_timeout.get() )) + self.k40.n_timeouts = int(float( self.n_timeouts.get() )) + time_start = time() + self.k40.send_data(data,self.update_gui,self.stop,num_passes,pre_process_CRC, wait_for_laser=self.wait.get()) + self.run_time = time()-time_start + if DEBUG: + print(("Elapsed Time: %.6f" %(time()-time_start))) + + else: + self.statusMessage.set("Laser is not initialized.") + self.statusbar.configure( bg = 'yellow' ) + return + self.menu_View_Refresh() + + ########################################################################## + ########################################################################## + def write_egv_to_file(self,data,fname): + if len(data) == 0: + raise Exception("No data available to write to file.") + try: + fout = open(fname,'w') + except: + raise Exception("Unable to open file ( %s ) for writing." %(fname)) + fout.write("Document type : LHYMICRO-GL file\n") + fout.write("Creator-Software: K40 Whisperer\n") + + fout.write("\n") + fout.write("%0%0%0%0%") + for char_val in data: + char = chr(char_val) + fout.write("%s" %(char)) + + #fout.write("\n") + fout.close + self.menu_View_Refresh() + self.statusMessage.set("Data saved to: %s" %(fname)) + + def Home(self, event=None): + if self.GUI_Disabled: + return + if self.k40 != None: + self.k40.home_position() + self.laserX = 0.0 + self.laserY = 0.0 + self.pos_offset = [0.0,0.0] + self.menu_View_Refresh() + + def GoTo(self): + xpos = float(self.gotoX.get()) + ypos = float(self.gotoY.get()) + if self.k40 != None: + self.k40.home_position() + self.laserX = 0.0 + self.laserY = 0.0 + self.Rapid_Move(xpos,ypos) + self.menu_View_Refresh() + + def Reset(self): + if self.k40 != None: + try: + self.k40.reset_usb() + self.statusMessage.set("USB Reset Succeeded") + except: + debug_message(traceback.format_exc()) + pass + + def Stop(self,event=None): + if self.stop[0]==True: + return + line1 = "Sending data to the laser from K40 Whisperer is currently Paused." + line2 = "Press \"OK\" to abort any jobs currently running." + line3 = "Press \"Cancel\" to resume." + if self.k40 != None: + try: + self.k40.pause_un_pause() + except: + if message_ask_ok_cancel("Stop Laser Job.", "\n%s\n%s" %(line2,line3)): + self.stop[0]=True + + if message_ask_ok_cancel("Stop Laser Job.", "%s\n\n%s\n%s" %(line1,line2,line3)): + self.stop[0]=True + else: + if self.k40 != None: + self.k40.pause_un_pause() + + def Hide_Advanced(self,event=None): + self.advanced.set(0) + self.menu_View_Refresh() + + def Release_USB(self): + if self.k40 != None: + try: + self.k40.release_usb() + self.statusMessage.set("USB Release Succeeded") + except: + debug_message(traceback.format_exc()) + pass + self.k40=None + + def Initialize_Laser(self,event=None): + if self.GUI_Disabled: + return + self.stop[0]=True + self.Release_USB() + self.k40=None + self.move_head_window_temporary([0.0,0.0]) + self.k40=K40_CLASS() + try: + self.k40.initialize_device() + self.k40.say_hello() + if self.init_home.get(): + self.Home() + else: + self.Unlock() + + except Exception as e: + error_text = "%s" %(e) + if "BACKEND" in error_text.upper(): + error_text = error_text + " (libUSB driver not installed)" + self.statusMessage.set("USB Error: %s" %(error_text)) + self.statusbar.configure( bg = 'red' ) + self.k40=None + debug_message(traceback.format_exc()) + + except: + self.statusMessage.set("Unknown USB Error") + self.statusbar.configure( bg = 'red' ) + self.k40=None + debug_message(traceback.format_exc()) + + def Unfreeze_Laser(self,event=None): + if self.GUI_Disabled: + return + if self.k40 != None: + try: + self.k40.unfreeze() + self.statusMessage.set("Unfreeze Complete") + self.statusbar.configure( bg = 'white' ) + except: + pass + + def Unlock(self,event=None): + if self.GUI_Disabled: + return + if self.k40 != None: + try: + self.k40.unlock_rail() + self.statusMessage.set("Rail Unlock Succeeded") + self.statusbar.configure( bg = 'white' ) + except: + self.statusMessage.set("Rail Unlock Failed.") + self.statusbar.configure( bg = 'red' ) + debug_message(traceback.format_exc()) + pass + + ########################################################################## + ########################################################################## + + def menu_File_Quit(self): + if message_ask_ok_cancel("Exit", "Exiting...."): + self.Quit_Click(None) + + def Reset_RasterPath_and_Update_Time(self, varName=0, index=0, mode=0): + self.RengData.reset_path() + self.refreshTime() + + def View_Refresh_and_Reset_RasterPath(self, varName=0, index=0, mode=0): + self.RengData.reset_path() + self.SCALE = 0 + self.menu_View_Refresh() + + def menu_View_inputCSYS_Refresh_Callback(self, varName, index, mode): + self.move_head_window_temporary([0.0,0.0]) + self.SCALE = 0 + self.menu_View_Refresh() + + def menu_View_Refresh_Callback(self, varName=0, index=0, mode=0): + self.SCALE = 0 + self.menu_View_Refresh() + + if DEBUG: + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe, 2) + print('menu_View_Refresh_Callback called by: %s' %(calframe[1][3])) + + def menu_View_Refresh(self): + if DEBUG: + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe, 2) + print('menu_View_Refresh called by: %s' %(calframe[1][3])) + + try: + app.master.title(title_text+" "+ self.DESIGN_FILE) + except: + pass + dummy_event = Event() + dummy_event.widget=self.master + self.Master_Configure(dummy_event,1) + self.Plot_Data() + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + W = xmax-xmin + H = ymax-ymin + + if self.units.get()=="in": + X_display = self.laserX + self.pos_offset[0]/1000.0 + Y_display = self.laserY + self.pos_offset[1]/1000.0 + W_display = W + H_display = H + U_display = self.units.get() + else: + X_display = (self.laserX + self.pos_offset[0]/1000.0)*self.units_scale + Y_display = (self.laserY + self.pos_offset[1]/1000.0)*self.units_scale + W_display = W*self.units_scale + H_display = H*self.units_scale + U_display = self.units.get() + if self.HomeUR.get(): + X_display = -X_display + + self.statusMessage.set(" Current Position: X=%.3f Y=%.3f ( W X H )=( %.3f%s X %.3f%s ) " + %(X_display, + Y_display, + W_display, + U_display, + H_display, + U_display)) + + self.statusbar.configure( bg = 'white' ) + + def menu_Inside_First_Callback(self, varName, index, mode): + if self.GcodeData.ecoords != []: + if self.VcutData.sorted == True: + self.menu_Reload_Design() + elif self.VengData.sorted == True: + self.menu_Reload_Design() + + def menu_Mode_Change(self): + dummy_event = Event() + dummy_event.widget=self.master + self.Master_Configure(dummy_event,1) + + def menu_Calc_Raster_Time(self,event=None): + self.set_gui("disabled") + self.stop[0]=False + self.make_raster_coords() + self.stop[0]=True + self.refreshTime() + self.set_gui("normal") + self.menu_View_Refresh() + + + def menu_Help_About(self): + application="K40 Whisperer" + about = "%s Version %s\n\n" %(application,version) + about = about + "By Scorch.\n" + about = about + "\163\143\157\162\143\150\100\163\143\157\162" + about = about + "\143\150\167\157\162\153\163\056\143\157\155\n" + about = about + "https://www.scorchworks.com/\n\n" + try: + python_version = "%d.%d.%d" %(sys.version_info.major,sys.version_info.minor,sys.version_info.micro) + except: + python_version = "" + about = about + "Python "+python_version+" (%d bit)" %(struct.calcsize("P") * 8) + message_box("About %s" %(application),about) + + def menu_Help_Web(self): + webbrowser.open_new(r"https://www.scorchworks.com/K40whisperer/k40whisperer.html") + + def menu_Help_Manual(self): + webbrowser.open_new(r"https://www.scorchworks.com/K40whisperer/k40w_manual.html") + + def KEY_F1(self, event): + if self.GUI_Disabled: + return + self.menu_Help_About() + + def KEY_F2(self, event): + if self.GUI_Disabled: + return + self.GEN_Settings_Window() + + def KEY_F3(self, event): + if self.GUI_Disabled: + return + self.RASTER_Settings_Window() + + def KEY_F4(self, event): + if self.GUI_Disabled: + return + self.ROTARY_Settings_Window() + self.menu_View_Refresh() + + def KEY_F5(self, event): + if self.GUI_Disabled: + return + self.menu_View_Refresh() + + def KEY_F6(self, event): + if self.GUI_Disabled: + return + self.advanced.set(not self.advanced.get()) + self.menu_View_Refresh() + + def bindConfigure(self, event): + if not self.initComplete: + self.initComplete = 1 + self.menu_Mode_Change() + + def Master_Configure(self, event, update=0): + if event.widget != self.master: + return + x = int(self.master.winfo_x()) + y = int(self.master.winfo_y()) + w = int(self.master.winfo_width()) + h = int(self.master.winfo_height()) + if (self.x, self.y) == (-1,-1): + self.x, self.y = x,y + if abs(self.w-w)>10 or abs(self.h-h)>10 or update==1: + ################################################### + # Form changed Size (resized) adjust as required # + ################################################### + self.w=w + self.h=h + + if True: + # Left Column # + w_label=120 + w_entry=48 + w_units=52 + + x_label_L=10 + x_entry_L=x_label_L+w_label+20-5 + x_units_L=x_entry_L+w_entry+2 + + Yloc=10 + self.Initialize_Button.place (x=12, y=Yloc, width=130*2, height=23) + Yloc=Yloc+33 + + self.Open_Button.place (x=12, y=Yloc, width=130, height=40) + self.Reload_Button.place(x=12+130, y=Yloc, width=130, height=40) + if h>=660: + Yloc=Yloc+50 + self.separator1.place(x=x_label_L, y=Yloc,width=w_label+75+40+30, height=2) + Yloc=Yloc+6 + self.Label_Position_Control.place(x=x_label_L, y=Yloc, width=w_label*2, height=21) + + Yloc=Yloc+25 + self.Home_Button.place (x=12, y=Yloc, width=130, height=23) + self.UnLock_Button.place(x=12+130, y=Yloc, width=130, height=23) + + Yloc=Yloc+33 + self.Label_Step.place(x=x_label_L, y=Yloc, width=w_label, height=21) + self.Label_Step_u.place(x=x_units_L, y=Yloc, width=w_units, height=21) + self.Entry_Step.place(x=x_entry_L, y=Yloc, width=w_entry, height=23) + + ########################################################################### + Yloc=Yloc+30 + bsz=40 + xoffst=67 + + self.Up_Button_100.place (x=xoffst+12+bsz - bsz * 2 , y=Yloc, width=bsz * 5, height=bsz) + self.UL_Button.place (x=xoffst+12-bsz*2 , y=Yloc, width=bsz, height=bsz) + self.UR_Button.place (x=xoffst+12+bsz*4, y=Yloc, width=bsz, height=bsz) + + Yloc=Yloc+bsz + + self.Up_Button_10.place (x=xoffst+12+bsz - bsz , y=Yloc, width=bsz * 3, height=bsz) + + Yloc=Yloc+bsz + + self.Up_Button.place (x=xoffst+12+bsz , y=Yloc, width=bsz, height=bsz) + + Yloc=Yloc+bsz + + self.Left_Button.place (x=xoffst+12 ,y=Yloc, width=bsz, height=bsz) + self.Left_Button_10.place (x=xoffst+12-bsz ,y=Yloc-bsz, width=bsz, height=bsz*3) + self.Left_Button_100.place (x=xoffst+12-bsz*2 ,y=Yloc-bsz*2, width=bsz, height=bsz*5) + + self.CC_Button.place (x=xoffst+12+bsz ,y=Yloc, width=bsz, height=bsz) + + self.Right_Button.place (x=xoffst+12+bsz*2,y=Yloc, width=bsz, height=bsz) + self.Right_Button_10.place (x=xoffst+12+bsz*3,y=Yloc-bsz, width=bsz, height=bsz*3) + self.Right_Button_100.place (x=xoffst+12+bsz*4,y=Yloc-bsz*2, width=bsz, height=bsz*5) + + Yloc=Yloc+bsz + + self.Down_Button.place (x=xoffst+12+bsz , y=Yloc, width=bsz, height=bsz) + + Yloc=Yloc+bsz + + self.Down_Button_10.place (x=xoffst+12+bsz - bsz , y=Yloc, width=bsz * 3, height=bsz) + + Yloc=Yloc+bsz + + self.Down_Button_100.place (x=xoffst+12+bsz - bsz * 2 , y=Yloc, width=bsz * 5, height=bsz) + self.LL_Button.place (x=xoffst+12-bsz*2 , y=Yloc, width=bsz, height=bsz) + self.LR_Button.place (x=xoffst+12+bsz*4, y=Yloc, width=bsz, height=bsz) + + Yloc=Yloc+bsz + ########################################################################### + self.Label_GoToX.place(x=x_entry_L, y=Yloc, width=w_entry, height=23) + self.Label_GoToY.place(x=x_units_L, y=Yloc, width=w_entry, height=23) + Yloc=Yloc+25 + self.GoTo_Button.place (x=12, y=Yloc, width=100, height=23) + self.Entry_GoToX.place(x=x_entry_L, y=Yloc, width=w_entry, height=23) + self.Entry_GoToY.place(x=x_units_L, y=Yloc, width=w_entry, height=23) + ########################################################################### + else: + ########################################################################### + self.separator1.place_forget() + self.Label_Position_Control.place_forget() + ## + Yloc=Yloc+50 + self.separator1.place(x=x_label_L, y=Yloc,width=w_label+75+40+30, height=2) + Yloc=Yloc+6 + self.Home_Button.place (x=12, y=Yloc, width=130, height=23) + self.UnLock_Button.place(x=12+130, y=Yloc, width=130, height=23) + ## + self.Label_Step.place_forget() + self.Label_Step_u.place_forget() + self.Entry_Step.place_forget() + self.UL_Button.place_forget() + self.Up_Button.place_forget() + self.Up_Button_10.place_forget() + self.Up_Button_100.place_forget() + self.UR_Button.place_forget() + self.Left_Button.place_forget() + self.Left_Button_10.place_forget() + self.Left_Button_100.place_forget() + self.CC_Button.place_forget() + self.Right_Button.place_forget() + self.Right_Button_10.place_forget() + self.Right_Button_100.place_forget() + self.LL_Button.place_forget() + self.Down_Button.place_forget() + self.Down_Button_10.place_forget() + self.Down_Button_100.place_forget() + self.LR_Button.place_forget() + self.Label_GoToX.place_forget() + self.Label_GoToY.place_forget() + self.GoTo_Button.place_forget() + self.Entry_GoToX.place_forget() + self.Entry_GoToY.place_forget() + ########################################################################### + + #From Bottom up + BUinit = self.h-70 + Yloc = BUinit + self.Stop_Button.place (x=12, y=Yloc, width=130*2, height=30) + + self.Stop_Button.configure(bg='light coral') + Yloc=Yloc-10+10 + + wadv = 220 #200 + wadv_use = wadv-20 + Xvert_sep = 280 + Xadvanced = Xvert_sep+10 + w_label_adv= wadv-80 # 110 w_entry + + if self.GcodeData.ecoords == []: + self.Grun_Button.place_forget() + self.Reng_Veng_Vcut_Button.place_forget() + self.Reng_Veng_Button.place_forget() + self.Veng_Vcut_Button.place_forget() + + Yloc=Yloc-30 + self.Vcut_Button.place (x=12, y=Yloc, width=130, height=23) + self.Entry_Vcut_feed.place (x=x_entry_L, y=Yloc, width=w_entry, height=23) + self.Label_Vcut_feed_u.place(x=x_units_L, y=Yloc, width=w_units, height=23) + Y_Vcut=Yloc + + Yloc=Yloc-30 + self.Veng_Button.place (x=12, y=Yloc, width=130, height=23) + self.Entry_Veng_feed.place( x=x_entry_L, y=Yloc, width=w_entry, height=23) + self.Label_Veng_feed_u.place(x=x_units_L, y=Yloc, width=w_units, height=23) + Y_Veng=Yloc + + Yloc=Yloc-30 + self.Reng_Button.place (x=12, y=Yloc, width=130, height=23) + self.Entry_Reng_feed.place( x=x_entry_L, y=Yloc, width=w_entry, height=23) + self.Label_Reng_feed_u.place(x=x_units_L, y=Yloc, width=w_units, height=23) + Y_Reng=Yloc + + if self.comb_vector.get() or self.comb_engrave.get(): + if self.comb_engrave.get(): + self.Veng_Button.place_forget() + self.Reng_Button.place_forget() + if self.comb_vector.get(): + self.Vcut_Button.place_forget() + self.Veng_Button.place_forget() + + if self.comb_engrave.get(): + if self.comb_vector.get(): + self.Reng_Veng_Vcut_Button.place(x=12, y=Y_Reng, width=130, height=23*3+14) + else: + self.Reng_Veng_Button.place(x=12, y=Y_Reng, width=130, height=23*2+7) + elif self.comb_vector.get(): + self.Veng_Vcut_Button.place(x=12, y=Y_Veng, width=130, height=23*2+7) + + + else: + self.Vcut_Button.place_forget() + self.Entry_Vcut_feed.place_forget() + self.Label_Vcut_feed_u.place_forget() + + self.Veng_Button.place_forget() + self.Entry_Veng_feed.place_forget() + self.Label_Veng_feed_u.place_forget() + + self.Reng_Button.place_forget() + self.Entry_Reng_feed.place_forget() + self.Label_Reng_feed_u.place_forget() + + self.Reng_Veng_Vcut_Button.place_forget() + self.Reng_Veng_Button.place_forget() + self.Veng_Vcut_Button.place_forget() + + Yloc=Yloc-30 + self.Grun_Button.place (x=12, y=Yloc, width=130*2, height=23) + + if h>=560: + Yloc=Yloc-15 + self.separator2.place(x=x_label_L, y=Yloc,width=w_label+75+40+30, height=2) + else: + self.separator2.place_forget() + + # End Left Column # + + if self.advanced.get(): + + self.PreviewCanvas.configure( width = self.w-300-wadv, height = self.h-50 ) + self.PreviewCanvas_frame.place(x=280+wadv, y=10) + self.separator_vert.place(x=280, y=10,width=2, height=self.h-50) + + adv_Yloc=25-10 #15 + self.Label_Advanced_column.place(x=Xadvanced, y=adv_Yloc, width=wadv_use, height=21) + adv_Yloc=adv_Yloc+25 + self.separator_adv.place(x=Xadvanced, y=adv_Yloc,width=wadv_use, height=2) + + if h>=560: + adv_Yloc=adv_Yloc+25-20 #15 + self.Label_Halftone_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Halftone_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.Label_Negate_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Negate_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.separator_adv2.place(x=Xadvanced, y=adv_Yloc,width=wadv_use, height=2) + + adv_Yloc=adv_Yloc+25-20 + self.Label_Mirror_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Mirror_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.Label_Rotate_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Rotate_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.Label_inputCSYS_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_inputCSYS_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.separator_adv3.place(x=Xadvanced, y=adv_Yloc,width=wadv_use, height=2) + + adv_Yloc=adv_Yloc+25-20 + self.Label_Inside_First_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Inside_First_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc+25 + self.Label_Rotary_Enable_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Rotary_Enable_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + else: + #self.Label_Advanced_column.place_forget() + #self.separator_adv.place_forget() + self.Label_Halftone_adv.place_forget() + self.Checkbutton_Halftone_adv.place_forget() + self.Label_Negate_adv.place_forget() + self.Checkbutton_Negate_adv.place_forget() + self.separator_adv2.place_forget() + self.Label_Mirror_adv.place_forget() + self.Checkbutton_Mirror_adv.place_forget() + self.Label_Rotate_adv.place_forget() + self.Checkbutton_Rotate_adv.place_forget() + self.Label_inputCSYS_adv.place_forget() + self.Checkbutton_inputCSYS_adv.place_forget() + self.separator_adv3.place_forget() + self.Label_Inside_First_adv.place_forget() + self.Checkbutton_Inside_First_adv.place_forget() + self.Label_Rotary_Enable_adv.place_forget() + self.Checkbutton_Rotary_Enable_adv.place_forget() + + adv_Yloc = BUinit + self.Hide_Adv_Button.place (x=Xadvanced, y=adv_Yloc, width=wadv_use, height=30) + + if self.RengData.image != None: + self.Label_inputCSYS_adv.configure(state="disabled") + self.Checkbutton_inputCSYS_adv.place_forget() + else: + self.Label_inputCSYS_adv.configure(state="normal") + + if self.GcodeData.ecoords == []: + #adv_Yloc = adv_Yloc-40 + self.Label_Vcut_passes.place(x=Xadvanced, y=Y_Vcut, width=w_label_adv, height=21) + self.Entry_Vcut_passes.place(x=Xadvanced+w_label_adv+2, y=Y_Vcut, width=w_entry, height=23) + + #adv_Yloc=adv_Yloc-30 + self.Label_Veng_passes.place(x=Xadvanced, y=Y_Veng, width=w_label_adv, height=21) + self.Entry_Veng_passes.place(x=Xadvanced+w_label_adv+2, y=Y_Veng, width=w_entry, height=23) + + #adv_Yloc=adv_Yloc-30 + self.Label_Reng_passes.place(x=Xadvanced, y=Y_Reng, width=w_label_adv, height=21) + self.Entry_Reng_passes.place(x=Xadvanced+w_label_adv+2, y=Y_Reng, width=w_entry, height=23) + self.Label_Gcde_passes.place_forget() + self.Entry_Gcde_passes.place_forget() + adv_Yloc = Y_Reng + + #### + adv_Yloc=adv_Yloc-15 + self.separator_comb.place(x=Xadvanced-1, y=adv_Yloc, width=wadv_use, height=2) + + adv_Yloc=adv_Yloc-25 + self.Label_Comb_Vector_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Comb_Vector_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + + adv_Yloc=adv_Yloc-25 + self.Label_Comb_Engrave_adv.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Checkbutton_Comb_Engrave_adv.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=25, height=23) + #### + + else: + adv_Yloc=adv_Yloc-40 + self.Label_Gcde_passes.place(x=Xadvanced, y=adv_Yloc, width=w_label_adv, height=21) + self.Entry_Gcde_passes.place(x=Xadvanced+w_label_adv+2, y=adv_Yloc, width=w_entry, height=23) + self.Label_Vcut_passes.place_forget() + self.Entry_Vcut_passes.place_forget() + self.Label_Veng_passes.place_forget() + self.Entry_Veng_passes.place_forget() + self.Label_Reng_passes.place_forget() + self.Entry_Reng_passes.place_forget() + + else: + self.PreviewCanvas_frame.place_forget() + self.separator_vert.place_forget() + self.Label_Advanced_column.place_forget() + self.separator_adv.place_forget() + self.Label_Halftone_adv.place_forget() + self.Checkbutton_Halftone_adv.place_forget() + self.Label_Negate_adv.place_forget() + self.Checkbutton_Negate_adv.place_forget() + self.separator_adv2.place_forget() + self.Label_Mirror_adv.place_forget() + self.Checkbutton_Mirror_adv.place_forget() + self.Label_Rotate_adv.place_forget() + self.Checkbutton_Rotate_adv.place_forget() + self.Label_inputCSYS_adv.place_forget() + self.Checkbutton_inputCSYS_adv.place_forget() + self.separator_adv3.place_forget() + self.Label_Inside_First_adv.place_forget() + self.Checkbutton_Inside_First_adv.place_forget() + + self.Label_Rotary_Enable_adv.place_forget() + self.Checkbutton_Rotary_Enable_adv.place_forget() + + self.separator_comb.place_forget() + self.Label_Comb_Engrave_adv.place_forget() + self.Checkbutton_Comb_Engrave_adv.place_forget() + self.Label_Comb_Vector_adv.place_forget() + self.Checkbutton_Comb_Vector_adv.place_forget() + + + self.Entry_Vcut_passes.place_forget() + self.Label_Vcut_passes.place_forget() + self.Entry_Veng_passes.place_forget() + self.Label_Veng_passes.place_forget() + self.Entry_Reng_passes.place_forget() + self.Label_Reng_passes.place_forget() + self.Label_Gcde_passes.place_forget() + self.Entry_Gcde_passes.place_forget() + self.Hide_Adv_Button.place_forget() + + self.PreviewCanvas.configure( width = self.w-300, height = self.h-50 ) + self.PreviewCanvas_frame.place(x=Xvert_sep, y=10) + self.separator_vert.place_forget() + + self.Set_Input_States() + + self.Plot_Data() + + def Recalculate_RQD_Click(self, event): + self.menu_View_Refresh() + + def Set_Input_States(self): + pass + + def Set_Input_States_Event(self,event): + self.Set_Input_States() + + def Set_Input_States_RASTER(self,event=None): + if self.halftone.get(): + self.Label_Halftone_DPI.configure(state="normal") + self.Halftone_DPI_OptionMenu.configure(state="normal") + self.Label_Halftone_u.configure(state="normal") + self.Label_bezier_M1.configure(state="normal") + self.bezier_M1_Slider.configure(state="normal") + self.Label_bezier_M2.configure(state="normal") + self.bezier_M2_Slider.configure(state="normal") + self.Label_bezier_weight.configure(state="normal") + self.bezier_weight_Slider.configure(state="normal") + else: + self.Label_Halftone_DPI.configure(state="disabled") + self.Halftone_DPI_OptionMenu.configure(state="disabled") + self.Label_Halftone_u.configure(state="disabled") + self.Label_bezier_M1.configure(state="disabled") + self.bezier_M1_Slider.configure(state="disabled") + self.Label_bezier_M2.configure(state="disabled") + self.bezier_M2_Slider.configure(state="disabled") + self.Label_bezier_weight.configure(state="disabled") + self.bezier_weight_Slider.configure(state="disabled") + + def Set_Input_States_BATCH(self): + if self.post_exec.get(): + self.Entry_Batch_Path.configure(state="normal") + else: + self.Entry_Batch_Path.configure(state="disabled") +## def Set_Input_States_Unsharp(self,event=None): +## if self.unsharp_flag.get(): +## self.Label_Unsharp_Radius.configure(state="normal") +## self.Label_Unsharp_Radius_u.configure(state="normal") +## self.Entry_Unsharp_Radius.configure(state="normal") +## self.Label_Unsharp_Percent.configure(state="normal") +## self.Label_Unsharp_Percent_u.configure(state="normal") +## self.Entry_Unsharp_Percent.configure(state="normal") +## self.Label_Unsharp_Threshold.configure(state="normal") +## self.Entry_Unsharp_Threshold.configure(state="normal") +## +## else: +## self.Label_Unsharp_Radius.configure(state="disabled") +## self.Label_Unsharp_Radius_u.configure(state="disabled") +## self.Entry_Unsharp_Radius.configure(state="disabled") +## self.Label_Unsharp_Percent.configure(state="disabled") +## self.Label_Unsharp_Percent_u.configure(state="disabled") +## self.Entry_Unsharp_Percent.configure(state="disabled") +## self.Label_Unsharp_Threshold.configure(state="disabled") +## self.Entry_Unsharp_Threshold.configure(state="disabled") + + def Set_Input_States_Rotary(self,event=None): + if self.rotary.get(): + self.Label_Laser_R_Scale.configure(state="normal") + self.Entry_Laser_R_Scale.configure(state="normal") + self.Label_Laser_Rapid_Feed.configure(state="normal") + self.Label_Laser_Rapid_Feed_u.configure(state="normal") + self.Entry_Laser_Rapid_Feed.configure(state="normal") + else: + self.Label_Laser_R_Scale.configure(state="disabled") + self.Entry_Laser_R_Scale.configure(state="disabled") + self.Label_Laser_Rapid_Feed.configure(state="disabled") + self.Label_Laser_Rapid_Feed_u.configure(state="disabled") + self.Entry_Laser_Rapid_Feed.configure(state="disabled") + +# def Set_Input_States_RASTER_Event(self,event): +# self.Set_Input_States_RASTER() + + def Imaging_Free(self,image_in,bg="#ffffff"): + image_in = image_in.convert('L') + wim,him = image_in.size + image_out=PhotoImage(width=wim,height=him) + pixel=image_in.load() + if bg!=None: + image_out.put(bg, to=(0,0,wim,him)) + for y in range(0,him): + for x in range(0,wim): + val=pixel[x,y] + if val!=255: + image_out.put("#%02x%02x%02x" %(val,val,val),(x,y)) + return image_out + + ########################################## + # CANVAS PLOTTING STUFF # + ########################################## + def Plot_Data(self): + self.PreviewCanvas.delete(ALL) + self.calc_button.place_forget() + + for seg in self.segID: + self.PreviewCanvas.delete(seg) + self.segID = [] + + cszw = int(self.PreviewCanvas.cget("width")) + cszh = int(self.PreviewCanvas.cget("height")) + buff=10 + wc = float(cszw/2) + hc = float(cszh/2) + + maxx = float(self.LaserXsize.get()) / self.units_scale + minx = 0.0 + maxy = 0.0 + miny = -float(self.LaserYsize.get()) / self.units_scale + midx=(maxx+minx)/2 + midy=(maxy+miny)/2 + + + if self.inputCSYS.get() and self.RengData.image == None: + xmin,xmax,ymin,ymax = 0.0,0.0,0.0,0.0 + else: + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + + if (self.HomeUR.get()): + XlineShift = maxx - self.laserX - (xmax-xmin) + else: + XlineShift = self.laserX + YlineShift = self.laserY + + if min((xmax-xmin),(ymax-ymin)) > 0 and self.zoom2image.get(): + self.PlotScale = max((xmax-xmin)/(cszw-buff), (ymax-ymin)/(cszh-buff)) + x_lft = minx / self.PlotScale - self.laserX / self.PlotScale + (cszw-(xmax-xmin)/self.PlotScale)/2 + x_rgt = maxx / self.PlotScale - self.laserX / self.PlotScale + (cszw-(xmax-xmin)/self.PlotScale)/2 + y_bot = -miny / self.PlotScale + self.laserY / self.PlotScale + (cszh-(ymax-ymin)/self.PlotScale)/2 + y_top = -maxy / self.PlotScale + self.laserY / self.PlotScale + (cszh-(ymax-ymin)/self.PlotScale)/2 + self.segID.append( self.PreviewCanvas.create_rectangle( + x_lft, y_bot, x_rgt, y_top, fill="gray80", outline="gray80", width = 0) ) + else: + self.PlotScale = max((maxx-minx)/(cszw-buff), (maxy-miny)/(cszh-buff)) + x_lft = cszw/2 + (minx-midx) / self.PlotScale + x_rgt = cszw/2 + (maxx-midx) / self.PlotScale + y_bot = cszh/2 + (maxy-midy) / self.PlotScale + y_top = cszh/2 + (miny-midy) / self.PlotScale + self.segID.append( self.PreviewCanvas.create_rectangle( + x_lft, y_bot, x_rgt, y_top, fill="gray80", outline="gray80", width = 0) ) + + + ###################################### + ### Plot Raster Image ### + ###################################### + if self.RengData.image != None: + if self.include_Reng.get(): + try: + new_SCALE = (1.0/self.PlotScale)/self.input_dpi + if new_SCALE != self.SCALE: + self.SCALE = new_SCALE + nw=int(self.SCALE*self.wim) + nh=int(self.SCALE*self.him) + + plot_im = self.RengData.image.convert("L") +## if self.unsharp_flag.get(): +## from PIL import ImageFilter +## filter = ImageFilter.UnsharpMask() +## filter.radius = float(self.unsharp_r.get()) +## filter.percent = int(float(self.unsharp_p.get())) +## filter.threshold = int(float(self.unsharp_t.get())) +## plot_im = plot_im.filter(filter) + + if self.negate.get(): + plot_im = ImageOps.invert(plot_im) + + if self.halftone.get() == False: + plot_im = plot_im.point(lambda x: 0 if x<128 else 255, '1') + plot_im = plot_im.convert("L") + + if self.mirror.get(): + plot_im = ImageOps.mirror(plot_im) + + if self.rotate.get(): + plot_im = plot_im.rotate(90,expand=True) + nh=int(self.SCALE*self.wim) + nw=int(self.SCALE*self.him) + + try: + self.UI_image = ImageTk.PhotoImage(plot_im.resize((nw,nh), Image.ANTIALIAS)) + except: + debug_message("Imaging_Free Used.") + self.UI_image = self.Imaging_Free(plot_im.resize((nw,nh), Image.ANTIALIAS)) + except: + self.SCALE = 1 + debug_message(traceback.format_exc()) + + self.Plot_Raster(self.laserX+.001, self.laserY-.001, x_lft,y_top,self.PlotScale,im=self.UI_image) + else: + self.UI_image = None + + + ###################################### + ### Plot Reng Coords ### + ###################################### + if self.include_Rpth.get() and self.RengData.ecoords!=[]: + loop_old = -1 + + ##### + Xscale = 1/float(self.LaserXscale.get()) + Yscale = 1/float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = 1/float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + ###### + + for line in self.RengData.ecoords: + XY = line + x1 = XY[0]*Xscale + y1 = XY[1]*Yscale-ymax + loop = XY[2] + color = "black" + # check and see if we need to move to a new discontinuous start point + if (loop == loop_old): + self.Plot_Line(xold, yold, x1, y1, x_lft, y_top, XlineShift, YlineShift, self.PlotScale, color) + loop_old = loop + xold=x1 + yold=y1 + + + ###################################### + ### Plot Veng Coords ### + ###################################### + if self.include_Veng.get(): + loop_old = -1 + + + plot_coords = self.VengData.ecoords + if self.mirror.get() or self.rotate.get(): + plot_coords = self.mirror_rotate_vector_coords(plot_coords) + + for line in plot_coords: + XY = line + x1 = (XY[0]-xmin) + y1 = (XY[1]-ymax) + loop = XY[2] + # check and see if we need to move to a new discontinuous start point + if (loop == loop_old): + self.Plot_Line(xold, yold, x1, y1, x_lft, y_top, XlineShift, YlineShift, self.PlotScale, "blue") + loop_old = loop + xold=x1 + yold=y1 + + ###################################### + ### Plot Vcut Coords ### + ###################################### + if self.include_Vcut.get(): + loop_old = -1 + + plot_coords = self.VcutData.ecoords + if self.mirror.get() or self.rotate.get(): + plot_coords = self.mirror_rotate_vector_coords(plot_coords) + + for line in plot_coords: + XY = line + x1 = (XY[0]-xmin) + y1 = (XY[1]-ymax) + loop = XY[2] + # check and see if we need to move to a new discontinuous start point + if (loop == loop_old): + self.Plot_Line(xold, yold, x1, y1, x_lft, y_top, XlineShift, YlineShift, self.PlotScale, "red") + loop_old = loop + xold=x1 + yold=y1 + + ###################################### + ### Plot Gcode Coords ### + ###################################### + if self.include_Gcde.get(): + loop_old = -1 + scale=1 + + plot_coords = self.GcodeData.ecoords + if self.mirror.get() or self.rotate.get(): + plot_coords = self.mirror_rotate_vector_coords(plot_coords) + + for line in plot_coords: + XY = line + x1 = (XY[0]-xmin)*scale + y1 = (XY[1]-ymax)*scale + + loop = XY[2] + # check and see if we need to move to a new discontinuous start point + if (loop == loop_old): + self.Plot_Line(xold, yold, x1, y1, x_lft, y_top, XlineShift, YlineShift, self.PlotScale, "white") + loop_old = loop + xold=x1 + yold=y1 + + + ###################################### + ### Plot Trace Coords ### + ###################################### + if self.trace_window.winfo_exists(): # or DEBUG: + ##### + Xscale = 1/float(self.LaserXscale.get()) + Yscale = 1/float(self.LaserYscale.get()) + if self.rotary.get(): + Rscale = 1/float(self.LaserRscale.get()) + Yscale = Yscale*Rscale + ###### + trace_coords = self.make_trace_path() + for i in range(len(trace_coords)): + trace_coords[i]=[trace_coords[i][0]*Xscale,trace_coords[i][1]*Yscale,trace_coords[i][2]] + + for line in trace_coords: + XY = line + x1 = (XY[0]-xmin)*scale + y1 = (XY[1]-ymax)*scale + loop = XY[2] + # check and see if we need to move to a new discontinuous start point + if (loop == loop_old): + green = "#%02x%02x%02x" % (0, 200, 0) + self.Plot_Line(xold, yold, x1, y1, x_lft, y_top, XlineShift, YlineShift, + self.PlotScale, green, thick=2,tag_value=('LaserTag', 'trace')) + loop_old = loop + xold=x1 + yold=y1 + + + ###################################### + self.refreshTime() + dot_col = "grey50" + xoff = self.pos_offset[0]/1000.0 + yoff = self.pos_offset[1]/1000.0 + + if abs(self.pos_offset[0])+abs(self.pos_offset[1]) > 0: + head_offset=True + else: + head_offset=False + + self.Plot_circle(self.laserX+xoff,self.laserY+yoff,x_lft,y_top,self.PlotScale,dot_col,radius=5,cross_hair=head_offset) + + def Plot_Raster(self, XX, YY, Xleft, Ytop, PlotScale, im): + if (self.HomeUR.get()): + maxx = float(self.LaserXsize.get()) / self.units_scale + xmin,xmax,ymin,ymax = self.Get_Design_Bounds() + xplt = Xleft + ( maxx-XX-(xmax-xmin) )/PlotScale + else: + xplt = Xleft + XX/PlotScale + + yplt = Ytop - YY/PlotScale + self.segID.append( + self.PreviewCanvas.create_image(xplt, yplt, anchor=NW, image=self.UI_image,tags='LaserTag') + ) + + + def offset_eccords(self,ecoords_in,offset_val): + if not PYCLIPPER: + return ecoords_in + + loop_num = ecoords_in[0][2] + pco = pyclipper.PyclipperOffset() + ecoords_out=[] + pyclip_path = [] + for i in range(0,len(ecoords_in)): + pyclip_path.append([ecoords_in[i][0]*1000,ecoords_in[i][1]*1000]) + + pco.AddPath(pyclip_path, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) + try: + plot_coords = pco.Execute(offset_val*1000.0)[0] + plot_coords.append(plot_coords[0]) + except: + plot_coords=[] + + for i in range(0,len(plot_coords)): + ecoords_out.append([plot_coords[i][0]/1000.0,plot_coords[i][1]/1000.0,loop_num]) + return ecoords_out + + + def Plot_circle(self, XX, YY, Xleft, Ytop, PlotScale, col, radius=0, cross_hair=False): + circle_tags = ('LaserTag','LaserDot') + if (self.HomeUR.get()): + maxx = float(self.LaserXsize.get()) / self.units_scale + xplt = Xleft + maxx/PlotScale - XX/PlotScale + else: + xplt = Xleft + XX/PlotScale + yplt = Ytop - YY/PlotScale + + + if cross_hair: + radius=radius*2 + leg = int(radius*.707) + self.segID.append( + self.PreviewCanvas.create_polygon( + xplt-radius, + yplt, + xplt-leg, + yplt+leg, + xplt, + yplt+radius, + xplt+leg, + yplt+leg, + xplt+radius, + yplt, + xplt+leg, + yplt-leg, + xplt, + yplt-radius, + xplt-leg, + yplt-leg, + fill=col, outline=col, width = 1, stipple='gray12',tags=circle_tags )) + + self.segID.append( + self.PreviewCanvas.create_line( xplt-radius, + yplt, + xplt+radius, + yplt, + fill=col, capstyle="round", width = 1, tags=circle_tags )) + self.segID.append( + self.PreviewCanvas.create_line( xplt, + yplt-radius, + xplt, + yplt+radius, + fill=col, capstyle="round", width = 1, tags=circle_tags )) + else: + self.segID.append( + self.PreviewCanvas.create_oval( + xplt-radius, + yplt-radius, + xplt+radius, + yplt+radius, + fill=col, outline=col, width = 0, stipple='gray50',tags=circle_tags )) + + + def Plot_Line(self, XX1, YY1, XX2, YY2, Xleft, Ytop, XlineShift, YlineShift, PlotScale, col, thick=0, tag_value='LaserTag'): + xplt1 = Xleft + (XX1 + XlineShift )/PlotScale + xplt2 = Xleft + (XX2 + XlineShift )/PlotScale + yplt1 = Ytop - (YY1 + YlineShift )/PlotScale + yplt2 = Ytop - (YY2 + YlineShift )/PlotScale + + self.segID.append( + self.PreviewCanvas.create_line( xplt1, + yplt1, + xplt2, + yplt2, + fill=col, capstyle="round", width = thick, tags=tag_value) ) + + ################################################################################ + # Temporary Move Window # + ################################################################################ + def move_head_window_temporary(self,new_pos_offset): + if self.GUI_Disabled: + return + dx_inches = round(new_pos_offset[0]/1000.0,3) + dy_inches = round(new_pos_offset[1]/1000.0,3) + Xnew,Ynew = self.XY_in_bounds(dx_inches,dy_inches,no_size=True) + + pos_offset_X = round((Xnew-self.laserX)*1000.0) + pos_offset_Y = round((Ynew-self.laserY)*1000.0) + new_pos_offset = [pos_offset_X,pos_offset_Y] + + if self.inputCSYS.get() and self.RengData.image == None: + new_pos_offset = [0,0] + xdist = -self.pos_offset[0] + ydist = -self.pos_offset[1] + else: + xdist = -self.pos_offset[0] + new_pos_offset[0] + ydist = -self.pos_offset[1] + new_pos_offset[1] + + if self.k40 != None: + if self.Send_Rapid_Move( xdist,ydist ): + self.pos_offset = new_pos_offset + self.menu_View_Refresh() + else: + self.pos_offset = new_pos_offset + self.menu_View_Refresh() + + ################################################################################ + # General Settings Window # + ################################################################################ + def GEN_Settings_Window(self): + gen_width = 560 + gen_settings = Toplevel(width=gen_width, height=575) #460+75) + gen_settings.grab_set() # Use grab_set to prevent user input in the main window + gen_settings.focus_set() + gen_settings.resizable(0,0) + gen_settings.title('General Settings') + gen_settings.iconname("General Settings") + + D_Yloc = 6 + D_dY = 26 + xd_label_L = 12 + + w_label=150 + w_entry=40 + w_units=45 + xd_entry_L=xd_label_L+w_label+10 + xd_units_L=xd_entry_L+w_entry+5 + sep_border=10 + + #Radio Button + D_Yloc=D_Yloc+D_dY + self.Label_Units = Label(gen_settings,text="Units") + self.Label_Units.place(x=xd_label_L, y=D_Yloc, width=113, height=21) + self.Radio_Units_IN = Radiobutton(gen_settings,text="inch", value="in", + width="100", anchor=W) + self.Radio_Units_IN.place(x=w_label+22, y=D_Yloc, width=75, height=23) + self.Radio_Units_IN.configure(variable=self.units, command=self.Entry_units_var_Callback ) + self.Radio_Units_MM = Radiobutton(gen_settings,text="mm", value="mm", + width="100", anchor=W) + self.Radio_Units_MM.place(x=w_label+110, y=D_Yloc, width=75, height=23) + self.Radio_Units_MM.configure(variable=self.units, command=self.Entry_units_var_Callback ) + + D_Yloc=D_Yloc+D_dY + self.Label_init_home = Label(gen_settings,text="Home Upon Initialize") + self.Label_init_home.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_init_home = Checkbutton(gen_settings,text="", anchor=W) + self.Checkbutton_init_home.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_init_home.configure(variable=self.init_home) + + + D_Yloc=D_Yloc+D_dY + self.Label_post_home = Label(gen_settings,text="After Job Finishes:") + self.Label_post_home.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + + Xoption_width = 120 + Xoption_col1 = xd_entry_L + Xoption_col2 = xd_entry_L+Xoption_width + Xoption_col3 = xd_entry_L+Xoption_width*2 + + self.Checkbutton_post_home = Checkbutton(gen_settings,text="Unlock Rail", anchor=W) + self.Checkbutton_post_home.place(x=Xoption_col1, y=D_Yloc, width=Xoption_width, height=23) + self.Checkbutton_post_home.configure(variable=self.post_home) + + self.Checkbutton_post_beep = Checkbutton(gen_settings,text="Beep", anchor=W) + self.Checkbutton_post_beep.place(x=Xoption_col2, y=D_Yloc, width=Xoption_width, height=23) + self.Checkbutton_post_beep.configure(variable=self.post_beep) + + D_Yloc=D_Yloc+D_dY + self.Checkbutton_post_disp = Checkbutton(gen_settings,text="Popup Report", anchor=W) + self.Checkbutton_post_disp.place(x=Xoption_col1, y=D_Yloc, width=Xoption_width, height=23) + self.Checkbutton_post_disp.configure(variable=self.post_disp) + + self.Checkbutton_post_exec = Checkbutton(gen_settings,text="Run Batch File:", anchor=W, command=self.Set_Input_States_BATCH) + self.Checkbutton_post_exec.place(x=Xoption_col2, y=D_Yloc, width=Xoption_width, height=23) + self.Checkbutton_post_exec.configure(variable=self.post_exec) + + + self.Entry_Batch_Path = Entry(gen_settings) + self.Entry_Batch_Path.place(x=Xoption_col3, y=D_Yloc, width=Xoption_width, height=23) + self.Entry_Batch_Path.configure(textvariable=self.batch_path) + + + D_Yloc=D_Yloc+D_dY + self.Label_Preprocess_CRC = Label(gen_settings,text="Preprocess CRC Data") + self.Label_Preprocess_CRC.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Preprocess_CRC = Checkbutton(gen_settings,text="", anchor=W) + self.Checkbutton_Preprocess_CRC.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_Preprocess_CRC.configure(variable=self.pre_pr_crc) + + D_Yloc=D_Yloc+D_dY + self.Label_Reduce_Memory = Label(gen_settings,text="Reduce Memory Use") + self.Label_Reduce_Memory.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Reduce_Memory = Checkbutton(gen_settings,text="(needed for large designs or low memory computers)", anchor=W) + self.Checkbutton_Reduce_Memory.place(x=xd_entry_L, y=D_Yloc, width=350, height=23) + self.Checkbutton_Reduce_Memory.configure(variable=self.reduced_mem) + self.reduced_mem.trace_variable("w", self.Reduced_Memory_Callback) + + D_Yloc=D_Yloc+D_dY + self.Label_Wait = Label(gen_settings,text="Wait for Laser to Finish") + self.Label_Wait.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Wait = Checkbutton(gen_settings,text="(after all data has been sent over USB)", anchor=W) + self.Checkbutton_Wait.place(x=xd_entry_L, y=D_Yloc, width=350, height=23) + self.Checkbutton_Wait.configure(variable=self.wait) + #self.wait.trace_variable("w", self.Wait_Callback) + + #D_Yloc=D_Yloc+D_dY + #self.Label_Timeout = Label(gen_settings,text="USB Timeout") + #self.Label_Timeout.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + #self.Label_Timeout_u = Label(gen_settings,text="ms", anchor=W) + #self.Label_Timeout_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + #self.Entry_Timeout = Entry(gen_settings,width="15") + #self.Entry_Timeout.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + #self.Entry_Timeout.configure(textvariable=self.t_timeout) + #self.t_timeout.trace_variable("w", self.Entry_Timeout_Callback) + #self.entry_set(self.Entry_Timeout,self.Entry_Timeout_Check(),2) + + #D_Yloc=D_Yloc+D_dY + #self.Label_N_Timeouts = Label(gen_settings,text="Number of Timeouts") + #self.Label_N_Timeouts.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + #self.Entry_N_Timeouts = Entry(gen_settings,width="15") + #self.Entry_N_Timeouts.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + #self.Entry_N_Timeouts.configure(textvariable=self.n_timeouts) + #self.n_timeouts.trace_variable("w", self.Entry_N_Timeouts_Callback) + #self.entry_set(self.Entry_N_Timeouts,self.Entry_N_Timeouts_Check(),2) + + D_Yloc=D_Yloc+D_dY*1.25 + self.gen_separator1 = Frame(gen_settings, height=2, bd=1, relief=SUNKEN) + self.gen_separator1.place(x=xd_label_L, y=D_Yloc,width=gen_width-40, height=2) + + D_Yloc=D_Yloc+D_dY*.25 + self.Label_Inkscape_title = Label(gen_settings,text="Inkscape Options") + self.Label_Inkscape_title.place(x=xd_label_L, y=D_Yloc, width=gen_width-40, height=21) + + D_Yloc=D_Yloc+D_dY + font_entry_width=215 + self.Label_Inkscape_Path = Label(gen_settings,text="Inkscape Executable") + self.Label_Inkscape_Path.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_Inkscape_Path = Entry(gen_settings,width="15") + self.Entry_Inkscape_Path.place(x=xd_entry_L, y=D_Yloc, width=font_entry_width, height=23) + self.Entry_Inkscape_Path.configure(textvariable=self.inkscape_path) + self.Entry_Inkscape_Path.bind('', self.Inkscape_Path_Message) + self.Inkscape_Path = Button(gen_settings,text="Find Inkscape") + self.Inkscape_Path.place(x=xd_entry_L+font_entry_width+10, y=D_Yloc, width=110, height=23) + self.Inkscape_Path.bind("", self.Inkscape_Path_Click) + + D_Yloc=D_Yloc+D_dY + self.Label_Ink_Timeout = Label(gen_settings,text="Inkscape Timeout") + self.Label_Ink_Timeout.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Label_Ink_Timeout_u = Label(gen_settings,text="minutes", anchor=W) + self.Label_Ink_Timeout_u.place(x=xd_units_L, y=D_Yloc, width=w_units*2, height=21) + self.Entry_Ink_Timeout = Entry(gen_settings,width="15") + self.Entry_Ink_Timeout.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Ink_Timeout.configure(textvariable=self.ink_timeout) + self.ink_timeout.trace_variable("w", self.Entry_Ink_Timeout_Callback) + self.entry_set(self.Entry_Ink_Timeout,self.Entry_Ink_Timeout_Check(),2) + + D_Yloc=D_Yloc+D_dY*1.25 + self.gen_separator2 = Frame(gen_settings, height=2, bd=1, relief=SUNKEN) + self.gen_separator2.place(x=xd_label_L, y=D_Yloc,width=gen_width-40, height=2) + + D_Yloc=D_Yloc+D_dY*.5 + self.Label_no_com = Label(gen_settings,text="Home in Upper Right") + self.Label_no_com.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_no_com = Checkbutton(gen_settings,text="", anchor=W) + self.Checkbutton_no_com.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_no_com.configure(variable=self.HomeUR) + self.HomeUR.trace_variable("w",self.menu_View_Refresh_Callback) + + D_Yloc=D_Yloc+D_dY + self.Label_Board_Name = Label(gen_settings,text="Board Name", anchor=CENTER ) + self.Board_Name_OptionMenu = OptionMenu(gen_settings, self.board_name, + "LASER-M2", + "LASER-M1", + "LASER-M", + "LASER-B2", + "LASER-B1", + "LASER-B", + "LASER-A") + self.Label_Board_Name.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Board_Name_OptionMenu.place(x=xd_entry_L, y=D_Yloc, width=w_entry*3, height=23) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_Area_Width = Label(gen_settings,text="Laser Area Width") + self.Label_Laser_Area_Width.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Label_Laser_Area_Width_u = Label(gen_settings,textvariable=self.units, anchor=W) + self.Label_Laser_Area_Width_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + self.Entry_Laser_Area_Width = Entry(gen_settings,width="15") + self.Entry_Laser_Area_Width.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_Area_Width.configure(textvariable=self.LaserXsize) + self.LaserXsize.trace_variable("w", self.Entry_Laser_Area_Width_Callback) + self.entry_set(self.Entry_Laser_Area_Width,self.Entry_Laser_Area_Width_Check(),2) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_Area_Height = Label(gen_settings,text="Laser Area Height") + self.Label_Laser_Area_Height.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Label_Laser_Area_Height_u = Label(gen_settings,textvariable=self.units, anchor=W) + self.Label_Laser_Area_Height_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + self.Entry_Laser_Area_Height = Entry(gen_settings,width="15") + self.Entry_Laser_Area_Height.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_Area_Height.configure(textvariable=self.LaserYsize) + self.LaserYsize.trace_variable("w", self.Entry_Laser_Area_Height_Callback) + self.entry_set(self.Entry_Laser_Area_Height,self.Entry_Laser_Area_Height_Check(),2) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_X_Scale = Label(gen_settings,text="X Scale Factor") + self.Label_Laser_X_Scale.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_Laser_X_Scale = Entry(gen_settings,width="15") + self.Entry_Laser_X_Scale.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_X_Scale.configure(textvariable=self.LaserXscale) + self.LaserXscale.trace_variable("w", self.Entry_Laser_X_Scale_Callback) + self.entry_set(self.Entry_Laser_X_Scale,self.Entry_Laser_X_Scale_Check(),2) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_Y_Scale = Label(gen_settings,text="Y Scale Factor") + self.Label_Laser_Y_Scale.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_Laser_Y_Scale = Entry(gen_settings,width="15") + self.Entry_Laser_Y_Scale.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_Y_Scale.configure(textvariable=self.LaserYscale) + self.LaserYscale.trace_variable("w", self.Entry_Laser_Y_Scale_Callback) + self.entry_set(self.Entry_Laser_Y_Scale,self.Entry_Laser_Y_Scale_Check(),2) + + D_Yloc=D_Yloc+D_dY+10 + self.Label_SaveConfig = Label(gen_settings,text="Configuration File") + self.Label_SaveConfig.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + + self.GEN_SaveConfig = Button(gen_settings,text="Save") + self.GEN_SaveConfig.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=21, anchor="nw") + self.GEN_SaveConfig.bind("", self.Write_Config_File) + + ## Buttons ## + gen_settings.update_idletasks() + Ybut=int(gen_settings.winfo_height())-30 + Xbut=int(gen_settings.winfo_width()/2) + + self.GEN_Close = Button(gen_settings,text="Close") + self.GEN_Close.place(x=Xbut, y=Ybut, width=130, height=30, anchor="center") + self.GEN_Close.bind("", self.Close_Current_Window_Click) + + self.Set_Input_States_BATCH() + + ################################################################################ + # Raster Settings Window # + ################################################################################ + def RASTER_Settings_Window(self): + Wset=425+280 + Hset=330 #260 + raster_settings = Toplevel(width=Wset, height=Hset) + raster_settings.grab_set() # Use grab_set to prevent user input in the main window + raster_settings.focus_set() + raster_settings.resizable(0,0) + raster_settings.title('Raster Settings') + raster_settings.iconname("Raster Settings") + + D_Yloc = 6 + D_dY = 24 + xd_label_L = 12 + + w_label=155 + w_entry=60 + w_units=35 + xd_entry_L=xd_label_L+w_label+10 + xd_units_L=xd_entry_L+w_entry+5 + + D_Yloc=D_Yloc+D_dY + self.Label_Rstep = Label(raster_settings,text="Scanline Step", anchor=CENTER ) + self.Label_Rstep.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Label_Rstep_u = Label(raster_settings,text="in", anchor=W) + self.Label_Rstep_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + self.Entry_Rstep = Entry(raster_settings,width="15") + self.Entry_Rstep.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Rstep.configure(textvariable=self.rast_step) + self.rast_step.trace_variable("w", self.Entry_Rstep_Callback) + + D_Yloc=D_Yloc+D_dY + self.Label_EngraveUP = Label(raster_settings,text="Engrave Bottom Up") + self.Checkbutton_EngraveUP = Checkbutton(raster_settings,text=" ", anchor=W) + self.Checkbutton_EngraveUP.configure(variable=self.engraveUP) + self.Label_EngraveUP.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_EngraveUP.place(x=w_label+22, y=D_Yloc, width=75, height=23) + + D_Yloc=D_Yloc+D_dY + self.Label_Halftone = Label(raster_settings,text="Halftone (Dither)") + self.Label_Halftone.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Halftone = Checkbutton(raster_settings,text=" ", anchor=W, command=self.Set_Input_States_RASTER) + self.Checkbutton_Halftone.place(x=w_label+22, y=D_Yloc, width=75, height=23) + self.Checkbutton_Halftone.configure(variable=self.halftone) + self.halftone.trace_variable("w", self.menu_View_Refresh_Callback) + + ############ + D_Yloc=D_Yloc+D_dY + self.Label_Halftone_DPI = Label(raster_settings,text="Halftone Resolution", anchor=CENTER ) + + if self.reduced_mem.get(): + if self.ht_size == "1000": self.ht_size = "500" + if self.ht_size == "333": self.ht_size = "500" + if self.ht_size == "200": self.ht_size = "250" + if self.ht_size == "143": self.ht_size = "167" + self.Halftone_DPI_OptionMenu = OptionMenu(raster_settings, self.ht_size, + "500", + "250", + "167", + "125") + else: + self.Halftone_DPI_OptionMenu = OptionMenu(raster_settings, self.ht_size, + "1000", + "500", + "333", + "250", + "200", + "167", + "143", + "125") + + self.Label_Halftone_DPI.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Halftone_DPI_OptionMenu.place(x=xd_entry_L, y=D_Yloc, width=w_entry+30, height=23) + + self.Label_Halftone_u = Label(raster_settings,text="dpi", anchor=W) + self.Label_Halftone_u.place(x=xd_units_L+30, y=D_Yloc, width=w_units, height=21) + + ############ + D_Yloc=D_Yloc+D_dY+5 + self.Label_bezier_M1 = Label(raster_settings, + text="Slope, Black (%.1f)"%(self.bezier_M1_default), + anchor=CENTER ) + self.bezier_M1_Slider = Scale(raster_settings, from_=1, to=50, resolution=0.1, \ + orient=HORIZONTAL, variable=self.bezier_M1) + self.bezier_M1_Slider.place(x=xd_entry_L, y=D_Yloc, width=(Wset-xd_entry_L-25-280 )) + D_Yloc=D_Yloc+21 + self.Label_bezier_M1.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.bezier_M1.trace_variable("w", self.bezier_M1_Callback) + + D_Yloc=D_Yloc+D_dY-8 + self.Label_bezier_M2 = Label(raster_settings, + text="Slope, White (%.2f)"%(self.bezier_M2_default), + anchor=CENTER ) + self.bezier_M2_Slider = Scale(raster_settings, from_=0.0, to=1, \ + orient=HORIZONTAL,resolution=0.01, variable=self.bezier_M2) + self.bezier_M2_Slider.place(x=xd_entry_L, y=D_Yloc, width=(Wset-xd_entry_L-25-280 )) + D_Yloc=D_Yloc+21 + self.Label_bezier_M2.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.bezier_M2.trace_variable("w", self.bezier_M2_Callback) + + D_Yloc=D_Yloc+D_dY-8 + self.Label_bezier_weight = Label(raster_settings, + text="Transition (%.1f)"%(self.bezier_M1_default), + anchor=CENTER ) + self.bezier_weight_Slider = Scale(raster_settings, from_=0, to=10, resolution=0.1, \ + orient=HORIZONTAL, variable=self.bezier_weight) + self.bezier_weight_Slider.place(x=xd_entry_L, y=D_Yloc, width=(Wset-xd_entry_L-25-280 )) + D_Yloc=D_Yloc+21 + self.Label_bezier_weight.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.bezier_weight.trace_variable("w", self.bezier_weight_Callback) + +## show_unsharp = False +## if DEBUG and show_unsharp: +## D_Yloc=D_Yloc+D_dY +## self.Label_UnsharpMask = Label(raster_settings,text="Unsharp Mask") +## self.Label_UnsharpMask.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) +## self.Checkbutton_UnsharpMask = Checkbutton(raster_settings,text=" ", anchor=W, command=self.Set_Input_States_Unsharp) +## self.Checkbutton_UnsharpMask.place(x=w_label+22, y=D_Yloc, width=75, height=23) +## self.Checkbutton_UnsharpMask.configure(variable=self.unsharp_flag) +## self.unsharp_flag.trace_variable("w", self.menu_View_Refresh_Callback) +## +## D_Yloc=D_Yloc+D_dY +## self.Label_Unsharp_Radius = Label(raster_settings,text="Unsharp Mask Radius", anchor=CENTER ) +## self.Label_Unsharp_Radius.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) +## self.Label_Unsharp_Radius_u = Label(raster_settings,text="Pixels", anchor=W) +## self.Label_Unsharp_Radius_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) +## self.Entry_Unsharp_Radius = Entry(raster_settings,width="15") +## self.Entry_Unsharp_Radius.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) +## self.Entry_Unsharp_Radius.configure(textvariable=self.unsharp_r) +## self.unsharp_r.trace_variable("w", self.Entry_Unsharp_Radius_Callback) +## +## D_Yloc=D_Yloc+D_dY +## self.Label_Unsharp_Percent = Label(raster_settings,text="Unsharp Mask Percent", anchor=CENTER ) +## self.Label_Unsharp_Percent.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) +## self.Label_Unsharp_Percent_u = Label(raster_settings,text="%", anchor=W) +## self.Label_Unsharp_Percent_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) +## self.Entry_Unsharp_Percent = Entry(raster_settings,width="15") +## self.Entry_Unsharp_Percent.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) +## self.Entry_Unsharp_Percent.configure(textvariable=self.unsharp_p) +## self.unsharp_p.trace_variable("w", self.Entry_Unsharp_Percent_Callback) +## +## D_Yloc=D_Yloc+D_dY +## self.Label_Unsharp_Threshold = Label(raster_settings,text="Unsharp Mask Threshold", anchor=CENTER ) +## self.Label_Unsharp_Threshold.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) +## #self.Label_Unsharp_Threshold_u = Label(raster_settings,text="Pixels", anchor=W) +## #self.Label_Unsharp_Threshold_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) +## self.Entry_Unsharp_Threshold = Entry(raster_settings,width="15") +## self.Entry_Unsharp_Threshold.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) +## self.Entry_Unsharp_Threshold.configure(textvariable=self.unsharp_t) +## self.unsharp_t.trace_variable("w", self.Entry_Unsharp_Threshold_Callback) + + # Bezier Canvas + self.Bezier_frame = Frame(raster_settings, bd=1, relief=SUNKEN) + self.Bezier_frame.place(x=Wset-280, y=10, height=265, width=265) + self.BezierCanvas = Canvas(self.Bezier_frame, background="white") + self.BezierCanvas.pack(side=LEFT, fill=BOTH, expand=1) + self.BezierCanvas.create_line( 5,260-0,260,260-255,fill="grey75", capstyle="round", width = 2, tags='perm') + + + M1 = self.bezier_M1_default + M2 = self.bezier_M2_default + w = self.bezier_weight_default + num = 10 + x,y = self.generate_bezier(M1,M2,w,n=num) + for i in range(0,num): + self.BezierCanvas.create_line( 5+x[i],260-y[i],5+x[i+1],260-y[i+1],fill="grey85", stipple='gray25',\ + capstyle="round", width = 2, tags='perm') + + + ## Buttons ## + raster_settings.update_idletasks() + Ybut=int(raster_settings.winfo_height())-30 + Xbut=int(raster_settings.winfo_width()/2) + + self.RASTER_Close = Button(raster_settings,text="Close") + self.RASTER_Close.place(x=Xbut, y=Ybut, width=130, height=30, anchor="center") + self.RASTER_Close.bind("", self.Close_Current_Window_Click) + + self.bezier_M1_Callback() + self.Set_Input_States_RASTER() + #if DEBUG and show_unsharp: + # self.Set_Input_States_Unsharp() + + + ################################################################################ + # Rotary Settings Window # + ################################################################################ + def ROTARY_Settings_Window(self): + rotary_settings = Toplevel(width=350, height=175) + rotary_settings.grab_set() # Use grab_set to prevent user input in the main window + rotary_settings.focus_set() + rotary_settings.resizable(0,0) + rotary_settings.title('Rotary Settings') + rotary_settings.iconname("Rotary Settings") + + D_Yloc = 6 + D_dY = 30 + xd_label_L = 12 + + w_label=180 + w_entry=40 + w_units=45 + xd_entry_L=xd_label_L+w_label+10 + xd_units_L=xd_entry_L+w_entry+5 + sep_border=10 + + + D_Yloc=D_Yloc+D_dY-15 + self.Label_Rotary_Enable = Label(rotary_settings,text="Use Rotary Settings") + self.Label_Rotary_Enable.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Rotary_Enable = Checkbutton(rotary_settings,text="", anchor=W, command=self.Set_Input_States_Rotary) + self.Checkbutton_Rotary_Enable.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_Rotary_Enable.configure(variable=self.rotary) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_R_Scale = Label(rotary_settings,text="Rotary Scale Factor (Y axis)") + self.Label_Laser_R_Scale.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_Laser_R_Scale = Entry(rotary_settings,width="15") + self.Entry_Laser_R_Scale.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_R_Scale.configure(textvariable=self.LaserRscale) + self.LaserRscale.trace_variable("w", self.Entry_Laser_R_Scale_Callback) + self.entry_set(self.Entry_Laser_R_Scale,self.Entry_Laser_R_Scale_Check(),2) + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_Rapid_Feed = Label(rotary_settings,text="Rapid Speed (default=0)") + self.Label_Laser_Rapid_Feed.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Label_Laser_Rapid_Feed_u = Label(rotary_settings,textvariable=self.funits, anchor=W) + self.Label_Laser_Rapid_Feed_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + self.Entry_Laser_Rapid_Feed = Entry(rotary_settings,width="15") + self.Entry_Laser_Rapid_Feed.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_Laser_Rapid_Feed.configure(textvariable=self.rapid_feed) + self.rapid_feed.trace_variable("w", self.Entry_Laser_Rapid_Feed_Callback) + self.entry_set(self.Entry_Laser_Rapid_Feed,self.Entry_Laser_Rapid_Feed_Check(),2) + + ## Buttons ## + rotary_settings.update_idletasks() + Ybut=int(rotary_settings.winfo_height())-30 + Xbut=int(rotary_settings.winfo_width()/2) + + self.GEN_Close = Button(rotary_settings,text="Close") + self.GEN_Close.place(x=Xbut, y=Ybut, width=130, height=30, anchor="center") + self.GEN_Close.bind("", self.Close_Current_Window_Click) + + self.Set_Input_States_Rotary() + + ################################################################################ + # Toggle Fullscreen # + ################################################################################ + + def Toggle_Fullscreen(self, dummy=None): + if (root.attributes('-fullscreen')): + root.attributes('-fullscreen', False) + else: + root.attributes('-fullscreen', True) + + ################################################################################ + # Trace Send Window # + ################################################################################ + + def TRACE_Settings_Window(self, dummy=None): + if self.GUI_Disabled: + return + trace_window = Toplevel(width=350, height=180) + self.trace_window=trace_window + trace_window.grab_set() # Use grab_set to prevent user input in the main window during calculations + trace_window.resizable(0,0) + trace_window.title('Trace Boundary') + trace_window.iconname("Trace Boundary") + + def Close_Click(): + win_id=self.grab_current() + self.PreviewCanvas.delete('trace') + win_id.destroy() + + def Close_and_Send_Click(): + win_id=self.grab_current() + self.PreviewCanvas.delete('trace') + win_id.destroy() + self.Trace_Eng() + + D_Yloc = 0 + D_dY = 28 + xd_label_L = 12 + + w_label=225 + w_entry=40 + w_units=50 + xd_entry_L=xd_label_L+w_label+10 + xd_units_L=xd_entry_L+w_entry+5 + + D_Yloc=D_Yloc+D_dY + self.Label_Laser_Trace = Label(trace_window,text="Laser 'On' During Trace") + self.Label_Laser_Trace.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Laser_Trace = Checkbutton(trace_window,text="", anchor=W) + self.Checkbutton_Laser_Trace.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_Laser_Trace.configure(variable=self.trace_w_laser) + + D_Yloc=D_Yloc+D_dY + self.Label_Trace_Gap = Label(trace_window,text="Gap Between Design and Trace") + self.Label_Trace_Gap.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_Trace_Gap = Entry(trace_window,width="15") + self.Entry_Trace_Gap.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Label_Trace_Gap_u = Label(trace_window,textvariable=self.units, anchor=W) + self.Label_Trace_Gap_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + self.Entry_Trace_Gap.configure(textvariable=self.trace_gap,justify='center') + self.trace_gap.trace_variable("w", self.Entry_Trace_Gap_Callback) + self.entry_set(self.Entry_Trace_Gap,self.Entry_Trace_Gap_Check(),2) + if not PYCLIPPER: + self.Label_Trace_Gap.configure(state="disabled") + self.Label_Trace_Gap_u.configure(state="disabled") + self.Entry_Trace_Gap.configure(state="disabled") + + D_Yloc=D_Yloc+D_dY + self.Trace_Button = Button(trace_window,text="Trace Boundary With Laser Head",command=Close_and_Send_Click) + self.Trace_Button.place(x=xd_label_L, y=D_Yloc, width=w_label, height=23) + + self.Entry_Trace_Speed = Entry(trace_window,width="15") + self.Entry_Trace_Speed.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + green = "#%02x%02x%02x" % (0, 200, 0) + self.Entry_Trace_Speed.configure(textvariable=self.trace_speed,justify='center',fg=green) + self.trace_speed.trace_variable("w", self.Entry_Trace_Speed_Callback) + self.entry_set(self.Entry_Trace_Speed,self.Entry_Trace_Speed_Check(),2) + self.Label_Trace_Speed_u = Label(trace_window,textvariable=self.funits, anchor=W) + self.Label_Trace_Speed_u.place(x=xd_units_L, y=D_Yloc, width=w_units, height=21) + + + ## Buttons ## + trace_window.update_idletasks() + Ybut=int(trace_window.winfo_height())-30 + Xbut=int(trace_window.winfo_width()/2) + + self.Trace_Close = Button(trace_window,text="Cancel",command=Close_Click) + self.Trace_Close.place(x=Xbut, y=Ybut, width=130, height=30, anchor="center") + ################################################################################ + + ################################################################################ + # EGV Send Window # + ################################################################################ + def EGV_Send_Window(self,EGV_filename): + + egv_send = Toplevel(width=400, height=180) + egv_send.grab_set() # Use grab_set to prevent user input in the main window during calculations + egv_send.resizable(0,0) + egv_send.title('EGV Send') + egv_send.iconname("EGV Send") + + D_Yloc = 0 + D_dY = 28 + xd_label_L = 12 + + w_label=150 + w_entry=40 + w_units=35 + xd_entry_L=xd_label_L+w_label+10 + xd_units_L=xd_entry_L+w_entry+5 + + D_Yloc=D_Yloc+D_dY + self.Label_Preprocess_CRC = Label(egv_send,text="Preprocess CRC Data") + self.Label_Preprocess_CRC.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Checkbutton_Preprocess_CRC = Checkbutton(egv_send,text="", anchor=W) + self.Checkbutton_Preprocess_CRC.place(x=xd_entry_L, y=D_Yloc, width=75, height=23) + self.Checkbutton_Preprocess_CRC.configure(variable=self.pre_pr_crc) + + D_Yloc=D_Yloc+D_dY + self.Label_N_EGV_Passes = Label(egv_send,text="Number of EGV Passes") + self.Label_N_EGV_Passes.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + self.Entry_N_EGV_Passes = Entry(egv_send,width="15") + self.Entry_N_EGV_Passes.place(x=xd_entry_L, y=D_Yloc, width=w_entry, height=23) + self.Entry_N_EGV_Passes.configure(textvariable=self.n_egv_passes) + self.n_egv_passes.trace_variable("w", self.Entry_N_EGV_Passes_Callback) + self.entry_set(self.Entry_N_EGV_Passes,self.Entry_N_EGV_Passes_Check(),2) + + D_Yloc=D_Yloc+D_dY + font_entry_width=215 + self.Label_Inkscape_Path = Label(egv_send,text="EGV File:") + self.Label_Inkscape_Path.place(x=xd_label_L, y=D_Yloc, width=w_label, height=21) + + EGV_Name = os.path.basename(EGV_filename) + self.Label_Inkscape_Path = Label(egv_send,text=EGV_Name,anchor="w") #,bg="yellow") + self.Label_Inkscape_Path.place(x=xd_entry_L, y=D_Yloc, width=200, height=21,anchor="nw") + + ## Buttons ## + egv_send.update_idletasks() + Ybut=int(egv_send.winfo_height())-30 + Xbut=int(egv_send.winfo_width()/2) + + self.EGV_Close = Button(egv_send,text="Cancel") + self.EGV_Close.place(x=Xbut, y=Ybut, width=130, height=30, anchor="e") + self.EGV_Close.bind("", self.Close_Current_Window_Click) + + def Close_and_Send_Click(): + win_id=self.grab_current() + win_id.destroy() + self.Open_EGV(EGV_filename, n_passes=int( float(self.n_egv_passes.get()) )) + + self.EGV_Send = Button(egv_send,text="Send EGV Data",command=Close_and_Send_Click) + self.EGV_Send.place(x=Xbut, y=Ybut, width=130, height=30, anchor="w") + ################################################################################ + + +################################################################################ +# Function for outputting messages to different locations # +# depending on what options are enabled # +################################################################################ +def fmessage(text,newline=True): + global QUIET + if (not QUIET): + if newline==True: + try: + sys.stdout.write(text) + sys.stdout.write("\n") + debug_message(traceback.format_exc()) + except: + debug_message(traceback.format_exc()) + pass + else: + try: + sys.stdout.write(text) + debug_message(traceback.format_exc()) + except: + debug_message(traceback.format_exc()) + pass + +################################################################################ +# Message Box # +################################################################################ +def message_box(title,message): + title = "%s (K40 Whisperer V%s)" %(title,version) + if VERSION == 3: + tkinter.messagebox.showinfo(title,message) + else: + tkMessageBox.showinfo(title,message) + pass + +################################################################################ +# Message Box ask OK/Cancel # +################################################################################ +def message_ask_ok_cancel(title, mess): + if VERSION == 3: + result=tkinter.messagebox.askokcancel(title, mess) + else: + result=tkMessageBox.askokcancel(title, mess) + return result + +################################################################################ +# Debug Message Box # +################################################################################ +def debug_message(message): + global DEBUG + title = "Debug Message" + if DEBUG: + if VERSION == 3: + tkinter.messagebox.showinfo(title,message) + else: + tkMessageBox.showinfo(title,message) + pass + +################################################################################ +# Choose Units Dialog # +################################################################################ +if VERSION < 3: + import tkSimpleDialog +else: + import tkinter.simpledialog as tkSimpleDialog + +class UnitsDialog(tkSimpleDialog.Dialog): + def body(self, master): + self.resizable(0,0) + self.title('Units') + self.iconname("Units") + + self.uom = StringVar() + self.uom.set("Millimeters") + + Label(master, text="Select DXF Import Units:").grid(row=0) + Radio_Units_IN = Radiobutton(master,text="Inches", value="Inches") + Radio_Units_MM = Radiobutton(master,text="Millimeters", value="Millimeters") + Radio_Units_CM = Radiobutton(master,text="Centimeters", value="Centimeters") + + Radio_Units_IN.grid(row=1, sticky=W) + Radio_Units_MM.grid(row=2, sticky=W) + Radio_Units_CM.grid(row=3, sticky=W) + + Radio_Units_IN.configure(variable=self.uom) + Radio_Units_MM.configure(variable=self.uom) + Radio_Units_CM.configure(variable=self.uom) + + def apply(self): + self.result = self.uom.get() + return + + +class toplevel_dummy(): + def winfo_exists(self): + return False + +class pxpiDialog(tkSimpleDialog.Dialog): + + def __init__(self, + parent, + units = "mm", + SVG_Size =None, + SVG_ViewBox =None, + SVG_inkscape_version=None): + + self.result = None + self.svg_pxpi = StringVar() + self.other = StringVar() + self.svg_width = StringVar() + self.svg_height = StringVar() + self.svg_units = StringVar() + self.fixed_size = False + self.svg_units.set(units) + if units=="mm": + self.scale=1.0 + else: + self.scale=1/25.4 + + + ################################### + ## Set initial pxpi # + ################################### + pxpi = 72.0 + if SVG_inkscape_version != None: + if SVG_inkscape_version >=.92: + pxpi = 96.0 + else: + pxpi = 90.0 + + self.svg_pxpi.set("%d"%(pxpi)) + self.other.set("%d"%(pxpi)) + + ################################### + ## Set minx/miny # + ################################### + if SVG_ViewBox!=None and SVG_ViewBox[0]!=None and SVG_ViewBox[1]!=None: + self.minx_pixels = SVG_ViewBox[0] + self.miny_pixels = SVG_ViewBox[1] + else: + self.minx_pixels = 0.0 + self.miny_pixels = 0.0 + + ################################### + ## Set Initial Size # + ################################### + if SVG_Size!=None and SVG_Size[2]!=None and SVG_Size[3]!=None: + self.width_pixels = SVG_Size[2] + self.height_pixels = SVG_Size[3] + elif SVG_ViewBox!=None and SVG_ViewBox[2]!=None and SVG_ViewBox[3]!=None: + self.width_pixels = SVG_ViewBox[2] + self.height_pixels = SVG_ViewBox[3] + else: + self.width_pixels = 500.0 + self.height_pixels = 500.0 + ################################### + ## Set Initial Size # + ################################### + if SVG_Size[0]!=None and SVG_Size[1]!=None: + width = SVG_Size[0] + height = SVG_Size[1] + self.fixed_size=True + else: + width = self.width_pixels/float(self.svg_pxpi.get())*25.4 + height = self.height_pixels/float(self.svg_pxpi.get())*25.4 + + self.svg_width.set("%f" %(width*self.scale)) + self.svg_height.set("%f" %(height*self.scale)) + ################################### + tkSimpleDialog.Dialog.__init__(self, parent) + + + def body(self, master): + self.resizable(0,0) + self.title('SVG Import Scale:') + self.iconname("SVG Scale") + + ########################################################################### + def Entry_custom_Check(): + try: + value = float(self.other.get()) + if value <= 0.0: + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_custom_Callback(varName, index, mode): + if Entry_custom_Check() > 0: + Entry_Custom_pxpi.configure( bg = 'red' ) + else: + Entry_Custom_pxpi.configure( bg = 'white' ) + pxpi = float(self.other.get()) + width = self.width_pixels/pxpi*25.4 + height = self.height_pixels/pxpi*25.4 + if self.fixed_size: + pass + else: + Set_Value(width=width*self.scale,height=height*self.scale) + self.svg_pxpi.set("custom") + ################################################### + def Entry_Width_Check(): + try: + value = float(self.svg_width.get())/self.scale + if value <= 0.0: + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Width_Callback(varName, index, mode): + if Entry_Width_Check() > 0: + Entry_Custom_Width.configure( bg = 'red' ) + else: + Entry_Custom_Width.configure( bg = 'white' ) + width = float(self.svg_width.get())/self.scale + pxpi = self.width_pixels*25.4/width + height = self.height_pixels/pxpi*25.4 + Set_Value(other=pxpi,height=height*self.scale) + self.svg_pxpi.set("custom") + ################################################### + def Entry_Height_Check(): + try: + value = float(self.svg_height.get()) + if value <= 0.0: + return 2 # Value is invalid number + except: + return 3 # Value not a number + return 0 # Value is a valid number + def Entry_Height_Callback(varName, index, mode): + if Entry_Height_Check() > 0: + Entry_Custom_Height.configure( bg = 'red' ) + else: + Entry_Custom_Height.configure( bg = 'white' ) + height = float(self.svg_height.get())/self.scale + pxpi = self.height_pixels*25.4/height + width = self.width_pixels/pxpi*25.4 + Set_Value(other=pxpi,width=width*self.scale) + self.svg_pxpi.set("custom") + ################################################### + def SVG_pxpi_callback(varName, index, mode): + if self.svg_pxpi.get() == "custom": + try: + pxpi=float(self.other.get()) + except: + pass + else: + pxpi=float(self.svg_pxpi.get()) + width = self.width_pixels/pxpi*25.4 + height = self.height_pixels/pxpi*25.4 + if self.fixed_size: + Set_Value(other=pxpi) + else: + Set_Value(other=pxpi,width=width*self.scale,height=height*self.scale) + + ########################################################################### + + def Set_Value(other=None,width=None,height=None): + self.svg_pxpi.trace_vdelete("w",self.trace_id_svg_pxpi) + self.other.trace_vdelete("w",self.trace_id_pxpi) + self.svg_width.trace_vdelete("w",self.trace_id_width) + self.svg_height.trace_vdelete("w",self.trace_id_height) + self.update_idletasks() + + if other != None: + self.other.set("%f" %(other)) + if width != None: + self.svg_width.set("%f" %(width)) + if height != None: + self.svg_height.set("%f" %(height)) + + self.trace_id_svg_pxpi = self.svg_pxpi.trace_variable("w", SVG_pxpi_callback) + self.trace_id_pxpi = self.other.trace_variable("w", Entry_custom_Callback) + self.trace_id_width = self.svg_width.trace_variable("w", Entry_Width_Callback) + self.trace_id_height = self.svg_height.trace_variable("w", Entry_Height_Callback) + self.update_idletasks() + + ########################################################################### + t0="This dialog opens if the SVG file you are opening\n" + t1="does not contain enough information to determine\n" + t2="the intended physical size of the design.\n" + t3="Select an SVG Import Scale:\n" + Title_Text0 = Label(master, text=t0+t1+t2, anchor=W) + Title_Text1 = Label(master, text=t3, anchor=W) + + Radio_SVG_pxpi_96 = Radiobutton(master,text=" 96 units/in", value="96") + Label_SVG_pxpi_96 = Label(master,text="(File saved with Inkscape v0.92 or newer)", anchor=W) + + Radio_SVG_pxpi_90 = Radiobutton(master,text=" 90 units/in", value="90") + Label_SVG_pxpi_90 = Label(master,text="(File saved with Inkscape v0.91 or older)", anchor=W) + + Radio_SVG_pxpi_72 = Radiobutton(master,text=" 72 units/in", value="72") + Label_SVG_pxpi_72 = Label(master,text="(File saved with Adobe Illustrator)", anchor=W) + + Radio_Res_Custom = Radiobutton(master,text=" Custom:", value="custom") + Bottom_row = Label(master, text=" ") + + + Entry_Custom_pxpi = Entry(master,width="10") + Entry_Custom_pxpi.configure(textvariable=self.other) + Label_pxpi_units = Label(master,text="units/in", anchor=W) + self.trace_id_pxpi = self.other.trace_variable("w", Entry_custom_Callback) + + Label_Width = Label(master,text="Width", anchor=W) + Entry_Custom_Width = Entry(master,width="10") + Entry_Custom_Width.configure(textvariable=self.svg_width) + Label_Width_units = Label(master,textvariable=self.svg_units, anchor=W) + self.trace_id_width = self.svg_width.trace_variable("w", Entry_Width_Callback) + + Label_Height = Label(master,text="Height", anchor=W) + Entry_Custom_Height = Entry(master,width="10") + Entry_Custom_Height.configure(textvariable=self.svg_height) + Label_Height_units = Label(master,textvariable=self.svg_units, anchor=W) + self.trace_id_height = self.svg_height.trace_variable("w", Entry_Height_Callback) + + if self.fixed_size == True: + Entry_Custom_Width.configure(state="disabled") + Entry_Custom_Height.configure(state="disabled") + ########################################################################### + rn=0 + Title_Text0.grid(row=rn,column=0,columnspan=5, sticky=W) + + rn=rn+1 + Title_Text1.grid(row=rn,column=0,columnspan=5, sticky=W) + + rn=rn+1 + Radio_SVG_pxpi_96.grid( row=rn, sticky=W) + Label_SVG_pxpi_96.grid( row=rn, column=1,columnspan=50, sticky=W) + + rn=rn+1 + Radio_SVG_pxpi_90.grid( row=rn, sticky=W) + Label_SVG_pxpi_90.grid( row=rn, column=1,columnspan=50, sticky=W) + + rn=rn+1 + Radio_SVG_pxpi_72.grid( row=rn, column=0, sticky=W) + Label_SVG_pxpi_72.grid( row=rn, column=1,columnspan=50, sticky=W) + + rn=rn+1 + Radio_Res_Custom.grid( row=rn, column=0, sticky=W) + Entry_Custom_pxpi.grid( row=rn, column=1, sticky=E) + Label_pxpi_units.grid( row=rn, column=2, sticky=W) + + rn=rn+1 + Label_Width.grid( row=rn, column=0, sticky=E) + Entry_Custom_Width.grid( row=rn, column=1, sticky=E) + Label_Width_units.grid( row=rn, column=2, sticky=W) + + rn=rn+1 + Label_Height.grid( row=rn, column=0, sticky=E) + Entry_Custom_Height.grid( row=rn, column=1, sticky=E) + Label_Height_units.grid( row=rn, column=2, sticky=W) + + rn=rn+1 + Bottom_row.grid(row=rn,columnspan=50) + + Radio_SVG_pxpi_96.configure (variable=self.svg_pxpi) + Radio_SVG_pxpi_90.configure (variable=self.svg_pxpi) + Radio_SVG_pxpi_72.configure (variable=self.svg_pxpi) + Radio_Res_Custom.configure (variable=self.svg_pxpi) + self.trace_id_svg_pxpi = self.svg_pxpi.trace_variable("w", SVG_pxpi_callback) + ########################################################################### + + def apply(self): + width = float(self.svg_width.get())/self.scale + height = float(self.svg_height.get())/self.scale + pxpi = float(self.other.get()) + viewbox = [self.minx_pixels, self.miny_pixels, width/25.4*pxpi, height/25.4*pxpi] + self.result = pxpi,viewbox + return + +################################################################################ +# Startup Application # +################################################################################ + +root = Tk() +app = Application(root) +app.master.title(title_text) +app.master.iconname("K40") +app.master.minsize(800,560) +app.master.geometry("800x560") + +try: + try: + import tkFont + default_font = tkFont.nametofont("TkDefaultFont") + except: + import tkinter.font + default_font = tkinter.font.nametofont("TkDefaultFont") + + default_font.configure(size=9) + default_font.configure(family='arial') + #print(default_font.cget("size")) + #print(default_font.cget("family")) +except: + debug_message("Font Set Failed.") + +################################## Set Icon ######################################## +Icon_Set=False + +try: + #debug_message("Icon set %s" %(sys.argv[0])) + root.iconbitmap(default="emblem") + #debug_message("Icon set worked %s" %(sys.argv[0])) + Icon_Set=True +except: + debug_message(traceback.format_exc()) + Icon_Set=False + +if not Icon_Set: + try: + scorch_ico_B64=b'R0lGODlhEAAQAIYAAA\ + AAABAQEBYWFhcXFxsbGyUlJSYmJikpKSwsLC4uLi8vLzExMTMzMzc3Nzg4ODk5OTs7Oz4+PkJCQkRERE\ + VFRUtLS0xMTE5OTlNTU1dXV1xcXGBgYGVlZWhoaGtra3FxcXR0dHh4eICAgISEhI+Pj5mZmZ2dnaKioq\ + Ojo62tra6urrS0tLi4uLm5ub29vcLCwsbGxsjIyMzMzM/Pz9PT09XV1dbW1tjY2Nzc3OHh4eLi4uXl5e\ + fn5+jo6Ovr6+/v7/Hx8fLy8vT09PX19fn5+fv7+/z8/P7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAEkALAAAAAAQABAAQAj/AJMIFBhBQYAACRIkWbgwAA\ + 4kEFEECACAxBAkGH8ESEKgBZIiAIQECBAjAA8kNwIkScKgQhAkRggAIJACCZIaJxgk2clgAY4OAAoEAO\ + ABCIIDSZIwkIHEBw0YFAAA6IGDCBIkLAhMyICka9cAKZCIRTLEBIMkaA0MSNGjSBEVIgpESEK3LgMCI1\ + aAWCFDA4EDSQInwaDACBEAImLwCAFARw4HFJJcgGADyZEAL3YQcMGBBpIjHx4EeIGkRoMFJgakWADABx\ + IkPwIgcIGkdm0AMJDo1g3jQBIBRZAINyKAwxEkyHEUSMIcwYYbEgwYmQGgyI8SD5Jo327hgIIAAQ5cBs\ + CQpHySgAA7' + icon_im =PhotoImage(data=scorch_ico_B64, format='gif') + root.call('wm', 'iconphoto', root._w, '-default', icon_im) + except: + pass +##################################################################################### + + +if LOAD_MSG != "": + message_box("K40 Whisperer",LOAD_MSG) + +opts, args = None, None +try: + opts, args = getopt.getopt(sys.argv[1:], "hpd",["help", "pi", "debug"]) +except: + print('Unable interpret command line options') + sys.exit() + +for option, value in opts: + if option in ('-h','--help'): + print(' ') + print('Usage: python k40_whisperer.py [-h -p]') + print('-h : print this help (also --help)') + print('-p : Small screen option (for small raspberry pi display) (also --pi)') + sys.exit() + elif option in ('-p','--pi'): + print("pi mode") + app.master.minsize(480,320) + app.master.geometry("480x320") + elif option in ('-d','--debug'): + DEBUG=True + +if DEBUG: + import inspect +debug_message("Debuging is turned on.") + +web_interface = "" +with open('interface.html', 'r') as file: + web_interface = file.read() + +class MyHandler(BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_response(200) + + def do_GET(self): + + parts = self.path.split('/'); + + if (parts[1] == "cmd"): + self.send_response(200) + self.send_header("Content-type", "text/plain") + + if (parts[2] == "move"): + app.Rapid_Move(float(parts[3]), float(parts[4])) + self.end_headers() + self.wfile.write(bytes("OK", "utf-8")) + return + + self.send_response(500) + self.end_headers() + self.wfile.write(bytes("INVALID COMMAND", "utf-8")) + return + + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(bytes(web_interface, "utf-8")) + self.wfile.flush() + +httpd = ThreadingHTTPServer(('', 8000), MyHandler) + +def start_server(): + httpd.serve_forever() + +server_thread = threading.Thread(target=start_server) +server_thread.start() + +#root.attributes('-fullscreen', True) +sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() +root.geometry("%dx%d+0+0" % (sw, sh)) +root.mainloop() + +httpd.shutdown() diff --git a/nano_library.py b/nano_library.py new file mode 100644 index 0000000..b90f490 --- /dev/null +++ b/nano_library.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python +''' +This script comunicated with the K40 Laser Cutter. + +Copyright (C) 2017-2023 Scorch www.scorchworks.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +''' +try: + import usb.core + import usb.util + import usb.backend.libusb0 +except: + print("Unable to load USB library (Sending data to Laser will not work.)") +import sys +import struct +import os +from shutil import copyfile +from egv import egv +import traceback +from windowsinhibitor import WindowsInhibitor +from time import time + +############################################################################## + +class K40_CLASS: + def __init__(self): + self.dev = None + self.n_timeouts = 10 + self.timeout = 200 # Time in milliseconds + self.write_addr = 0x2 # Write address + self.read_addr = 0x82 # Read address + self.read_length= 168 + + #### RESPONSE CODES #### + self.OK = 206 + self.BUFFER_FULL = 238 + self.CRC_ERROR = 207 + self.TASK_COMPLETE = 236 + self.UNKNOWN_2 = 239 #after failed initialization followed by succesful initialization + self.TASK_COMPLETE_M3 = 204 + ####################### + self.hello = [160] + self.unlock = [166,0,73,83,50,80,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,166,15] + self.home = [166,0,73,80,80,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,166,228] + self.estop = [166,0,73,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,166,130] + self.USB_Location = None + + + def say_hello(self): + cnt=0 + status_timeouts = self.n_timeouts + while cnt < status_timeouts: + cnt=cnt+1 + try: + self.send_packet(self.hello) + break + except: + pass + if cnt >= status_timeouts: + return None + + response = None + read_cnt = 0 + while response==None and read_cnt < status_timeouts: + try: + response = self.dev.read(self.read_addr,self.read_length,self.timeout) + except: + response = None + read_cnt = read_cnt + 1 + + DEBUG = False + if response != None: + if DEBUG: + if int(response[0]) != 255: + print ("0: ", response[0]) + elif int(response[1]) != 206: + print ("1: ", response[1]) + elif int(response[2]) != 111: + print ("2: ", response[2]) + elif int(response[3]) != 8: + print ("3: ", response[3]) + elif int(response[4]) != 19: #Get a 3 if you try to initialize when already initialized + print ("4: ", response[4]) + elif int(response[5]) != 0: + print ("5: ", response[5]) + else: + print (".",) + + if response[1]==self.OK or \ + response[1]==self.BUFFER_FULL or \ + response[1]==self.CRC_ERROR or \ + response[1]==self.TASK_COMPLETE or \ + response[1]==self.TASK_COMPLETE_M3 or \ + response[1]==self.UNKNOWN_2: + return response[1] + else: + return 9999 + else: + return None + + + def unlock_rail(self): + self.send_packet(self.unlock) + + def e_stop(self): + self.send_packet(self.estop) + + def home_position(self): + self.send_packet(self.home) + + def reset_usb(self): + self.dev.reset() + + def release_usb(self): + usb.util.dispose_resources(self.dev) + self.dev = None + self.USB_Location = None + + def pause_un_pause(self): + try: + self.send_data([ord('P'),ord('N')]) + except: + pass + + def unfreeze(self): + try: + self.send_data([ord('F'),ord('N'),ord('S'),ord('E')]) + #print("unfreeze sent") + except: + pass + + + ####################################################################### + # The one wire CRC algorithm is derived from the OneWire.cpp Library + # The latest version of this library may be found at: + # http://www.pjrc.com/teensy/td_libs_OneWire.html + ####################################################################### + def OneWireCRC(self,line): + crc=0 + for i in range(len(line)): + inbyte=line[i] + for j in range(8): + mix = (crc ^ inbyte) & 0x01 + crc >>= 1 + if (mix): + crc ^= 0x8C + inbyte >>= 1 + return crc + ####################################################################### + def none_function(self,dummy=None,bgcolor=None): + #Don't delete this function (used in send_data) + return False + + def send_data(self,data,update_gui=None,stop_calc=None,passes=1,preprocess_crc=True, wait_for_laser=False): + if stop_calc == None: + stop_calc=[] + stop_calc.append(0) + if update_gui == None: + update_gui = self.none_function + + NoSleep = WindowsInhibitor() + NoSleep.inhibit() + + blank = [166,0,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,166,80] + packets = [] + packet = blank[:] + cnt=2 + len_data = len(data) + for j in range(passes): + if j == 0: + istart = 0 + else: + istart = 1 + if passes > 1: + if j == passes-1: + data[-4]=ord("F") + else: + data[-4]=ord("@") + timestamp=0 + for i in range(istart,len_data): + if cnt > 31: + packet[-1] = self.OneWireCRC(packet[1:len(packet)-2]) + stamp=int(3*time()) #update every 1/3 of a second + if not preprocess_crc: + self.send_packet_w_error_checking(packet,update_gui,stop_calc) + if (stamp != timestamp): + timestamp=stamp #interlock + update_gui("Sending Data to Laser = %.1f%%" %(100.0*float(i)/float(len_data))) + else: + packets.append(packet) + if (stamp != timestamp): + timestamp=stamp #interlock + update_gui("Calculating CRC data and Generate Packets: %.1f%%" %(100.0*float(i)/float(len_data))) + packet = blank[:] + cnt = 2 + + if stop_calc[0]==True: + NoSleep.uninhibit() + self.stop_sending_data() + #raise Exception("Action Stopped by User.") + packet[cnt]=data[i] + cnt=cnt+1 + packet[-1]=self.OneWireCRC(packet[1:len(packet)-2]) + if not preprocess_crc: + self.send_packet_w_error_checking(packet,update_gui,stop_calc) + if cnt > 31: + self.send_packet_w_error_checking(blank,update_gui,stop_calc) + + else: + packets.append(packet) + if cnt > 31: + packets.append(blank[:]) + update_gui("CRC data and Packets are Ready") + packet_cnt = 0 + + for line in packets: + update_gui() + self.send_packet_w_error_checking(line,update_gui,stop_calc) + packet_cnt = packet_cnt+1.0 + update_gui( "Sending Data to Laser = %.1f%%" %( 100.0*packet_cnt/len(packets) ) ) + ############################################################## + if wait_for_laser: + self.wait_for_laser_to_finish(update_gui,stop_calc) + NoSleep.uninhibit() + + + def send_packet_w_error_checking(self,line,update_gui=None,stop_calc=None): + timeout_cnt = 1 + crc_cnt = 1 + while True: + if stop_calc[0]: + self.stop_sending_data() + + response = self.say_hello() + if response == self.BUFFER_FULL: + while response == self.BUFFER_FULL: + response = self.say_hello() + update_gui() + if stop_calc[0]: + self.stop_sending_data() + try: + self.send_packet(line) + except: + timeout_cnt=timeout_cnt+1 + if timeout_cnt < self.n_timeouts: + msg = "USB Timeout #%d" %(timeout_cnt) + update_gui(msg,bgcolor='yellow') + else: + msg = "The laser cutter is not responding (%d attempts). Press stop to stop trying!" %(timeout_cnt) + gui_active = update_gui(msg,bgcolor='red') + if not gui_active: + msg = "The laser cutter is not responding after %d attempts." %(timeout_cnt) + raise Exception(msg) + + if timeout_cnt > 20: + # try reconnect to laser + try: + self.initialize_device(self.USB_Location) + except: + pass + + continue + ###################################### + response = self.say_hello() + + if response == self.CRC_ERROR: + crc_cnt=crc_cnt+1 + if crc_cnt < self.n_timeouts: + msg = "Data transmission (CRC) error #%d" %(crc_cnt) + update_gui(msg,bgcolor='yellow') + else: + msg = "There are many data transmission errors (%d). Press stop to stop trying!" %(crc_cnt) + gui_active = update_gui(msg,bgcolor='red') + if not gui_active: + msg = "There are many data transmission errors (%d)." %(crc_cnt) + raise Exception(msg) + continue + elif response == None: + # The controller board is not reporting status. but we will + # assume things are going OK. until we cannot transmit to the controller. + break #break to move on to next packet + + else: #assume: response == self.OK: + break #break to move on to next packet + + + def wait_for_laser_to_finish(self,update_gui=None,stop_calc=None): + FINISHED = False + while not FINISHED: + response = self.say_hello() + if response == self.TASK_COMPLETE or response == self.TASK_COMPLETE_M3: + FINISHED = True + break + elif response == None: + msg = "Laser stopped responding after operation was complete." + update_gui(msg) + #raise Exception(msg) + FINISHED = True + else: #assume: response == self.OK: + msg = "Waiting for the laser to finish." + update_gui(msg) + if stop_calc[0]: + self.stop_sending_data() + + + def stop_sending_data(self): + self.e_stop() + raise Exception("Action Stopped by User.") + + def send_packet(self,line): + self.dev.write(self.write_addr,line,self.timeout) + + def print_command(self,data): + for x in data: + sys.stdout.write(chr(x)) + sys.stdout.write("\n") + + + def rapid_move(self,dxmils,dymils): + if (dxmils!=0 or dymils!=0): + data=[] + egv_inst = egv(target=lambda s:data.append(s)) + egv_inst.make_move_data(dxmils,dymils) + self.send_data(data, wait_for_laser=False) + + def detach_ch341_kernel_driver(self, device=None): + if sys.platform.startswith('linux') and device is not None: + if device.is_kernel_driver_active(0): + try: + device.detach_kernel_driver(0) + print('Device detached from ch341 linux driver') + except usb.core.USBError as e: + print ("Could not detatch from ch341 linux driver: %s" % str(e)) + + def initialize_device(self,USB_Location=None,verbose=False): + try: + self.release_usb() + except: + pass + + backend = usb.backend.libusb0.get_backend() + if backend==None and os.name == 'nt': + exedir = os.path.dirname(sys.executable) + os.environ['PATH'] = exedir + os.pathsep + os.environ['PATH'] + + # Find a laser device + self.dev = None + laser_cnt=0 + if USB_Location == None: + for device in usb.core.find(idVendor=0x1a86, idProduct=0x5512, find_all=True): + self.dev=device + try: + # detach device from linux kernel driver + self.detach_ch341_kernel_driver(device=self.dev) + # set the active configuration. With no arguments, the first + # configuration will be the active one + self.dev.set_configuration() + if (self.say_hello()!=None): + self.USB_Location = (self.dev.bus,self.dev.address) + break + except: + self.dev = None + else: + self.dev = usb.core.find(idVendor=0x1a86, idProduct=0x5512, bus=USB_Location[0], address=USB_Location[1]) + # detach device from linux kernel driver + self.detach_ch341_kernel_driver(device=self.dev) + self.dev.set_configuration() + self.USB_Location = (self.dev.bus,self.dev.address) + + if self.dev is None: + raise Exception("Laser USB Device not found. (libUSB driver may not be installed)") + + if verbose: + print("-------------- dev --------------") + print(self.dev) + # set the active configuration. With no arguments, the first + # configuration will be the active one + #try: + # self.dev.set_configuration() + #except: + # raise Exception("Unable to set USB Device configuration.") + + # get an endpoint instance + cfg = self.dev.get_active_configuration() + if verbose: + print ("-------------- cfg --------------") + print (cfg) + intf = cfg[(0,0)] + if verbose: + print ("-------------- intf --------------") + print (intf) + ep = usb.util.find_descriptor( + intf, + # match the first OUT endpoint + custom_match = \ + lambda e: \ + usb.util.endpoint_direction(e.bEndpointAddress) == \ + usb.util.ENDPOINT_OUT) + if ep == None: + raise Exception("Unable to match the USB 'OUT' endpoint.") + if verbose: + print ("-------------- ep --------------") + print (ep) + #self.dev.clear_halt(ep) + #print self.dev.get_active_configuration() + # dev.ctrl_transfer(bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength = None, 2000) + ctrlxfer = self.dev.ctrl_transfer( 0x40, 177, 0x0102, 0, 0, 2000) + if verbose: + print ("---------- ctrlxfer ------------") + print (ctrlxfer) + + return self.USB_Location + + def hex2dec(self,hex_in): + #format of "hex_in" is ["40","e7"] + dec_out=[] + for a in hex_in: + dec_out.append(int(a,16)) + return dec_out + +if __name__ == "__main__": + k40=K40_CLASS() + run_laser = False + + try: + USB_LOCATION=k40.initialize_device(verbose=False) + + # the following does not work for python 2.5 + except RuntimeError as e: #(RuntimeError, TypeError, NameError, StandardError): + print(e) + print("Exiting...") + os._exit(0) + + print('initialize with location=',USB_LOCATION) + k40.initialize_device(k40.USB_Location,verbose=False) + + print('hello',k40.say_hello()) + #print k40.reset_position() + #print k40.unlock_rail() + print ("DONE") + + + + diff --git a/py2exe_setup.py b/py2exe_setup.py new file mode 100644 index 0000000..52b8026 --- /dev/null +++ b/py2exe_setup.py @@ -0,0 +1,24 @@ +#run this from the command line: python py2exe_setup.py py2exe + +from distutils.core import setup +import py2exe + +setup( + options = { + "py2exe": + { + "dll_excludes": ["crypt32.dll","MSVCP90.dll"], + "excludes": ["numpy"], + "compressed": 1, "optimize": 0, + "includes": ["lxml.etree", "lxml._elementpath", "gzip"], + } + }, + zipfile = None, + windows=[ + { + "script":"k40_whisperer.py", + "icon_resources":[(0,"scorchworks.ico"),(1,"scorchworks.ico")] + } + ], +) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b06217f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +lxml +pyusb +pillow +pyclipper diff --git a/scorchworks.ico b/scorchworks.ico new file mode 100644 index 0000000000000000000000000000000000000000..990458841b641e9cbf537829a954c4550149348e GIT binary patch literal 151878 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 literal 0 HcmV?d00001 diff --git a/simplepath.py b/simplepath.py new file mode 100644 index 0000000..01905d5 --- /dev/null +++ b/simplepath.py @@ -0,0 +1,211 @@ +""" +simplepath.py +functions for digesting paths into a simple list structure + +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +""" +import re, math + +def lexPath(d): + """ + returns and iterator that breaks path data + identifies command and parameter tokens + """ + offset = 0 + length = len(d) + delim = re.compile(r'[ \t\r\n,]+') + command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') + parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + while 1: + m = delim.match(d, offset) + if m: + offset = m.end() + if offset >= length: + break + m = command.match(d, offset) + if m: + yield [d[offset:m.end()], True] + offset = m.end() + continue + m = parameter.match(d, offset) + if m: + yield [d[offset:m.end()], False] + offset = m.end() + continue + #TODO: create new exception + raise Exception('Invalid path data!') +''' +pathdefs = {commandfamily: + [ + implicitnext, + #params, + [casts,cast,cast], + [coord type,x,y,0] + ]} +''' +pathdefs = { + 'M':['L', 2, [float, float], ['x','y']], + 'L':['L', 2, [float, float], ['x','y']], + 'H':['H', 1, [float], ['x']], + 'V':['V', 1, [float], ['y']], + 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], + 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], + 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], + 'T':['T', 2, [float, float], ['x','y']], + 'A':['A', 7, [float, float, float, int, int, float, float], ['r','r','a',0,'s','x','y']], + 'Z':['L', 0, [], []] + } +def parsePath(d): + """ + Parse SVG path and return an array of segments. + Removes all shorthand notation. + Converts coordinates to absolute. + """ + retval = [] + lexer = lexPath(d) + + pen = (0.0,0.0) + subPathStart = pen + lastControl = pen + lastCommand = '' + + while 1: + try: + token, isCommand = next(lexer) + except StopIteration: + break + params = [] + needParam = True + if isCommand: + if not lastCommand and token.upper() != 'M': + raise Exception('Invalid path, must begin with moveto.') + else: + command = token + else: + #command was omited + #use last command's implicit next command + needParam = False + if lastCommand: + if lastCommand.isupper(): + command = pathdefs[lastCommand][0] + else: + command = pathdefs[lastCommand.upper()][0].lower() + else: + raise Exception('Invalid path, no initial command.') + numParams = pathdefs[command.upper()][1] + while numParams > 0: + if needParam: + try: + token, isCommand = next(lexer) + if isCommand: + raise Exception('Invalid number of parameters') + except StopIteration: + raise Exception('Unexpected end of path') + cast = pathdefs[command.upper()][2][-numParams] + param = cast(token) + if command.islower(): + if pathdefs[command.upper()][3][-numParams]=='x': + param += pen[0] + elif pathdefs[command.upper()][3][-numParams]=='y': + param += pen[1] + params.append(param) + needParam = True + numParams -= 1 + #segment is now absolute so + outputCommand = command.upper() + + #Flesh out shortcut notation + if outputCommand in ('H','V'): + if outputCommand == 'H': + params.append(pen[1]) + if outputCommand == 'V': + params.insert(0,pen[0]) + outputCommand = 'L' + if outputCommand in ('S','T'): + params.insert(0,pen[1]+(pen[1]-lastControl[1])) + params.insert(0,pen[0]+(pen[0]-lastControl[0])) + if outputCommand == 'S': + outputCommand = 'C' + if outputCommand == 'T': + outputCommand = 'Q' + + #current values become "last" values + if outputCommand == 'M': + subPathStart = tuple(params[0:2]) + pen = subPathStart + if outputCommand == 'Z': + pen = subPathStart + else: + pen = tuple(params[-2:]) + + if outputCommand in ('Q','C'): + lastControl = tuple(params[-4:-2]) + else: + lastControl = pen + lastCommand = command + + retval.append([outputCommand,params]) + return retval + +def formatPath(a): + """Format SVG path data from an array""" + return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) + +def translatePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] += x + elif defs[3][i] == 'y': + params[i] += y + +def scalePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] *= x + elif defs[3][i] == 'y': + params[i] *= y + elif defs[3][i] == 'r': # radius parameter + params[i] *= x + elif defs[3][i] == 's': # sweep-flag parameter + if x*y < 0: + params[i] = 1 - params[i] + elif defs[3][i] == 'a': # x-axis-rotation angle + if y < 0: + params[i] = - params[i] + +def rotatePath(p, a, cx = 0, cy = 0): + if a == 0: + return p + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + x = params[i] - cx + y = params[i + 1] - cy + r = math.sqrt((x**2) + (y**2)) + if r != 0: + theta = math.atan2(y, x) + a + params[i] = (r * math.cos(theta)) + cx + params[i + 1] = (r * math.sin(theta)) + cy + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/simplestyle.py b/simplestyle.py new file mode 100644 index 0000000..6502914 --- /dev/null +++ b/simplestyle.py @@ -0,0 +1,245 @@ +""" +simplestyle.py +Two simple functions for working with inline css +and some color handling on top. + +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + +svgcolors={ + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgreen':'#006400', + 'darkgrey':'#a9a9a9', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightgrey':'#d3d3d3', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370db', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#db7093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'rebeccapurple':'#663399', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' +} + +def parseStyle(s): + """Create a dictionary from the value of an inline style attribute""" + if s is None: + return {} + else: + return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i.strip())]) + +def formatStyle(a): + """Format an inline style attribute from a dictionary""" + return ";".join([att+":"+str(val) for att,val in a.iteritems()]) + +def isColor(c): + """Determine if its a color we can use. If not, leave it unchanged.""" + if c.startswith('#') and (len(c)==4 or len(c)==7): + return True + if c.lower() in svgcolors.keys(): + return True + if c.startswith('rgb('): #however, rgb() shouldnt occur at this point? + return True + #might be "none" or some undefined color constant + return False + +def parseColor(c): + """Creates a rgb int array""" + tmp = svgcolors.get(c.lower()) + if tmp is not None: + c = tmp + elif c.startswith('#') and len(c)==4: + c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:] + elif c.startswith('rgb('): + # remove the rgb(...) stuff + tmp = c.strip()[4:-1] + numbers = [number.strip() for number in tmp.split(',')] + converted_numbers = [] + if len(numbers) == 3: + for num in numbers: + if num.endswith(r'%'): + converted_numbers.append(int(float(num[0:-1])*255/100)) + else: + converted_numbers.append(int(num)) + return tuple(converted_numbers) + else: + return (0,0,0) + try: + r=int(c[1:3],16) + g=int(c[3:5],16) + b=int(c[5:],16) + except: + # unknown color ... + # Return a default color. Maybe not the best thing to do but probably + # better than raising an exception. + return(0,0,0) + return (r,g,b) + +def formatColoria(a): + """int array to #rrggbb""" + return '#%02x%02x%02x' % (a[0],a[1],a[2]) + +def formatColorfa(a): + """float array to #rrggbb""" + return '#%02x%02x%02x' % (int(round(a[0]*255)),int(round(a[1]*255)),int(round(a[2]*255))) + +def formatColor3i(r,g,b): + """3 ints to #rrggbb""" + return '#%02x%02x%02x' % (r,g,b) + +def formatColor3f(r,g,b): + """3 floats to #rrggbb""" + return '#%02x%02x%02x' % (int(round(r*255)),int(round(g*255)),int(round(b*255))) + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/simpletransform.py b/simpletransform.py new file mode 100644 index 0000000..dea626f --- /dev/null +++ b/simpletransform.py @@ -0,0 +1,261 @@ +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr +Copyright (C) 2010 Alvin Penner, penner@vaxxine.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +barraud@math.univ-lille1.fr + +This code defines several functions to make handling of transform +attribute easier. +''' +import inkex, cubicsuperpath, bezmisc, simplestyle +import copy, math, re + +def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): + if transf=="" or transf==None: + return(mat) + stransf = transf.strip() + result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf) +#-- translate -- + if result.group(1)=="translate": + args=result.group(2).replace(',',' ').split() + dx=float(args[0]) + if len(args)==1: + dy=0.0 + else: + dy=float(args[1]) + matrix=[[1,0,dx],[0,1,dy]] +#-- scale -- + if result.group(1)=="scale": + args=result.group(2).replace(',',' ').split() + sx=float(args[0]) + if len(args)==1: + sy=sx + else: + sy=float(args[1]) + matrix=[[sx,0,0],[0,sy,0]] +#-- rotate -- + if result.group(1)=="rotate": + args=result.group(2).replace(',',' ').split() + a=float(args[0])*math.pi/180 + if len(args)==1: + cx,cy=(0.0,0.0) + else: + cx,cy=list(map(float,args[1:])) + matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]] + matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]]) +#-- skewX -- + if result.group(1)=="skewX": + a=float(result.group(2))*math.pi/180 + matrix=[[1,math.tan(a),0],[0,1,0]] +#-- skewY -- + if result.group(1)=="skewY": + a=float(result.group(2))*math.pi/180 + matrix=[[1,0,0],[math.tan(a),1,0]] +#-- matrix -- + if result.group(1)=="matrix": + a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split() + matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]] + + matrix=composeTransform(mat,matrix) + if result.end() < len(stransf): + return(parseTransform(stransf[result.end():], matrix)) + else: + return matrix + +def formatTransform(mat): + return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2])) + +def invertTransform(mat): + det = mat[0][0]*mat[1][1] - mat[0][1]*mat[1][0] + if det !=0: # det is 0 only in case of 0 scaling + # invert the rotation/scaling part + a11 = mat[1][1]/det + a12 = -mat[0][1]/det + a21 = -mat[1][0]/det + a22 = mat[0][0]/det + # invert the translational part + a13 = -(a11*mat[0][2] + a12*mat[1][2]) + a23 = -(a21*mat[0][2] + a22*mat[1][2]) + return [[a11,a12,a13],[a21,a22,a23]] + else: + return[[0,0,-mat[0][2]],[0,0,-mat[1][2]]] + +def composeTransform(M1,M2): + a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0] + a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1] + a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0] + a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1] + + v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2] + v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2] + return [[a11,a12,v1],[a21,a22,v2]] + +def composeParents(node, mat): + trans = node.get('transform') + if trans: + mat = composeTransform(parseTransform(trans), mat) + if node.getparent().tag == inkex.addNS('g','svg'): + mat = composeParents(node.getparent(), mat) + return mat + +def applyTransformToNode(mat,node): + m=parseTransform(node.get("transform")) + newtransf=formatTransform(composeTransform(mat,m)) + node.set("transform", newtransf) + +def applyTransformToPoint(mat,pt): + x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2] + y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2] + pt[0]=x + pt[1]=y + +def applyTransformToPath(mat,path): + for comp in path: + for ctl in comp: + for pt in ctl: + applyTransformToPoint(mat,pt) + +def fuseTransform(node): + if node.get('d')==None: + #FIXME: how do you raise errors? + raise AssertionError('can not fuse "transform" of elements that have no "d" attribute') + t = node.get("transform") + if t == None: + return + m = parseTransform(t) + d = node.get('d') + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + node.set('d', cubicsuperpath.formatPath(p)) + del node.attrib["transform"] + +#################################################################### +##-- Some functions to compute a rough bbox of a given list of objects. +##-- this should be shipped out in an separate file... + +def boxunion(b1,b2): + if b1 is None: + return b2 + elif b2 is None: + return b1 + else: + return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3]))) + +def roughBBox(path): + xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1] + for pathcomp in path: + for ctl in pathcomp: + for pt in ctl: + xmin = min(xmin,pt[0]) + xMax = max(xMax,pt[0]) + ymin = min(ymin,pt[1]) + yMax = max(yMax,pt[1]) + return xmin,xMax,ymin,yMax + +def refinedBBox(path): + xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1] + for pathcomp in path: + for i in range(1, len(pathcomp)): + cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0]) + xmin = min(xmin, cmin) + xMax = max(xMax, cmax) + cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1]) + ymin = min(ymin, cmin) + yMax = max(yMax, cmax) + return xmin,xMax,ymin,yMax + +def cubicExtrema(y0, y1, y2, y3): + cmin = min(y0, y3) + cmax = max(y0, y3) + d1 = y1 - y0 + d2 = y2 - y1 + d3 = y3 - y2 + if (d1 - 2*d2 + d3): + if (d2*d2 > d1*d3): + t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + elif (d3 - d1): + t = -d1/(d3 - d1) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + return cmin, cmax + +def computeBBox(aList,mat=[[1,0,0],[0,1,0]]): + bbox=None + for node in aList: + m = parseTransform(node.get('transform')) + m = composeTransform(mat,m) + #TODO: text not supported! + d = None + if node.get("d"): + d = node.get('d') + elif node.get('points'): + d = 'M' + node.get('points') + elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]: + d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \ + 'h' + node.get('width') + 'v' + node.get('height') + \ + 'h-' + node.get('width') + elif node.tag in [ inkex.addNS('line','svg'), 'line' ]: + d = 'M' + node.get('x1') + ',' + node.get('y1') + \ + ' ' + node.get('x2') + ',' + node.get('y2') + elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \ + inkex.addNS('ellipse','svg'), 'ellipse' ]: + rx = node.get('r') + if rx is not None: + ry = rx + else: + rx = node.get('rx') + ry = node.get('ry') + cx = float(node.get('cx', '0')) + cy = float(node.get('cy', '0')) + x1 = cx - float(rx) + x2 = cx + float(rx) + d = 'M %f %f ' % (x1, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy) + + if d is not None: + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + bbox=boxunion(refinedBBox(p),bbox) + + elif node.tag == inkex.addNS('use','svg') or node.tag=='use': + refid=node.get(inkex.addNS('href','xlink')) + path = '//*[@id="%s"]' % refid[1:] + refnode = node.xpath(path) + bbox=boxunion(computeBBox(refnode,m),bbox) + + bbox=boxunion(computeBBox(node,m),bbox) + return bbox + + +def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): + if node.getparent() is not None: + applyTransformToPoint(invertTransform(composeParents(node, mat)), pt) + return pt + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/svg_reader.py b/svg_reader.py new file mode 100644 index 0000000..719df27 --- /dev/null +++ b/svg_reader.py @@ -0,0 +1,920 @@ +#!/usr/bin/env python +''' +Copyright (C) 2017-2021 Scorch www.scorchworks.com +Derived from dxf_outlines.py by Aaron Spike and Alvin Penner + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +''' + +# standard library +import math +import tempfile, os, sys, shutil + +import zipfile +import re +# local library +import inkex +import simplestyle +import simpletransform +import cubicsuperpath +import cspsubdiv +import traceback + +from PIL import Image +Image.MAX_IMAGE_PIXELS = None + +from lxml import etree + +try: + inkex.localize() +except: + pass + +#### Subprocess timout stuff ###### +from subprocess import Popen, PIPE +from threading import Timer +def run_external(cmd, timeout_sec): + stdout=None + stderr=None + FLAG=[True] + try: + proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE, stdin=PIPE, startupinfo=None) + except Exception as e: + raise Exception("\n%s\n\nExecutable Path:\n%s" %(e,cmd[0])) + if timeout_sec > 0: + kill_proc = lambda p: kill_sub_process(p,timeout_sec, FLAG) + + timer = Timer(timeout_sec, kill_proc, [proc]) + try: + timer.start() + stdout,stderr = proc.communicate() + finally: + timer.cancel() + if not FLAG[0]: + raise Exception("\nInkscape sub-process terminated after %d seconds." %(timeout_sec)) + return (stdout,stderr) + +def kill_sub_process(p,timeout_sec, FLAG): + FLAG[0]=False + p.kill() + +################################## +class SVG_TEXT_EXCEPTION(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class SVG_ENCODING_EXCEPTION(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class SVG_PXPI_EXCEPTION(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class CSS_value_class(): + def __init__(self,type_name,value): + type_name_list = type_name.split('.') + try: + self.object_type = type_name_list[0] + except: + self.object_type = "" + + try: + self.value_name = type_name_list[1] + except: + self.value_name = "" + self.data_string = value + + +class CSS_values_class(): + def __init__(self): + self.CSS_value_list = [] + + def add(self,type_name,value): + self.CSS_value_list.append(CSS_value_class(type_name,value)) + + def get_css_value(self,tag_type,class_val): + value = "" + for entry in self.CSS_value_list: + if entry.object_type == "": + if entry.value_name == class_val: + value = value + entry.data_string + if entry.object_type == tag_type: + if entry.value_name == class_val: + value = entry.data_string + break + return value + + +class SVG_READER(inkex.Effect): + def __init__(self): + inkex.Effect.__init__(self) + self.flatness = 0.01 + self.image_dpi = 1000 + self.inkscape_exe_list = [] + self.inkscape_exe_list.append("C:\\Program Files\\Inkscape\\bin\\inkscape.exe") + self.inkscape_exe_list.append("C:\\Program Files (x86)\\Inkscape\\bin\\inkscape.exe") + self.inkscape_exe_list.append("C:\\Program Files\\Inkscape\\inkscape.exe") + self.inkscape_exe_list.append("C:\\Program Files (x86)\\Inkscape\\inkscape.exe") + self.inkscape_exe_list.append("/usr/bin/inkscape") + self.inkscape_exe_list.append("/usr/local/bin/inkscape") + self.inkscape_exe_list.append("/Applications/Inkscape.app/Contents/Resources/bin/inkscape") + self.inkscape_exe_list.append("/Applications/Inkscape.app/Contents/MacOS/Inkscape") + self.inkscape_exe = None + self.lines =[] + self.Cut_Type = {} + self.Xsize=40 + self.Ysize=40 + self.raster = True + self.SVG_dpi = None + + self.SVG_inkscape_version = None + self.SVG_size_mm = None + self.SVG_size_px = None + self.SVG_ViewBox = None + + self.raster_PIL = None + self.cut_lines = [] + self.eng_lines = [] + self.id_cnt = 0 + + self.png_area = "--export-area-page" + self.timout = 180 #timeout time for external calls to Inkscape in seconds + + self.layers = ['0'] + self.layer = '0' + self.layernames = [] + self.txt2paths = False + self.CSS_values = CSS_values_class() + + def parse_svg(self,filename): + try: + self.parse(filename) + #self.parse(filename, encoding='utf-8') + except Exception as e: + exception_msg = "%s" %(e) + if exception_msg.find("encoding"): + self.parse(filename,encoding="ISO-8859-1") + else: + raise Exception(e) + + def set_inkscape_path(self,PATH): + if PATH!=None: + self.inkscape_exe_list.insert(0,PATH) + for location in self.inkscape_exe_list: + if ( os.path.isfile( location ) ): + self.inkscape_exe=location + break + + def colmod(self,r,g,b,path_id): + changed=False + k40_action = 'raster' + delta = 10 + # Check if the color is Red (or close to it) + if (r >= 255-delta) and (g <= delta) and (b <= delta): + k40_action = "cut" + self.Cut_Type[path_id]=k40_action + (r,g,b) = (255,255,255) + changed=True + # Check if the color is Blue (or close to it) + elif (r <= delta) and (g <= delta) and (b >= 255-delta): + k40_action = "engrave" + self.Cut_Type[path_id]=k40_action + (r,g,b) = (255,255,255) + changed=True + else: + k40_action = "raster" + self.Cut_Type[path_id]=k40_action + changed=False + color_out = '#%02x%02x%02x' %(r,g,b) + return (color_out, changed, k40_action) + + + def process_shape(self, node, mat, group_stroke = None): + ################################# + ### Determine the shape type ### + ################################# + try: + i = node.tag.find('}') + if i >= 0: + tag_type = node.tag[i+1:] + except: + tag_type="" + + ############################################## + ### Set a unique identifier for each shape ### + ############################################## + self.id_cnt=self.id_cnt+1 + path_id = "ID%d"%(self.id_cnt) + sw_flag = False + changed = False + ####################################### + ### Handle references to CSS data ### + ####################################### + class_val = node.get('class') + if class_val: + css_data = "" + for cv in class_val.split(' '): + if css_data!="": + css_data = self.CSS_values.get_css_value(tag_type,cv)+";"+css_data + else: + css_data = self.CSS_values.get_css_value(tag_type,cv) + + # Remove the reference to the CSS data + del node.attrib['class'] + + # Check if a style entry already exists. If it does + # append the the existing style data to the CSS data + # otherwise create a new style entry. + if node.get('style'): + if css_data!="": + css_data = css_data + ";" + node.get('style') + node.set('style', css_data) + else: + node.set('style', css_data) + + style = node.get('style') + self.Cut_Type[path_id]="raster" # Set default type to raster + + text_message_warning = "SVG File with Color Coded Text Outlines Found: (i.e. Blue: engrave/ Red: cut)" + line1 = "SVG File with color coded text outlines found (i.e. Blue: engrave/ Red: cut)." + line2 = "Automatic conversion to paths failed: Try upgrading to Inkscape .90 or later" + line3 = "To convert manually in Inkscape: select the text then select \"Path\"-\"Object to Path\" in the menu bar." + text_message_fatal = "%s\n\n%s\n\n%s" %(line1,line2,line3) + + ############################################## + ### Handle 'style' data outside of style ### + ############################################## + stroke_outside = node.get('stroke') + if not stroke_outside: + stroke_outside = group_stroke + if stroke_outside: + stroke_width_outside = node.get('stroke-width') + + col = stroke_outside + col= col.strip() + if simplestyle.isColor(col): + c=simplestyle.parseColor(col) + (new_val,changed,k40_action)=self.colmod(c[0],c[1],c[2],path_id) + else: + new_val = col + if changed: + node.set('stroke',new_val) + node.set('stroke-width',"0.0") + node.set('k40_action', k40_action) + sw_flag = True + + if sw_flag == True: + if node.tag == inkex.addNS('text','svg') or node.tag == inkex.addNS('flowRoot','svg'): + if (self.txt2paths==False): + raise SVG_TEXT_EXCEPTION(text_message_warning) + else: + raise Exception(text_message_fatal) + + ################################################### + ### Handle 'k40_action' data outside of style ### + ################################################### + if node.get('k40_action'): + k40_action = node.get('k40_action') + changed=True + self.Cut_Type[path_id]=k40_action + + ############################################## + ### Handle 'style' data ### + ############################################## + if style: + declarations = style.split(';') + i_sw = -1 + + sw_prop = 'stroke-width' + for i,decl in enumerate(declarations): + parts = decl.split(':', 2) + if len(parts) == 2: + (prop, col) = parts + prop = prop.strip().lower() + + if prop == 'display' and col == "none": + # display is 'none' return without processing group + return + + if prop == 'k40_action': + changed = True + self.Cut_Type[path_id]=col + + #if prop in color_props: + if prop == sw_prop: + i_sw = i + if prop == 'stroke': + col= col.strip() + if simplestyle.isColor(col): + c=simplestyle.parseColor(col) + (new_val,changed,k40_action)=self.colmod(c[0],c[1],c[2],path_id) + else: + new_val = col + if changed: + declarations[i] = prop + ':' + new_val + declarations.append('k40_action' + ':' + k40_action) + sw_flag = True + if sw_flag == True: + if node.tag == inkex.addNS('text','svg') or node.tag == inkex.addNS('flowRoot','svg'): + if (self.txt2paths==False): + raise SVG_TEXT_EXCEPTION(text_message_warning) + else: + raise Exception(text_message_fatal) + + if i_sw != -1: + declarations[i_sw] = sw_prop + ':' + "0.0" + else: + declarations.append(sw_prop + ':' + "0.0") + node.set('style', ';'.join(declarations)) + ############################################## + + ##################################################### + ### If vector data was found save the path data ### + ##################################################### + if changed: + if node.get('display')=='none': + return + if node.tag == inkex.addNS('path','svg'): + d = node.get('d') + if not d: + return + p = cubicsuperpath.parsePath(d) + elif node.tag == inkex.addNS('rect','svg'): + x = 0.0 + y = 0.0 + if node.get('x'): + x=float(node.get('x')) + if node.get('y'): + y=float(node.get('y')) + + width = float(node.get('width')) + height = float(node.get('height')) + rx = 0.0 + ry = 0.0 + if node.get('rx'): + rx=float(node.get('rx')) + if node.get('ry'): + ry=float(node.get('ry')) + + if max(rx,ry) > 0.0: + if rx==0.0 or ry==0.0: + rx = max(rx,ry) + ry = rx + Rxmax = abs(width)/2.0 + Rymax = abs(height)/2.0 + rx = min(rx,Rxmax) + ry = min(ry,Rymax) + L1 = "M %f,%f %f,%f " %(x+rx , y , x+width-rx , y ) + C1 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width , y+ry ) + L2 = "M %f,%f %f,%f " %(x+width , y+ry , x+width , y+height-ry) + C2 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width-rx , y+height ) + L3 = "M %f,%f %f,%f " %(x+width-rx , y+height , x+rx , y+height ) + C3 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x , y+height-ry) + L4 = "M %f,%f %f,%f " %(x , y+height-ry, x , y+ry ) + C4 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+rx , y ) + d = L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4 + else: + d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height) + p = cubicsuperpath.parsePath(d) + + elif node.tag == inkex.addNS('circle','svg'): + cx = 0.0 + cy = 0.0 + if node.get('cx'): + cx=float(node.get('cx')) + if node.get('cy'): + cy=float(node.get('cy')) + if node.get('r'): + r = float(node.get('r')) + d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+r,cy, r,r,cx,cy+r, r,r,cx-r,cy, r,r,cx,cy-r, r,r,cx+r,cy) + else: #if there is no radius assume it is a path + if node.get('d'): + d = node.get('d') + p = cubicsuperpath.parsePath(d) + else: + raise Exception("Radius of SVG circle is not defined.") + p = cubicsuperpath.parsePath(d) + + elif node.tag == inkex.addNS('ellipse','svg'): + cx = 0.0 + cy = 0.0 + if node.get('cx'): + cx=float(node.get('cx')) + if node.get('cy'): + cy=float(node.get('cy')) + if node.get('r'): + r = float(node.get('r')) + rx = r + ry = r + if node.get('rx'): + rx = float(node.get('rx')) + if node.get('ry'): + ry = float(node.get('ry')) + + d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+rx,cy, rx,ry,cx,cy+ry, rx,ry,cx-rx,cy, rx,ry,cx,cy-ry, rx,ry,cx+rx,cy) + p = cubicsuperpath.parsePath(d) + + elif (node.tag == inkex.addNS('polygon','svg')) or (node.tag == inkex.addNS('polyline','svg')): + points = node.get('points') + if not points: + return + + points = points.replace(',', ' ') + while points.find(' ') > -1: + points = points.replace(' ', ' ') + + points = points.strip().split(" ") + d = "M " + for i in range(0,len(points),2): + x = float(points[i]) + y = float(points[i+1]) + d = d + "%f,%f " %(x,y) + + #Close the loop if it is a ploygon + if node.tag == inkex.addNS('polygon','svg'): + d = d + "Z" + p = cubicsuperpath.parsePath(d) + + elif node.tag == inkex.addNS('line','svg'): + x1 = float(node.get('x1')) + y1 = float(node.get('y1')) + x2 = float(node.get('x2')) + y2 = float(node.get('y2')) + d = "M " + d = "M %f,%f %f,%f" %(x1,y1,x2,y2) + p = cubicsuperpath.parsePath(d) + else: + #print("something was ignored") + #print(node.tag) + return + + trans = node.get('transform') + if trans: + mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans)) + simpletransform.applyTransformToPath(mat, p) + + ########################################## + ## Break Curves down into small lines ### + ########################################## + f = self.flatness + is_flat = 0 + while is_flat < 1: + try: + cspsubdiv.cspsubdiv(p, f) + is_flat = 1 + except IndexError: + break + except: + f += 0.1 + if f>2 : + break + #something has gone very wrong. + ########################################## + rgb=(0,0,0) + for sub in p: + for i in range(len(sub)-1): + x1 = sub[i][1][0] + y1 = sub[i][1][1] + x2 = sub[i+1][1][0] + y2 = sub[i+1][1][1] + self.lines.append([x1,y1,x2,y2,rgb,path_id]) + ##################################################### + ### End of saving the vector path data ### + ##################################################### + + + def process_clone(self, node): + trans = node.get('transform') + x = node.get('x') + y = node.get('y') + mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + if trans: + mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans)) + if x: + mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]]) + if y: + mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]]) + # push transform + if trans or x or y: + self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat)) + # get referenced node + refid = node.get(inkex.addNS('href','xlink')) + #print(refid,node.get('id'),node.get('layer')) + refnode = self.getElementById(refid[1:]) + if refnode is not None: + if refnode.tag == inkex.addNS('g','svg') or refnode.tag == inkex.addNS('switch','svg'): + self.process_group(refnode) + elif refnode.tag == inkex.addNS('use', 'svg'): + #print(refnode,'1') + self.process_clone(refnode) + else: + self.process_shape(refnode, self.groupmat[-1]) + # pop transform + if trans or x or y: + self.groupmat.pop() + + def process_group(self, group): + ############################################## + ### Get color set at group level + stroke_group = group.get('stroke') + if group.get('display')=='none': + return + ############################################## + ### Handle 'style' data + style = group.get('style') + if style: + declarations = style.split(';') + for i,decl in enumerate(declarations): + parts = decl.split(':', 2) + if len(parts) == 2: + (prop, val) = parts + prop = prop.strip().lower() + if prop == 'stroke': + stroke_group = val.strip() + if prop == 'display' and val == "none": + #group display is 'none' return without processing group + return + ############################################## + if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer': + style = group.get('style') + if style: + style = simplestyle.parseStyle(style) + if 'display' in style: + if style['display'] == 'none': + #layer display is 'none' return without processing layer + return + layer = group.get(inkex.addNS('label', 'inkscape')) + + layer = layer.replace(' ', '_') + if layer in self.layers: + self.layer = layer + trans = group.get('transform') + if trans: + self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans))) + for node in group: + if node.tag == inkex.addNS('g','svg') or node.tag == inkex.addNS('switch','svg'): + self.process_group(node) + elif node.tag == inkex.addNS('use', 'svg'): + #print(node.get('id'),'2',node.get('href')) + self.process_clone(node) + + elif node.tag == inkex.addNS('style', 'svg'): + if node.get('type')=="text/css": + self.parse_css(node.text) + + elif node.tag == inkex.addNS('defs', 'svg'): + for sub in node: + if sub.tag == inkex.addNS('style','svg'): + self.parse_css(sub.text) + else: + self.process_shape(node, self.groupmat[-1], group_stroke = stroke_group) + if trans: + self.groupmat.pop() + + def parse_css(self,css_string): + if css_string == None: + return + name_list=[] + value_list=[] + name="" + value="" + i=0 + while i < len(css_string): + c=css_string[i] + if c==",": + i=i+1 + name_list.append(name) + value_list.append(value) + name="" + value="" + continue + if c=="{": + i=i+1 + while i < len(css_string): + c=css_string[i] + i=i+1 + if c=="}": + break + else: + value = value+c + + if len(value_list)>0: + len_value_list = len(value_list) + k=-1 + while abs(k) <= len_value_list and value_list[k]=="": + value_list[k]=value + k=k-1 + name_list.append(name) + value_list.append(value) + name="" + value="" + continue + name=name+c + i=i+1 + for i in range(len(name_list)): + name_list[i]=" ".join(name_list[i].split()) + self.CSS_values.add(name_list[i],value_list[i]) + + + def unit2mm(self, string): #,dpi=None): + if string==None: + return None + # Returns mm given a string representation of units in another system + # a dictionary of unit to user unit conversion factors + uuconv = {'in': 25.4, + 'pt': 25.4/72.0, + 'mm': 1.0, + 'cm': 10.0, + 'm' : 1000.0, + 'km': 1000.0*1000.0, + 'pc': 25.4/6.0, + 'yd': 25.4*12*3, + 'ft': 25.4*12} + + unit = re.compile('(%s)$' % '|'.join(uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + string = string.replace(' ','') + p = param.match(string) + u = unit.search(string) + + if p: + retval = float(p.string[p.start():p.end()]) + else: + return None + if u: + retunit = u.string[u.start():u.end()] + else: + return None + try: + return retval * uuconv[retunit] + except KeyError: + return None + + def unit2px(self, string): + if string==None: + return None + string = string.replace(' ','') + string = string.replace('px','') + try: + retval = float(string) + except: + retval = None + return retval + + + def Make_PNG(self): + #create OS temp folder + tmp_dir = tempfile.mkdtemp() + #tmp_dir = self.tempDir() + if self.inkscape_exe != None: + try: + svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg") + png_temp_file = os.path.join(tmp_dir, "k40w_image.png") + dpi = "%d" %(self.image_dpi) + #print(dpi) + self.document.write(svg_temp_file) + #self.document.write("svg_temp_file.svg", encoding='utf-8') + + # Check Version of Inkscape + cmd = [ self.inkscape_exe, "-V"] + (stdout,stderr)=run_external(cmd, self.timout) + #print(stdout) + if stdout.find(b'Inkscape 1.')==-1: + cmd = [ self.inkscape_exe, self.png_area, "--export-dpi", dpi, \ + "--export-background","rgb(255, 255, 255)","--export-background-opacity", \ + "255" ,"--export-png", png_temp_file, svg_temp_file ] + else: + cmd = [ self.inkscape_exe, self.png_area, "--export-dpi", dpi, \ + "--export-background","rgb(255, 255, 255)","--export-background-opacity", \ + "255" ,"--export-type=png", "--export-filename=%s" %(png_temp_file), svg_temp_file ] + + run_external(cmd, self.timout) + self.raster_PIL = Image.open(png_temp_file) + self.raster_PIL = self.raster_PIL.convert("L") + except Exception as e: + try: + shutil.rmtree(tmp_dir) + except: + pass + error_text = "%s" %(e) + raise Exception("Inkscape Execution Failed (while making raster data).\n%s" %(error_text)) + else: + raise Exception("Inkscape Not found.") + try: + shutil.rmtree(tmp_dir) + except: + raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) ) + + +## def open_cdr_file(self,filename): +## #create OS temp folder +## svg_temp_file=filename +## #tmp_dir = tempfile.mkdtemp() +## tmp_dir = self.tempDir() +## if self.inkscape_exe != None: +## try: +## #svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg") +## txt2path_file = os.path.join(tmp_dir, "txt2path.svg") +## #self.document.write(svg_temp_file) +## cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg",txt2path_file, svg_temp_file ] +## run_external(cmd, self.timout) +## self.parse_svg(txt2path_file) +## except Exception as e: +## raise Exception("Inkscape Execution Failed (while converting text to paths).\n\n"+str(e)) +## else: +## raise Exception("Inkscape Not found.") +## try: +## shutil.rmtree(tmp_dir) +## except: +## raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) ) + + +## def tempDir(self): +## tmpdir = tempfile.mkdtemp() +## if os.path.isdir(tmpdir): +## print("first tmp dir exists") +## print(tmpdir) +## #raw_input("press any key...") +## else: +## print("try again") +## tmpdir_base = tempfile.gettempdir() +## print(tmpdir_base) +## tmpdir = tmpdir_base+"/k40whisperer" +## os.mkdir(tmpdir) +## if not os.path.isdir(tmpdir): +## print("still didn't work") +## #creatte folder intempdir +## #test if new dir exists +## +## return tmpdir + + + def convert_text2paths(self): + #create OS temp folder + tmp_dir = tempfile.mkdtemp() + #tmp_dir = self.tempDir() + if self.inkscape_exe != None: + try: + svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg") + txt2path_file = os.path.join(tmp_dir, "txt2path.svg") + self.document.write(svg_temp_file) + + # Check Version of Inkscape + cmd = [ self.inkscape_exe, "-V"] + (stdout,stderr)=run_external(cmd, self.timout) + if stdout.find(b'Inkscape 1.')==-1: + cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg", \ + txt2path_file, svg_temp_file, ] + else: + cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg", \ + "--export-filename=%s" %(txt2path_file), svg_temp_file, ] + + (stdout,stderr)=run_external(cmd, self.timout) + self.parse_svg(txt2path_file) + except Exception as e: + raise Exception("Inkscape Execution Failed (while converting text to paths).\n\n"+str(e)) + else: + raise Exception("Inkscape Not found.") + try: + shutil.rmtree(tmp_dir) + except: + raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) ) + + + def set_size(self,pxpi,viewbox): + width_mm = viewbox[2]/pxpi*25.4 + height_mm = viewbox[3]/pxpi*25.4 + self.document.getroot().set('width', '%fmm' %(width_mm)) + self.document.getroot().set('height','%fmm' %(height_mm)) + self.document.getroot().set('viewBox', '%f %f %f %f' %(viewbox[0],viewbox[1],viewbox[2],viewbox[3])) + + + def make_paths(self, txt2paths=False ): + self.txt2paths = txt2paths + msg = "" + + if (self.txt2paths): + self.convert_text2paths() + + ################# + ## GET VIEWBOX ## + ################# + view_box_array = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS) #[0] + if view_box_array == []: + view_box_str = None + else: + view_box_str=view_box_array[0] + ################# + ## GET SIZE ## + ################# + h_array = self.document.getroot().xpath('@height', namespaces=inkex.NSS) + w_array = self.document.getroot().xpath('@width' , namespaces=inkex.NSS) + if h_array == []: + h_string = None + else: + h_string = h_array[0] + if w_array == []: + w_string = None + else: + w_string = w_array[0] + ################# + w_mm = self.unit2mm(w_string) + h_mm = self.unit2mm(h_string) + w_px = self.unit2px(w_string) + h_px = self.unit2px(h_string) + self.SVG_Size = [w_mm, h_mm, w_px, h_px] + + if view_box_str!=None: + view_box_list = view_box_str.split(' ') + DXpix= float(view_box_list[0]) + DYpix= float(view_box_list[1]) + Wpix = float(view_box_list[2]) + Hpix = float(view_box_list[3]) + self.SVG_ViewBox = [DXpix, DYpix, Wpix, Hpix] + else: + SVG_ViewBox = None + + if h_mm==None or w_mm==None or self.SVG_ViewBox==None: + line1 = "Cannot determine SVG size. Viewbox missing or Units not set." + raise SVG_PXPI_EXCEPTION("%s" %(line1)) + + scale_h = h_mm/Hpix + scale_w = w_mm/Wpix + Dx = DXpix * scale_w + Dy = DYpix * scale_h + + if abs(1.0-scale_h/scale_w) > .01: + line1 ="SVG Files with different scales in X and Y are not supported.\n" + line2 ="In Inkscape (v0.92): 'File'-'Document Properties'" + line3 ="on the 'Page' tab adjust 'Scale x:' in the 'Scale' section" + raise Exception("%s\n%s\n%s" %(line1,line2,line3)) + + for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS): + if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer': + layer = node.get(inkex.addNS('label', 'inkscape')) + self.layernames.append(layer.lower()) + layer = layer.replace(' ', '_') + if layer and not layer in self.layers: + self.layers.append(layer) + + self.groupmat = [[[scale_w, 0.0, 0.0-Dx], + [0.0 , -scale_h, h_mm+Dy]]] + + self.process_group(self.document.getroot()) + + + ################################################# + xmin= 0.0 + xmax= w_mm + ymin= -h_mm + ymax= 0.0 + self.Make_PNG() + + self.Xsize=xmax-xmin + self.Ysize=ymax-ymin + Xcorner=xmin + Ycorner=ymax + for ii in range(len(self.lines)): + self.lines[ii][0] = self.lines[ii][0]-Xcorner + self.lines[ii][1] = self.lines[ii][1]-Ycorner + self.lines[ii][2] = self.lines[ii][2]-Xcorner + self.lines[ii][3] = self.lines[ii][3]-Ycorner + + self.cut_lines = [] + self.eng_lines = [] + for line in self.lines: + ID=line[5] + if (self.Cut_Type[ID]=="engrave"): + self.eng_lines.append([line[0],line[1],line[2],line[3]]) + elif (self.Cut_Type[ID]=="cut"): + self.cut_lines.append([line[0],line[1],line[2],line[3]]) + else: + pass + +if __name__ == '__main__': + svg_reader = SVG_READER() + #svg_reader.parse("test.svg") + #svg_reader.make_paths() + tests=["100 mm ",".1 m ","4 in ","100 px ", "100 "] + for line in tests: + print(svg_reader.unit2mm(line),svg_reader.unit2px(line)) diff --git a/windowsinhibitor.py b/windowsinhibitor.py new file mode 100644 index 0000000..8fd2f77 --- /dev/null +++ b/windowsinhibitor.py @@ -0,0 +1,58 @@ +import os +import ctypes + +class WindowsInhibitor: + ''' + Prevent OS sleep/hibernate in windows; code from: + https://github.com/h3llrais3r/Deluge-PreventSuspendPlus/blob/master/preventsuspendplus/core.py + API documentation: + https://msdn.microsoft.com/en-us/library/windows/desktop/aa373208(v=vs.85).aspx + ''' + ES_CONTINUOUS = 0x80000000 + ES_SYSTEM_REQUIRED = 0x00000001 + ES_AWAYMODE_REQUIRED = 0x00000040 + + def __init__(self): + pass + + def inhibit(self): + if os.name == 'nt': #Prevent Windows from going to sleep + try: + ctypes.windll.kernel32.SetThreadExecutionState( + WindowsInhibitor.ES_CONTINUOUS | \ + WindowsInhibitor.ES_SYSTEM_REQUIRED) + except: + return False + return True + else: + return False + + def uninhibit(self): + import ctypes + #print("") + if os.name == 'nt': #Allow Windows to go to sleep + try: + ctypes.windll.kernel32.SetThreadExecutionState( + WindowsInhibitor.ES_CONTINUOUS) + except: + return False + return True + else: + return False + + + +if __name__ == "__main__": + # Running this will prevent the computer from going to sleep + # until you press a key to end the program + osSleep = WindowsInhibitor() + print("no sleep = ",osSleep.inhibit()) + #### + try: + raw_input("Press Enter to continue...") + except: + input("Press Enter to continue...") + + if osSleep: + print("stop no sleep = ",osSleep.uninhibit()) + ####