main
Petr Kratina 2023-06-21 21:53:51 +02:00
commit 8b8e587611
31 zmienionych plików z 15692 dodań i 0 usunięć

2
.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,2 @@
dev_venv/
__pycache__

319
Change_Log.txt 100644
Wyświetl plik

@ -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.

253
LaserSpeed.py 100644
Wyświetl plik

@ -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

50
README_Linux.txt 100644
Wyświetl plik

@ -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.

47
README_MacOS.md 100644
Wyświetl plik

@ -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
------------------------------------------------------------------------------------

288
bezmisc.py 100644
Wyświetl plik

@ -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 <http://www.cit.gu.edu.au/~anthony/info/graphics/bezier.curves>
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 <gravesen@mat.dth.dk>
"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

20
build_exe.bat 100644
Wyświetl plik

@ -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

70
convex_hull.py 100644
Wyświetl plik

@ -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)

38
cspsubdiv.py 100644
Wyświetl plik

@ -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

171
cubicsuperpath.py 100644
Wyświetl plik

@ -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<end:
end -=2*pi
NbSectors=int(abs(start-end)*2/pi)+1
dTeta=(end-start)/NbSectors
#v=dTeta*2/pi*0.552
#v=dTeta*2/pi*4*(sqrt(2)-1)/3
v = 4*tan(dTeta/4)/3
#if not sweepflag:
# v*=-1
p=[]
for i in range(0,NbSectors+1,1):
angle=start+i*dTeta
v1=[O[0]+cos(angle)-(-v)*sin(angle),O[1]+sin(angle)+(-v)*cos(angle)]
pt=[O[0]+cos(angle) ,O[1]+sin(angle) ]
v2=[O[0]+cos(angle)- v *sin(angle),O[1]+sin(angle)+ v *cos(angle)]
p.append([v1,pt,v2])
p[ 0][0]=p[ 0][1][:]
p[-1][2]=p[-1][1][:]
mat=matprod((rotmat(teta),[[rx,0],[0,ry]],rotmat(-teta)))
for pts in p:
applymat(mat, pts[0])
applymat(mat, pts[1])
applymat(mat, pts[2])
return(p)
def CubicSuperPath(simplepath):
csp = []
subpath = -1
subpathstart = []
last = []
lastctrl = []
for s in simplepath:
cmd, params = s
if cmd == 'M':
if last:
csp[subpath].append([lastctrl[:],last[:],last[:]])
subpath += 1
csp.append([])
subpathstart = params[:]
last = params[:]
lastctrl = params[:]
elif cmd == 'L':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = params[:]
lastctrl = params[:]
elif cmd == 'C':
csp[subpath].append([lastctrl[:],last[:],params[:2]])
last = params[-2:]
lastctrl = params[2:4]
elif cmd == 'Q':
q0=last[:]
q1=params[0:2]
q2=params[2:4]
x0= q0[0]
x1=1./3*q0[0]+2./3*q1[0]
x2= 2./3*q1[0]+1./3*q2[0]
x3= q2[0]
y0= q0[1]
y1=1./3*q0[1]+2./3*q1[1]
y2= 2./3*q1[1]+1./3*q2[1]
y3= q2[1]
csp[subpath].append([lastctrl[:],[x0,y0],[x1,y1]])
last = [x3,y3]
lastctrl = [x2,y2]
elif cmd == 'A':
arcp=ArcToPath(last[:],params[:])
arcp[ 0][0]=lastctrl[:]
last=arcp[-1][1]
lastctrl = arcp[-1][0]
csp[subpath]+=arcp[:-1]
elif cmd == 'Z':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = subpathstart[:]
lastctrl = subpathstart[:]
#append final superpoint
csp[subpath].append([lastctrl[:],last[:],last[:]])
return csp
def unCubicSuperPath(csp):
a = []
for subpath in csp:
if subpath:
a.append(['M',subpath[0][1][:]])
for i in range(1,len(subpath)):
a.append(['C',subpath[i-1][2][:] + subpath[i][0][:] + subpath[i][1][:]])
return a
def parsePath(d):
return CubicSuperPath(simplepath.parsePath(d))
def formatPath(p):
return simplepath.formatPath(unCubicSuperPath(p))
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99

1405
dxf.py 100644

Plik diff jest za duży Load Diff

131
ecoords.py 100644
Wyświetl plik

@ -0,0 +1,131 @@
#!/usr/bin/python
"""
Copyright (C) <2018> <Scorch>
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 <http://www.gnu.org/licenses/>.
"""
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

727
egv.py 100644
Wyświetl plik

@ -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")

634
embedded_images.py 100644
Wyświetl plik

@ -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\
vW2hEAaNOqBfCvrdu3cOPKlQugrt26D4jR28u3r1+/QwAIHiwYAyt6iBGf2+WpzxpV8uhJlgxqAoDLmC\
+zcEevs+fPoEH/agCgtOnS/1KrXs269T8AsGPDhkKvtu3buHPXHgegt28ApejR2wOguHEACvaEo8dcmp\
gBAKJLBzDiHT16MwBo3w6Dnvfv4MOL//c+bwiA8+jP/1vPvv16APDjw89Fr779+/jz55/XAIB/gAAEAq\
JHTxEAhAkVLnRBj14TABEluqBHbxUAjBlH0OPY0eNHkCBPASBZEsA/lCn/AWDZEkAqejFlzqRZ02bNQA\
B07ty5gZA3ekGFTsPzAMBRpEcVOKPX1Km5W8fm0aNa1epVrFmtYgLQ1SuAf/8AjCULgAo9tGnVrmXb1u\
1btOMELUkAwO7dWfT07uXLt5maCQAECxaxjt5hxIkVL2Z8GAkAyJH/AaBcGQAvepk1b+bc2XNneaySDA\
BQ2vRp1KlVo67Q5hk92LFlz6Zd2zZsSAB07wbwD8Bv4C7oDSde3P/4ceTJlR//AcD5c0X0pE+nXt36de\
zZsYMA0N37P/AAxI8HsIXeefTp1a9n3959emcA5M8XEI/effz59e/n398/QHoCBTYBYPAggH8KF/4D4P\
AhACP0JlKsaPEixowaJ5oB4PGjGHoiR5IsafIkypQibQBo6RLAv5gyZ84EYPOmzUv0dvLs6fMnUKDmYA\
AoarQoAgBKlwJAcIwe1KhSp1KtapWeIQBat2r95/Ur2LBivQIoa9ashWL01rJt6/ZtHABy58oNIIke3r\
x66aUTAeAvYAAgztErbPgw4sSGd0EA4Pix43+SJ1OubPky5n8ANnPuDIBBnXj0RpMubfo06tT/o7chEQ\
AgBS96smfTlg2PzgIAunfzBvDvN/DgwocTL278+D8Aypczb948AQspbQ55ivVL2DNnwnzF2lRoDRQVBw\
CQL2/+/Pl/6tezb+/+Pfz48ufDB2D/Pv78+vfz728f4D+BAwkWNHgQYUKFC/8BcPgQokMBSnDRs3gRY0\
aNGzl2pBdrSAAAI0mWHPkPZUqVK1m2dNkSQEyZMgn8oXcTZ06dOVkxAPATaFChQ4kCcCCLXlKlS5kyPS\
QAQFSpUv9VtXoVa1atALh25fqFXlixY8mWpTcCQFq1A4DRo5fNBAC5c+nWlavCGz160hwA8PsXQC56gw\
kXNnx43hMAixkv//73GHJkyY8BVLZceRY9zZs5d/bMuRIA0aNv0KMHCUBq1QUEAXD9GoABb/SuCQBwG3\
coevTGAPD92wo94cOJFzdO/BQA5cuV/3P+/DkA6dMBeKF3HXt27du5Yy8CAHx4AIPolaczAEB69ekDzK\
FHDx4MAPPpM2hHjx4GAPv5L6EHkJ7AgQQLGjzoBIDChQD+OfwHIKJEAKToWbyIMaPGjRwtqqsCIKTIkS\
RLDhhEL6XKlSxbunwJMyUlADRrAvgHIKdOALTo+fwJNKjQoUB3ADiKNCkAHJva0XvaBYDUqWvoWaXXbA\
0FAFy7ev0aQh29sWTLmj2LdqwpAGzbunULiv+e3Ll069q9ixcvu0w2APj9CxhwBTbO6Bk+jDix4sWMGx\
+WBCCyZAD/AFi+DEANvc2cO3v+DDq0aM+FAJg+TYSe6tWsW7t+DTv2ay0AatsG8C/3PwC8ewNwoI6e8O\
HEixs/jjz5cBAAmjtXRS+69OnUq1u/jl16OAMAunsH8C+8+PEAypsH8EIevfXs27t/Dz++e2ocANi/f1\
+ALHr8+/sHSE/gQIIFDR4k6I4EAIYNAfyDGFHixIgALF68qIjeRo4dPX4EGQ8KAJIlSSaoRU8lPXg1AL\
yECaDCNno1bd7EmVOnzTsAfP70+U/oUKJFjR4VCkDpUqZc3NGDGlWq1Cj/AKxeBRCH3lauXb1yzTYBwF\
iyAOrQQ5tW7Vq07aoAgBtX7j+6de3exZtX714Aff3+/dviUjp6hQ0fRpxY8eLD5iSdABBZ8uTJ/yxfxp\
xZ82bOnT1/xgxA9GjSpU2fRm3632rWrV2/hh1b9mzatWkDwJ1b927evX3/Ax5c+HDixY0fR55cuXAAzZ\
0/hx5d+nTq1QH8w55d+3bu3b1/Bw8ewHjy5cs/6LJrHj327d2/hx8fvjxbVxgAwJ9fv/5//f0D/CdwIM\
GCBg8iTPgPAMOGDhk2EEVvIsWKFi+qE6bpT5w3cAYdAsWOHsmSJk+ipGdJAYCWLl8C+CdzJs2aNm/i/7\
wJYCfPnjSm0QsqdChRoeasAEiqdCnTpkoFkEFHbyrVqlarMlsBYCvXrv++gg0rdizZsQDOokULgRe9tm\
7fwn2rKwGAunbtBhCCqRu9vn7pWXt0AwDhwoYP4KKneDHjxo1tNQAgefLkf5YvY86sWTOAzp47F4BFbz\
Tp0qZNt9sAYDVrADXS0aMnr1EEALZv4wYwghO93uVsAAguHAABZfSOI0+ufLkoAQCeQ3/+bzr16tavA8\
iuPTuDZ/S+gw8vfjy9NQDOo0ewjR49OgDew48vf34UevTm2QCgf78Aa/QA0hM4kGBBg8sSAFC4EMA/hw\
8hRnQIgGJFio7oZdS4kf9jR42oAIQUWYAePXUAUKYEAOlbFQQAFFThRo9eqwEAcOYEQo9euAEAgAZFRY\
9oUaNHkRb1A4BpU6b/oEaVChVAVasABBijt5VrV69fvcYCMJZsBHr00A0AsJYtBQBv4b61QI8eGgB38V\
6hRw8UAL9/V9ATPJhwYcOFbwFQvFjxP8ePHwOQPBlAJnqXMWfWvJkzPUwAQIcGwIsePVAAUKdWndqANX\
r0KgCQPXsJPXpfAOTWjYNeb9+/gQcXrghAceMA/iVXDoB5cwAZ5tGTPp16devXqf8AsJ07AASl6IWnRy\
0QEBdbnNFTT88PAPfv3QejN98VAPv2LcSjt59/f///AOkJHEiQYLsGABIq/MfwH4CHEAHIoEexosWLGD\
NqTBcDgMePIEOKDPmGnsmTKFOqXMmypQgAMGP+mwmgpk0F8ujp3Mmzp8+fQHuKS7MAgNGjSJFKINSOnt\
OnUKO+e0evqtWrWLNqvcpuAICvYP8BGEsWACJ6aNOqXcu2rVpoDQDIletCUjp6ePPi3QGgr99H9AILFt\
xMDQUAiBMrBkCHnuPHkCNLnuwYD4DLmDNnLkavs+fPoEOL/mzsAIDTqFOnFiDkCoDXsH28qQCgtu3buA\
FE8KKMnu/fwIMLHx78FoDjyJMn70WvufPn0KNLn+4cnqgfAQBo3869u3ftFdo8/6NHvrz58+jTq0/vCo\
D79/Dhv6FHv779+/jz699vHx0kgAQADCQ4id5BhAkVLmTY0GHDLwAkTgTwD8BFjACm0ePY0eNHkCFFjv\
wYBsBJlGborWTZ0uVLmDFlwhwGwOZNAP90AuDZs0A6ekGFDiVa1OhRpENVAWDaFAQ9qFGlTqVa1epVqt\
0CAODa9d/XrwDEjgWQi95ZtGnVrmXb1i3aeAIAzKXrjN5dvHn17uXb12/eUwAEDwbwz/DhfwAULwbwgt\
5jyJElT6Zc2TJkIgA0bzZEz/Nn0KFFjyZd2rM8EgBUrwbwz/Vr2ABkzwYghN5t3Ll17+bd23cyBgCEDy\
/wjv/eceTJlS9n3tw5DQDRpQP4V9369esAtG8H4CAbPfDhxY8nX948+G8lAKxn3569knn05M+nX9/+ff\
zzqS0A0N8/QAD/BhIsaJAggIQKE85wR+8hxIgSJ1KE6G4IgIwaMz4QRi/XAAAiRwIwQ+8kypQqV7Jcmc\
4EgJgyY/6rafMmzpw1AfDsyXPBLnpChxItanTovC8AljJdWuAUvahSo4ICYPWq1UH0tnLt6vUr2FcGAJ\
AtS/Yf2rRq17Jt+w8A3LhxF7iiZ/cu3rx2cwUA4PevX0L0BhMubNgPgMSKE3ei5/gx5MiSQR0AYPny5X\
+aN3Pu7PkzaACiR5NuAIke6tT/qlG7owHgNYAw8+jRrm37Nm4wAHbzbkPvN/Dgwi8tAGD8OPJ/ypczb+\
78OfToAKZTrz5dCDF62rdz7+79O/jw3XvpAGD+PHrz/9azb+/+Pfz48ucDqG//Pn4AF7aUSkcPID2BAw\
kWNGjQHKgqFAA0dPgQIoB/EylWtHgRY0aNGzlOBPARZEiRI0mWNEnyX0qVK1m2dPkSZkyZM2MCsHkTZ0\
6dO//19PkTaFChQ4kWNXoUKVEAS5k2dfoU6j+pU6lWtXoVa1atW7l2lQoAbFixY8mWNUv2X1q1a9m2df\
sWbly5c9UCsHsXb169e/n23fsPcGDBgwkXNnwYcWLCABg3/3b8+HGDF1TiJBJFS5gwadGECZv16dAbKS\
0UADB9GnXq1P9Yt3b9GnZs2bNpzwZwG3du3QA0dEGljl5w4cOJFzeOblQWCwCYN3f+HMA/6dOpV7d+HX\
t27dIBdPf+/TsMTevolTd/Hn169evPo6OkAkB8+fPn/7N/H39+/fv5998PEIDAgQQFEjFGL6HChQwbOn\
wIkeGvHQAqWrxY8Z/GjRw7evwIMqRGACRLmgTQII88eixbunzp8hkeGwUA2Lx5M0ALQdno+fwJNGhQeH\
EUADiKNCmAf0ybOn0KNapUqACqWr3qABO9rVy7eu1aCwWAsWTLmj1Lwxe9tWzbunV7af8BgLl06/67iz\
ev3r18+94FADiwYC/u6Bk+jDjxYWIeADh+DDmyZMklmtG7jDmz5sztrAD4DDr0v9GkS5s+jfo0gNWsWT\
OIRS+27Nm0Z6sqACC37t25PxTB4sXKhwIAihs/XhyBKnrMmzt//rwUAgDUq1f/hz279u3cu2MHAD58+A\
vG6Jk/jz49+m4aALh//77JMnr069uvD2wIgP38+U8A+IzeQIIFDRrsFQHAQoYL/z2EGFHiRIoALF60yK\
AXPY4dPX782AnASJIjYXSjl3KcIhgBALx8WQDIJ3n0bFZjAUDnTp1u6P0EGlTo0FgHABxFevTfUqZNnT\
5lCkDq1Kn/juhdxZpV61ZIALx+BfBhHD1643IAQJtW7Vos6+jRI1cCwFy6AIDQw5tX716+9PYAABwY8D\
/ChQ0fRvwPwGLGi2m4oxdZ8mTKlYMBwJwZABl69ODRABBa9GjSo2mwo0evEgDWrQEkoRdb9mzatempQw\
FA927d/3z/Bh4cOADixYlropdc+XLmzZNjABBd+hR69L4hAJBde/YCIDAgABBe/PhU9OjhApBePQBA9N\
y/hx9fvntFAOzft/9P/37+/fUDBCBwIAAH2ughTKhwIUOEswBAjAggHD16HgBgzOggGD1409bRCxnLAY\
CSJgFYokcvEoCWLgeQoydzJs2aNmVW/1sAYCdPAP9+Ag0qFADRogBgyKOndCnTpk6XbgIgdeoIevQ4Ac\
iqNcC4EwC+flVAix69LgDOoh3QjR69FgDewt1Dby7dunbv0n1XAgDfvgD+AQ4sODCAwoYBFKGneDHjxo\
4bfwIgefIKevR8AcisWQALAJ4/exZCjx4OAKZP86FHDxGA1q5z0Istezbt2rRtAMitG8C/3r5//wMgfD\
iAJPSOI0+ufPnyaQCeQwfQjB49FwCuY8+uHQI9enwAgA+PhB69VwDOo/9Abz379u7fv8cBYD59AP/u48\
cPYD9/ADAA0hM4kGBBgwfpMQGwkGEMevTOWQAwkWLFinLo0WMBgP9jxxP06HkCMJJkC3onUaZUuXLlvB\
IAYMYE8I9mzX8AcOYE8EAdPZ8/gQYVOpTeuAIAkCatMY8evTUAoEaVCrUQPXqxAGTVCmAVPXokAIQVq4\
NeWbNn0aZVK+4AALdvAfyTOxdAXbsAdtHTu5dvX79/99oCMJgwgAKs6CX2dSMAAMeOTbSiNzkNAMuXAV\
ShtzkGAM+fLdETPZp0adOn6akCsJo1gH+vXwOQPRsAF3q3cefWvZu3bmYGAAQXDiDAmnT0kCdXDm4KAO\
fPnR+hN336IgMAAICYRo97d+/fwYfv7gRAefMA/qX/B4B9+wLp6MWXP59+ffv15SEBsJ9/fwH/ACWcsM\
ImigoACBMqDNCJnsOHECNKnEhxorcAADJq/McRgMePANbQG0mypMmTKFPSI3cDgMuXMGPKfKklHr2bOH\
Pq3Mmzp88tAIIKBfDvH4CjSAFQo8e0qdOnUKNKdXqJAoCrWLNqxeqhFb2vYMOKHUu2rFmwxACoXQvg3z\
8AcOO+oEe3rt27ePPq1Ruu0AwAgAMLzmGpHL3DiBMnbrepxwACSrDRm0y5suXLmCmHAMC58z8AoEMDgE\
OvtOnTqFOrRj1MRwEKXIDRm027Nj0UAHLrBkWvt+/f9Npt0hEAgPHjxi/RW868ufPn0JeDAUC9unXroO\
hp3869u/fv3MEA/xhPfvwDMsroqV/PBID793zoyZ/fjtOOAADy69+v3xI9gPQEDiRY0OBBepIALGTYsG\
EvehElTqRY0eLELAA0buS4sYKbaPTeACBZEgu9dpx2BADQ0uVLmAGKXKNX0+ZNnDl12nQFwOdPoEB50S\
Na1OhRpEmPygIBwOlTqFGlTqX61AWkdfS0buXa1evXr5AAjCVbtmwxemnVrmXb1u1betfqaABQ1+5dvH\
n1BujhyR09wIEFDyZc2LDgWwAUL2bMmBY9yJElT6Zc2TLlZ2wmAODc2fPnzgF6eHJHz/Rp1KlVr2bduh\
QA2LFlyz5Ez/Zt3Ll17+bd2zYyMQ4ADCc+PP8Aj07t6C1n3tz5c+jRpTu/A8D6dezYp9Dj3t37d/DhxY\
8H3wLAefSc6K1n3979e/jx5cc/AsD+/X//AOznr0AeQHoCBxIsaPAgwoQFoQBo6BAPvYgSJ1KsaPEiRo\
vrBgDo6PHfPwAiRwIgRe8kypQqV7Js6VIlHQAyZ1KhZ/Mmzpw6d/LsuXMSgKBCAfz7B+AoUgAx6DFt6v\
Qp1KhSpz69BOAqVhn0tnLt6vUr2LBiwYYAYPbsv7T/ALBtC+AUvbhy59Kta/cu3rm8APDtG4Ee4MCCBx\
MubPgwYUsAFjMG8O/xPwCSJwOwII8e5syaN3Pu7PlzZnAARpMGEI4e6tT/qlezbu36tep2DQDQrv3vNm\
4AuncD4ELvN/DgwocTL248eAQAypfzouf8OfTo0qdTrw6dCYDs2gH86+79H4Dw4gFgomf+PPr06tezb3\
8+BoD48i/Rq2//Pv78+vfzt48IIACBAwH8M3jQIACFCxXWovcQYkSJEylWtPhwCgCNG+nQ8/gRZEiRI0\
mW9EgKQEqVAP61dPkSQEyZAAIQo3cTZ06dO3n29EnvCwChQ2XQM3oUaVKlS5k2pVcLQFSpUf9VtXr1Hw\
CtW7XKovcVbFixY8mWJesMAwC1a9W+ofcWbly5c+nWrUsKQF69ef/19fvXLwDBgwUzoncYcWLFixk3/0\
6c7QMAyZMpA9AAjl5mzZs5d/b8mfMeAKNJj/53GnVq1QBYt2bthF5s2bNp17Z9mx65FgB49/b9uxE94c\
OJFzd+HLnweUAANHfe/F906dOpRwdwHft1Bs/odff+HXx48eDV5QBwHv15Cr06AHD/HoCJdfTo17d/H3\
/+/MoSAPAPEIBAAP8KGjyIMCGAhQwXcqEHMaLEiRQrQnx3BIDGjRoX5KIH0g6AkSQBBEBFL6XKlSxbul\
w5zwmAmTRn/ruJM6fOnTcB+Pzpk4ArekSLGj2K9Kg8KgCaOm1qYBW9qVSvRQCANSsAIPLoef0KNqzYsf\
Q+BQCANi3af2zbun0L9/8tgLl05z4gRi+v3r18++ZlAyCw4MADOtE7jDjxFQCMGwMo8Iue5MmUK1uu7I\
sBgM2cN//7DDq06NGk/wE4jRo1BF30Wrt+Dfu1HgC0a9MO4Iie7t28dwszACC4cABc6Bk/jjy5cuO0Gg\
B4Dh36v+nUq1u/jr06gO3cuQ/4Qy+8+PHk6XkBgD49ej302rt/D5+evB4A6tsHEIEbvf38+/sHSE/gvD\
8CABxEiPDfQoYNHT6EGBHARIoVZUijl1HjRo2MAHwEeYbeSJIlTZosBUDlSgBO6L2EGVMmvWUqANzEmf\
PfTp49ff4EGrQnAKJFjQJwso3eUqZN6a0TVITJJ3r/Va1exZq16joUALwCYCCN3liyZclWMwJA7Vq2AP\
69hRtX7ly6desCwJtXL94Goej9BRxY8GDChQGf80ZP8WLGiyspABBZ8mQA/yxfxpxZ82bOnTMDAB1aNG\
gBR2jRQ51a9WrWrV2/nucKSAAAtW3frv1P927evX3/Bh5cOADixY0bb6AFlzx6zZ0/hx5devR4sagoAJ\
Bd+/bt/7x/Bx9e/Hjy5c2LB5Be/Xr26QvEQBNKmjx69e3fx28/HrROZVgAHABgIMGCBgf+S6hwIcOGDh\
9CjCgxIYCKFi9izKhxI8eOAP6BDClyJMmSJk+iTIkSAMuWLl/CjClzJst/Nm/iOMypcyfPnj5/Au0JYC\
jRokaPIk36bynTpk6fQo0qdSrVqlapAsiqdStXAP++gg0rdizZsmbP/gsIADs='
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'

BIN
emblem 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 148 KiB

141
ffgeom.py 100644
Wyświetl plik

@ -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

2071
g_code_library.py 100644

Plik diff jest za duży Load Diff

674
gpl-3.0.txt 100644
Wyświetl plik

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <http://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

403
inkex.py 100644
Wyświetl plik

@ -0,0 +1,403 @@
"""
inkex.py
A helper module for creating Inkscape extensions
Copyright (C) 2005,2010 Aaron Spike <aaron@ekips.org> and contributors
Contributors:
Aurelio A. Heckert <aurium(a)gmail.com>
Bulia Byak <buliabyak@users.sf.net>
Nicolas Dufour, nicoduf@yahoo.fr
Peter J. R. Moulder <pjrm@users.sourceforge.net>
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

77
interface.html 100644
Wyświetl plik

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>K40 Whisperer Web Interface</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body x-data="{}">
<div class="flex flex-col">
<div class="">test</div>
<div class="flex">
<div class="grid grid-cols-3 gap-2 w-full">
<div @click="move(-1, 1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 19.5l-15-15m0 0v11.25m0-11.25h11.25" />
</svg>
</div>
<div @click="move(0, 1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
</svg>
</div>
<div @click="move(1, 1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</div>
<div @click="move(-1, 0)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
</div>
<div class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>
</div>
<div @click="move(1, 0)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</div>
<div @click="move(-1, -1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 4.5l-15 15m0 0h11.25m-11.25 0V8.25" />
</svg>
</div>
<div @click="move(0, -1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
</svg>
</div>
<div @click="move(1, -1)" class="bg-gray-200 rounded-sm aspect-square flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25" />
</svg>
</div>
</div>
</div>
</div>
<script>
function move(x, y)
{
cmd('move', [x, y]);
}
function cmd(command, params)
{
fetch('/cmd/'+command+'/'+params.join('/'));
}
</script>
</body>
</html>

1
interpolate.py 100644
Wyświetl plik

@ -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])

5990
k40_whisperer.py 100644

Plik diff jest za duży Load Diff

457
nano_library.py 100644
Wyświetl plik

@ -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")

24
py2exe_setup.py 100644
Wyświetl plik

@ -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")]
}
],
)

4
requirements.txt 100644
Wyświetl plik

@ -0,0 +1,4 @@
lxml
pyusb
pillow
pyclipper

BIN
scorchworks.ico 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 148 KiB

211
simplepath.py 100644
Wyświetl plik

@ -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

245
simplestyle.py 100644
Wyświetl plik

@ -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

261
simpletransform.py 100644
Wyświetl plik

@ -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

920
svg_reader.py 100644
Wyświetl plik

@ -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))

Wyświetl plik

@ -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())
####